merge: 同步上游 v3.0.0 并更新 uv 依赖锁文件
This commit is contained in:
commit
e4f1391a28
|
|
@ -129,7 +129,7 @@ pnpm dev
|
|||
|
||||
### 后端 (Python)
|
||||
|
||||
- 使用 Python 3.13+ 类型注解
|
||||
- 使用 Python 3.11+ 类型注解
|
||||
- 遵循 PEP 8 代码风格
|
||||
- 使用 Ruff 进行代码格式化和检查
|
||||
- 使用 mypy 进行类型检查
|
||||
|
|
|
|||
674
LICENSE
674
LICENSE
|
|
@ -1,21 +1,661 @@
|
|||
MIT License
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (c) 2025 lintsinghua
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Preamble
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
|
|
|||
106
README.md
106
README.md
|
|
@ -1,28 +1,32 @@
|
|||
# DeepAudit - 人人拥有的 AI 审计战队,让漏洞挖掘触手可及 🦸♂️
|
||||
|
||||
> 让代码漏洞挖掘像呼吸一样简单,小白也能轻松挖洞
|
||||
|
||||
<div style="width: 100%; max-width: 600px; margin: 0 auto;">
|
||||
<img src="frontend/public/images/logo.png" alt="DeepAudit Logo" style="width: 100%; height: auto; display: block; margin: 0 auto;">
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<img src="frontend/public/DeepAudit.gif" alt="DeepAudit Demo" width="90%">
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/lintsinghua/DeepAudit/releases)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/lintsinghua/DeepAudit/releases)
|
||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||
[](https://reactjs.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://fastapi.tiangolo.com/)
|
||||
[](https://www.python.org/)
|
||||
[](https://www.python.org/)
|
||||
[](https://deepwiki.com/lintsinghua/DeepAudit)
|
||||
|
||||
[](https://github.com/lintsinghua/DeepAudit/stargazers)
|
||||
[](https://github.com/lintsinghua/DeepAudit/network/members)
|
||||
|
||||
<a href="https://trendshift.io/repositories/15634" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15634" alt="lintsinghua%2FDeepAudit | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
<p align="center">
|
||||
<strong>简体中文</strong> | <a href="README_EN.md">English</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<img src="frontend/public/DeepAudit.gif" alt="DeepAudit Demo" width="90%">
|
||||
</div>
|
||||
|
||||
---
|
||||
|
|
@ -168,17 +172,17 @@ DeepAudit/
|
|||
curl -fsSL https://raw.githubusercontent.com/lintsinghua/DeepAudit/v3.0.0/docker-compose.prod.yml | docker compose -f - up -d
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>🇨🇳 国内加速部署(点击展开)</summary>
|
||||
## 🇨🇳 国内加速部署(作者亲测非常无敌之快)
|
||||
|
||||
使用南京大学镜像站加速拉取 Docker 镜像(将 `ghcr.io` 替换为 `ghcr.nju.edu.cn`):
|
||||
|
||||
```bash
|
||||
# 国内加速版 - 使用南京大学 GHCR 镜像站
|
||||
curl -fsSL https://raw.githubusercontent.com/lintsinghua/DeepAudit/main/docker-compose.prod.cn.yml | docker compose -f - up -d
|
||||
curl -fsSL https://raw.githubusercontent.com/lintsinghua/DeepAudit/v3.0.0/docker-compose.prod.cn.yml | docker compose -f - up -d
|
||||
```
|
||||
<details>
|
||||
<summary>手动拉取镜像(如需单独拉取)(点击展开)</summary>
|
||||
|
||||
**手动拉取镜像(如需单独拉取):**
|
||||
```bash
|
||||
# 前端镜像
|
||||
docker pull ghcr.nju.edu.cn/lintsinghua/deepaudit-frontend:latest
|
||||
|
|
@ -189,9 +193,39 @@ docker pull ghcr.nju.edu.cn/lintsinghua/deepaudit-backend:latest
|
|||
# 沙箱镜像
|
||||
docker pull ghcr.nju.edu.cn/lintsinghua/deepaudit-sandbox:latest
|
||||
```
|
||||
</details>
|
||||
|
||||
> 💡 镜像源由 [南京大学开源镜像站](https://mirrors.nju.edu.cn/) 提供支持
|
||||
|
||||
<details>
|
||||
<summary>💡 配置 Docker 镜像加速(可选,进一步提升拉取速度)(点击展开)</summary>
|
||||
|
||||
如果拉取镜像仍然较慢,可以配置 Docker 镜像加速器。编辑 Docker 配置文件并添加以下镜像源:
|
||||
|
||||
**Linux / macOS**:编辑 `/etc/docker/daemon.json`
|
||||
|
||||
**Windows**:右键 Docker Desktop 图标 → Settings → Docker Engine
|
||||
|
||||
```json
|
||||
{
|
||||
"registry-mirrors": [
|
||||
"https://docker.1ms.run",
|
||||
"https://dockerproxy.com",
|
||||
"https://hub.rat.dev"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
保存后重启 Docker 服务:
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
sudo systemctl restart docker
|
||||
|
||||
# macOS / Windows
|
||||
# 重启 Docker Desktop 应用
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
> 🎉 **启动成功!** 访问 http://localhost:3000 开始体验。
|
||||
|
|
@ -348,7 +382,7 @@ DeepSeek-Coder · Codestral<br/>
|
|||
</tr>
|
||||
</table>
|
||||
|
||||
> 💡 支持 API 中转站,解决网络访问问题 | 详细配置 → [LLM 平台支持](docs/LLM_PROVIDERS.md)
|
||||
💡 支持 API 中转站,解决网络访问问题 | 详细配置 → [LLM 平台支持](docs/LLM_PROVIDERS.md)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -372,15 +406,14 @@ DeepSeek-Coder · Codestral<br/>
|
|||
|
||||
我们正在持续演进,未来将支持更多语言和更强大的 Agent 能力。
|
||||
|
||||
- [x] **v1.0**: 基础静态分析,集成 Semgrep
|
||||
- [x] **v2.0**: 引入 RAG 知识库,支持 Docker 安全沙箱
|
||||
- [x] **v3.0**: **Multi-Agent 协作架构** (Current)
|
||||
- [ ] 支持更多漏洞验证 PoC 模板
|
||||
- [ ] 支持更多语言
|
||||
- [x] 基础静态分析,集成 Semgrep
|
||||
- [x] 引入 RAG 知识库,支持 Docker 安全沙箱
|
||||
- [x] **Multi-Agent 协作架构** (Current)
|
||||
- [ ] 支持更真实的模拟服务环境,进行更真实漏洞验证流程
|
||||
- [ ] 沙箱从function_call优化集成为稳定MCP服务
|
||||
- [ ] **自动修复 (Auto-Fix)**: Agent 直接提交 PR 修复漏洞
|
||||
- [ ] **增量PR审计**: 持续跟踪 PR 变更,智能分析漏洞,并集成CI/CD流程
|
||||
- [ ] **优化RAG**: 支持自定义知识库
|
||||
- [ ] **优化Agent**: 支持自定义Agent
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -390,9 +423,32 @@ DeepSeek-Coder · Codestral<br/>
|
|||
我们非常欢迎您的贡献!无论是提交 Issue、PR 还是完善文档。
|
||||
请查看 [CONTRIBUTING.md](./CONTRIBUTING.md) 了解详情。
|
||||
|
||||
### 📬 联系作者
|
||||
|
||||
<div align="center">
|
||||
|
||||
**欢迎大家来和我交流探讨!无论是技术问题、功能建议还是合作意向,都期待与你沟通~**
|
||||
|
||||
| 联系方式 | |
|
||||
|:---:|:---:|
|
||||
| 📧 **邮箱** | **lintsinghua@qq.com** |
|
||||
| 🐙 **GitHub** | [@lintsinghua](https://github.com/lintsinghua) |
|
||||
|
||||
</div>
|
||||
|
||||
### 💬 交流群
|
||||
|
||||
<div align="center">
|
||||
|
||||
**欢迎大家入群交流分享、学习、摸鱼~**
|
||||
|
||||
<img src="frontend/public/images/DeepAudit群聊.png" alt="QQ交流群" width="200">
|
||||
|
||||
</div>
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 [MIT License](LICENSE) 开源。
|
||||
本项目采用 [AGPL-3.0 License](LICENSE) 开源。
|
||||
|
||||
## 📈 项目热度
|
||||
|
||||
|
|
@ -412,6 +468,14 @@ DeepSeek-Coder · Codestral<br/>
|
|||
|
||||
---
|
||||
|
||||
## 致谢
|
||||
|
||||
感谢以下开源项目的支持:
|
||||
|
||||
[FastAPI](https://fastapi.tiangolo.com/) · [LangChain](https://langchain.com/) · [LangGraph](https://langchain-ai.github.io/langgraph/) · [ChromaDB](https://www.trychroma.com/) · [LiteLLM](https://litellm.ai/) · [Tree-sitter](https://tree-sitter.github.io/) · [Kunlun-M](https://github.com/LoRexxar/Kunlun-M) · [Strix](https://github.com/usestrix/strix) · [React](https://react.dev/) · [Vite](https://vitejs.dev/) · [Radix UI](https://www.radix-ui.com/) · [TailwindCSS](https://tailwindcss.com/) · [shadcn/ui](https://ui.shadcn.com/)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 重要安全声明
|
||||
|
||||
### 法律合规声明
|
||||
|
|
|
|||
|
|
@ -0,0 +1,462 @@
|
|||
# DeepAudit - Your AI Security Audit Team, Making Vulnerability Discovery Accessible
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">简体中文</a> | <strong>English</strong>
|
||||
</p>
|
||||
|
||||
<div style="width: 100%; max-width: 600px; margin: 0 auto;">
|
||||
<img src="frontend/public/images/logo.png" alt="DeepAudit Logo" style="width: 100%; height: auto; display: block; margin: 0 auto;">
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/lintsinghua/DeepAudit/releases)
|
||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||
[](https://reactjs.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://fastapi.tiangolo.com/)
|
||||
[](https://www.python.org/)
|
||||
[](https://deepwiki.com/lintsinghua/DeepAudit)
|
||||
|
||||
[](https://github.com/lintsinghua/DeepAudit/stargazers)
|
||||
[](https://github.com/lintsinghua/DeepAudit/network/members)
|
||||
|
||||
<a href="https://trendshift.io/repositories/15634" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15634" alt="lintsinghua%2FDeepAudit | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<img src="frontend/public/DeepAudit.gif" alt="DeepAudit Demo" width="90%">
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## Screenshots
|
||||
|
||||
<div align="center">
|
||||
|
||||
### Agent Audit Entry
|
||||
|
||||
<img src="frontend/public/images/README-show/Agent审计入口(首页).png" alt="Agent Audit Entry" width="90%">
|
||||
|
||||
*Quick access to Multi-Agent deep audit from homepage*
|
||||
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%" align="center">
|
||||
<strong>Audit Flow Logs</strong><br/><br/>
|
||||
<img src="frontend/public/images/README-show/审计流日志.png" alt="Audit Flow Logs" width="95%"><br/>
|
||||
<em>Real-time view of Agent thinking and execution process</em>
|
||||
</td>
|
||||
<td width="50%" align="center">
|
||||
<strong>Smart Dashboard</strong><br/><br/>
|
||||
<img src="frontend/public/images/README-show/仪表盘.png" alt="Dashboard" width="95%"><br/>
|
||||
<em>Grasp project security posture at a glance</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" align="center">
|
||||
<strong>Instant Analysis</strong><br/><br/>
|
||||
<img src="frontend/public/images/README-show/即时分析.png" alt="Instant Analysis" width="95%"><br/>
|
||||
<em>Paste code / upload files, get results in seconds</em>
|
||||
</td>
|
||||
<td width="50%" align="center">
|
||||
<strong>Project Management</strong><br/><br/>
|
||||
<img src="frontend/public/images/README-show/项目管理.png" alt="Project Management" width="95%"><br/>
|
||||
<em>GitHub/GitLab import, multi-project collaboration</em>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div align="center">
|
||||
|
||||
### Professional Reports
|
||||
|
||||
<img src="frontend/public/images/README-show/审计报告示例.png" alt="Audit Report" width="90%">
|
||||
|
||||
*One-click export to PDF / Markdown / JSON* (Quick mode shown, not Agent mode report)
|
||||
|
||||
[View Full Agent Audit Report Example](https://lintsinghua.github.io/)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
**DeepAudit** is a next-generation code security audit platform based on **Multi-Agent collaborative architecture**. It's not just a static scanning tool, but simulates the thinking patterns of security experts through autonomous collaboration of multiple agents (**Orchestrator**, **Recon**, **Analysis**, **Verification**), achieving deep code understanding, vulnerability discovery, and **automated sandbox PoC verification**.
|
||||
|
||||
We are committed to solving three major pain points of traditional SAST tools:
|
||||
- **High false positive rate** — Lack of semantic understanding, massive false positives consume manpower
|
||||
- **Business logic blind spots** — Cannot understand cross-file calls and complex logic
|
||||
- **Lack of verification methods** — Don't know if vulnerabilities are actually exploitable
|
||||
|
||||
Users only need to import a project, and DeepAudit automatically starts working: identify tech stack → analyze potential risks → generate scripts → sandbox verification → generate report, ultimately outputting a professional audit report.
|
||||
|
||||
> **Core Philosophy**: Let AI attack like a hacker, defend like an expert.
|
||||
|
||||
## Why Choose DeepAudit?
|
||||
|
||||
<div align="center">
|
||||
|
||||
| Traditional Audit Pain Points | DeepAudit Solutions |
|
||||
| :--- | :--- |
|
||||
| **Low manual audit efficiency**<br>Can't keep up with CI/CD iteration speed, slowing release process | **Multi-Agent Autonomous Audit**<br>AI automatically orchestrates audit strategies, 24/7 automated execution |
|
||||
| **Too many false positives**<br>Lack of semantic understanding, spending lots of time cleaning noise daily | **RAG Knowledge Enhancement**<br>Combining code semantics with context, significantly reducing false positives |
|
||||
| **Data privacy concerns**<br>Worried about core source code leaking to cloud AI, can't meet compliance requirements | **Ollama Local Deployment Support**<br>Data stays on-premises, supports Llama3/DeepSeek and other local models |
|
||||
| **Can't confirm authenticity**<br>Outsourced projects have many vulnerabilities, don't know which are truly exploitable | **Sandbox PoC Verification**<br>Automatically generate and execute attack scripts, confirm real vulnerability impact |
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## System Architecture
|
||||
|
||||
### Architecture Diagram
|
||||
|
||||
DeepAudit adopts microservices architecture, driven by the Multi-Agent engine at its core.
|
||||
|
||||
<div align="center">
|
||||
<img src="frontend/public/images/README-show/架构图.png" alt="DeepAudit Architecture" width="90%">
|
||||
</div>
|
||||
|
||||
### Audit Workflow
|
||||
|
||||
| Step | Phase | Responsible Agent | Main Actions |
|
||||
|:---:|:---:|:---:|:---|
|
||||
| 1 | **Strategy Planning** | **Orchestrator** | Receive audit task, analyze project type, formulate audit plan, dispatch tasks to sub-agents |
|
||||
| 2 | **Information Gathering** | **Recon Agent** | Scan project structure, identify frameworks/libraries/APIs, extract attack surface (Entry Points) |
|
||||
| 3 | **Vulnerability Discovery** | **Analysis Agent** | Combine RAG knowledge base with AST analysis, deep code review, discover potential vulnerabilities |
|
||||
| 4 | **PoC Verification** | **Verification Agent** | **(Critical)** Write PoC scripts, execute in Docker sandbox. Self-correct and retry if failed |
|
||||
| 5 | **Report Generation** | **Orchestrator** | Aggregate all findings, filter out verified false positives, generate final report |
|
||||
|
||||
### Project Structure
|
||||
|
||||
```text
|
||||
DeepAudit/
|
||||
├── backend/ # Python FastAPI Backend
|
||||
│ ├── app/
|
||||
│ │ ├── agents/ # Multi-Agent Core Logic
|
||||
│ │ │ ├── orchestrator.py # Commander: Task Orchestration
|
||||
│ │ │ ├── recon.py # Scout: Asset Identification
|
||||
│ │ │ ├── analysis.py # Analyst: Vulnerability Discovery
|
||||
│ │ │ └── verification.py # Verifier: Sandbox PoC
|
||||
│ │ ├── core/ # Core Config & Sandbox Interface
|
||||
│ │ ├── models/ # Database Models
|
||||
│ │ └── services/ # RAG, LLM Service Wrappers
|
||||
│ └── tests/ # Unit Tests
|
||||
├── frontend/ # React + TypeScript Frontend
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # UI Component Library
|
||||
│ │ ├── pages/ # Page Routes
|
||||
│ │ └── stores/ # Zustand State Management
|
||||
├── docker/ # Docker Deployment Config
|
||||
│ ├── sandbox/ # Security Sandbox Image Build
|
||||
│ └── postgres/ # Database Initialization
|
||||
└── docs/ # Detailed Documentation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: One-Line Deployment (Recommended)
|
||||
|
||||
Using pre-built Docker images, no need to clone code, start with one command:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/lintsinghua/DeepAudit/v3.0.0/docker-compose.prod.yml | docker compose -f - up -d
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>💡 Configure Docker Registry Mirrors (Optional, for faster image pulling) (Click to expand)</summary>
|
||||
|
||||
If pulling images is still slow, you can configure Docker registry mirrors. Edit the Docker configuration file and add the following mirror sources:
|
||||
|
||||
**Linux / macOS**: Edit `/etc/docker/daemon.json`
|
||||
|
||||
**Windows**: Right-click Docker Desktop icon → Settings → Docker Engine
|
||||
|
||||
```json
|
||||
{
|
||||
"registry-mirrors": [
|
||||
"https://docker.1ms.run",
|
||||
"https://dockerproxy.com",
|
||||
"https://hub.rat.dev"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Restart Docker service after saving:
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
sudo systemctl restart docker
|
||||
|
||||
# macOS / Windows
|
||||
# Restart Docker Desktop application
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
> **Success!** Visit http://localhost:3000 to start exploring.
|
||||
|
||||
---
|
||||
|
||||
### Option 2: Clone and Deploy
|
||||
|
||||
Suitable for users who need custom configuration or secondary development:
|
||||
|
||||
```bash
|
||||
# 1. Clone project
|
||||
git clone https://github.com/lintsinghua/DeepAudit.git && cd DeepAudit
|
||||
|
||||
# 2. Configure environment variables
|
||||
cp backend/env.example backend/.env
|
||||
# Edit backend/.env and fill in your LLM API Key
|
||||
|
||||
# 3. One-click start
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
> First startup will automatically build the sandbox image, which may take a few minutes.
|
||||
|
||||
---
|
||||
|
||||
## Development Guide
|
||||
|
||||
For developers doing secondary development and debugging.
|
||||
|
||||
### Requirements
|
||||
- Python 3.11+
|
||||
- Node.js 20+
|
||||
- PostgreSQL 15+
|
||||
- Docker (for sandbox)
|
||||
|
||||
### 1. Backend Setup
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
# Use uv for environment management (recommended)
|
||||
uv sync
|
||||
source .venv/bin/activate
|
||||
|
||||
# Start API service
|
||||
uvicorn app.main:app --reload
|
||||
```
|
||||
|
||||
### 2. Frontend Setup
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### 3. Sandbox Environment
|
||||
|
||||
Development mode requires pulling the sandbox image locally:
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/lintsinghua/deepaudit-sandbox:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multi-Agent Intelligent Audit
|
||||
|
||||
### Supported Vulnerability Types
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
| Vulnerability Type | Description |
|
||||
|---------|------|
|
||||
| `sql_injection` | SQL Injection |
|
||||
| `xss` | Cross-Site Scripting |
|
||||
| `command_injection` | Command Injection |
|
||||
| `path_traversal` | Path Traversal |
|
||||
| `ssrf` | Server-Side Request Forgery |
|
||||
| `xxe` | XML External Entity Injection |
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
| Vulnerability Type | Description |
|
||||
|---------|------|
|
||||
| `insecure_deserialization` | Insecure Deserialization |
|
||||
| `hardcoded_secret` | Hardcoded Secrets |
|
||||
| `weak_crypto` | Weak Cryptography |
|
||||
| `authentication_bypass` | Authentication Bypass |
|
||||
| `authorization_bypass` | Authorization Bypass |
|
||||
| `idor` | Insecure Direct Object Reference |
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
> For detailed documentation, see **[Agent Audit Guide](docs/AGENT_AUDIT.md)**
|
||||
|
||||
---
|
||||
|
||||
## Supported LLM Platforms
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" width="33%">
|
||||
<h3>International Platforms</h3>
|
||||
<p>
|
||||
OpenAI GPT-4o / GPT-4<br/>
|
||||
Claude 3.5 Sonnet / Opus<br/>
|
||||
Google Gemini Pro<br/>
|
||||
DeepSeek V3
|
||||
</p>
|
||||
</td>
|
||||
<td align="center" width="33%">
|
||||
<h3>Chinese Platforms</h3>
|
||||
<p>
|
||||
Qwen (Tongyi Qianwen)<br/>
|
||||
Zhipu GLM-4<br/>
|
||||
Moonshot Kimi<br/>
|
||||
Wenxin · MiniMax · Doubao
|
||||
</p>
|
||||
</td>
|
||||
<td align="center" width="33%">
|
||||
<h3>Local Deployment</h3>
|
||||
<p>
|
||||
<strong>Ollama</strong><br/>
|
||||
Llama3 · Qwen2.5 · CodeLlama<br/>
|
||||
DeepSeek-Coder · Codestral<br/>
|
||||
<em>Code stays on-premises</em>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
> Supports API proxies to solve network access issues | Detailed configuration → [LLM Platform Support](docs/LLM_PROVIDERS.md)
|
||||
|
||||
---
|
||||
|
||||
## Feature Matrix
|
||||
|
||||
| Feature | Description | Mode |
|
||||
|------|------|------|
|
||||
| **Agent Deep Audit** | Multi-Agent collaboration, autonomous audit strategy orchestration | Agent |
|
||||
| **RAG Knowledge Enhancement** | Code semantic understanding, CWE/CVE knowledge base retrieval | Agent |
|
||||
| **Sandbox PoC Verification** | Docker isolated execution, verify vulnerability validity | Agent |
|
||||
| **Project Management** | GitHub/GitLab import, ZIP upload, 10+ language support | General |
|
||||
| **Instant Analysis** | Code snippet analysis in seconds, paste and use | General |
|
||||
| **Five-Dimensional Detection** | Bug · Security · Performance · Style · Maintainability | General |
|
||||
| **What-Why-How** | Precise location + cause explanation + fix suggestions | General |
|
||||
| **Audit Rules** | Built-in OWASP Top 10, supports custom rule sets | General |
|
||||
| **Prompt Templates** | Visual management, bilingual support | General |
|
||||
| **Report Export** | One-click export to PDF / Markdown / JSON | General |
|
||||
| **Runtime Configuration** | Configure LLM in browser, no service restart needed | General |
|
||||
|
||||
## Roadmap
|
||||
|
||||
We are continuously evolving, with more language support and stronger Agent capabilities coming.
|
||||
|
||||
- [x] Basic static analysis, Semgrep integration
|
||||
- [x] RAG knowledge base introduction, Docker security sandbox support
|
||||
- [x] **Multi-Agent Collaborative Architecture** (Current)
|
||||
- [ ] Support for more realistic simulated service environments for more authentic vulnerability verification
|
||||
- [ ] Optimize sandbox from function_call to stable MCP service
|
||||
- [ ] **Auto-Fix**: Agent directly submits PRs to fix vulnerabilities
|
||||
- [ ] **Incremental PR Audit**: Continuously track PR changes, intelligently analyze vulnerabilities, integrate with CI/CD
|
||||
- [ ] **Optimized RAG**: Support custom knowledge bases
|
||||
|
||||
---
|
||||
|
||||
## Contributing & Community
|
||||
|
||||
### Contributing Guide
|
||||
We warmly welcome your contributions! Whether it's submitting Issues, PRs, or improving documentation.
|
||||
Please check [CONTRIBUTING.md](./CONTRIBUTING.md) for details.
|
||||
|
||||
### Contact
|
||||
|
||||
<div align="center">
|
||||
|
||||
**Feel free to reach out for technical discussions, feature suggestions, or collaboration opportunities!**
|
||||
|
||||
| Contact | |
|
||||
|:---:|:---:|
|
||||
| **Email** | **lintsinghua@qq.com** |
|
||||
| **GitHub** | [@lintsinghua](https://github.com/lintsinghua) |
|
||||
|
||||
### 💬 Community Group
|
||||
|
||||
**Welcome to join our QQ group for discussion, sharing, learning, and chatting~**
|
||||
|
||||
<img src="frontend/public/images/DeepAudit群聊.png" alt="QQ Group" width="200">
|
||||
|
||||
</div>
|
||||
|
||||
## License
|
||||
|
||||
This project is open-sourced under the [AGPL-3.0 License](LICENSE).
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#lintsinghua/DeepAudit&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=lintsinghua/DeepAudit&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=lintsinghua/DeepAudit&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=lintsinghua/DeepAudit&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<strong>Made with ❤️ by <a href="https://github.com/lintsinghua">lintsinghua</a></strong>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Thanks to the following open-source projects for their support:
|
||||
|
||||
[FastAPI](https://fastapi.tiangolo.com/) · [LangChain](https://langchain.com/) · [LangGraph](https://langchain-ai.github.io/langgraph/) · [ChromaDB](https://www.trychroma.com/) · [LiteLLM](https://litellm.ai/) · [Tree-sitter](https://tree-sitter.github.io/) · [Kunlun-M](https://github.com/LoRexxar/Kunlun-M) · [Strix](https://github.com/usestrix/strix) · [React](https://react.dev/) · [Vite](https://vitejs.dev/) · [Radix UI](https://www.radix-ui.com/) · [TailwindCSS](https://tailwindcss.com/) · [shadcn/ui](https://ui.shadcn.com/)
|
||||
|
||||
---
|
||||
|
||||
## Important Security Notice
|
||||
|
||||
### Legal Compliance Statement
|
||||
1. **Any unauthorized vulnerability testing, penetration testing, or security assessment is prohibited**
|
||||
2. This project is only for cybersecurity academic research, teaching, and learning purposes
|
||||
3. It is strictly prohibited to use this project for any illegal purposes or unauthorized security testing
|
||||
|
||||
### Vulnerability Reporting Responsibility
|
||||
1. When discovering any security vulnerabilities, please report them through legitimate channels promptly
|
||||
2. It is strictly prohibited to use discovered vulnerabilities for illegal activities
|
||||
3. Comply with national cybersecurity laws and regulations, maintain cyberspace security
|
||||
|
||||
### Usage Restrictions
|
||||
- Only for educational and research purposes in authorized environments
|
||||
- Prohibited for security testing on unauthorized systems
|
||||
- Users are fully responsible for their own actions
|
||||
|
||||
### Disclaimer
|
||||
The author is not responsible for any direct or indirect losses caused by the use of this project. Users bear full legal responsibility for their own actions.
|
||||
|
||||
---
|
||||
|
||||
## Detailed Security Policy
|
||||
|
||||
For detailed information about installation policy, disclaimer, code privacy, API usage security, and vulnerability reporting, please refer to [DISCLAIMER.md](DISCLAIMER.md) and [SECURITY.md](SECURITY.md) files.
|
||||
|
||||
### Quick Reference
|
||||
- **Code Privacy Warning**: Your code will be sent to the selected LLM provider's servers
|
||||
- **Sensitive Code Handling**: Use local models for sensitive code
|
||||
- **Compliance Requirements**: Comply with data protection and privacy laws
|
||||
- **Vulnerability Reporting**: Report security issues through legitimate channels
|
||||
|
|
@ -304,6 +304,72 @@ async def _execute_agent_task(task_id: str):
|
|||
event_emitter=event_emitter, # 🔥 新增
|
||||
)
|
||||
|
||||
# 🔥 自动修正 target_files 路径
|
||||
# 如果发生了目录调整(例如 ZIP 解压后只有一层目录,root 被下移),
|
||||
# 原有的 target_files (如 "Prefix/file.php") 可能无法匹配。
|
||||
# 我们需要检测并移除这些无效的前缀。
|
||||
if task.target_files and len(task.target_files) > 0:
|
||||
# 1. 检查是否存在不匹配的文件
|
||||
all_exist = True
|
||||
for tf in task.target_files:
|
||||
if not os.path.exists(os.path.join(project_root, tf)):
|
||||
all_exist = False
|
||||
break
|
||||
|
||||
if not all_exist:
|
||||
logger.info(f"Target files path mismatch detected in {project_root}")
|
||||
# 尝试通过路径匹配来修复
|
||||
# 获取当前根目录的名称
|
||||
root_name = os.path.basename(project_root)
|
||||
|
||||
new_target_files = []
|
||||
fixed_count = 0
|
||||
|
||||
for tf in task.target_files:
|
||||
# 检查文件是否以 root_name 开头(例如 "PHP-Project/index.php" 而 root 是 ".../PHP-Project")
|
||||
if tf.startswith(root_name + "/"):
|
||||
fixed_path = tf[len(root_name)+1:]
|
||||
if os.path.exists(os.path.join(project_root, fixed_path)):
|
||||
new_target_files.append(fixed_path)
|
||||
fixed_count += 1
|
||||
continue
|
||||
|
||||
# 如果上面的没匹配,尝试暴力搜索(只针对未找到的文件)
|
||||
# 这种情况比较少见,先保留原样或标记为丢失
|
||||
if os.path.exists(os.path.join(project_root, tf)):
|
||||
new_target_files.append(tf)
|
||||
else:
|
||||
# 尝试查看 tf 的 basename 是否在根目录直接存在(针对常见的最简情况)
|
||||
basename = os.path.basename(tf)
|
||||
if os.path.exists(os.path.join(project_root, basename)):
|
||||
new_target_files.append(basename)
|
||||
fixed_count += 1
|
||||
else:
|
||||
# 实在找不到,保留原样,让后续流程报错或忽略
|
||||
new_target_files.append(tf)
|
||||
|
||||
if fixed_count > 0:
|
||||
logger.info(f"🔧 Auto-fixed {fixed_count} target file paths")
|
||||
await event_emitter.emit_info(f"🔧 自动修正了 {fixed_count} 个目标文件的路径")
|
||||
task.target_files = new_target_files
|
||||
|
||||
# 🔥 重新验证修正后的文件
|
||||
valid_target_files = []
|
||||
if task.target_files:
|
||||
for tf in task.target_files:
|
||||
if os.path.exists(os.path.join(project_root, tf)):
|
||||
valid_target_files.append(tf)
|
||||
else:
|
||||
logger.warning(f"⚠️ Target file not found: {tf}")
|
||||
|
||||
if not valid_target_files:
|
||||
logger.warning("❌ No valid target files found after adjustment!")
|
||||
await event_emitter.emit_warning("⚠️ 警告:无法找到指定的目标文件,将扫描所有文件")
|
||||
task.target_files = None # 回退到全量扫描
|
||||
elif len(valid_target_files) < len(task.target_files):
|
||||
logger.warning(f"⚠️ Partial target files missing. Found {len(valid_target_files)}/{len(task.target_files)}")
|
||||
task.target_files = valid_target_files
|
||||
|
||||
logger.info(f"🚀 Task {task_id} started with Dynamic Agent Tree architecture")
|
||||
|
||||
# 🔥 获取项目根目录后检查取消
|
||||
|
|
@ -445,7 +511,9 @@ async def _execute_agent_task(task_id: str):
|
|||
if isinstance(f, dict):
|
||||
logger.debug(f"[AgentTask] Finding {i+1}: {f.get('title', 'N/A')[:50]} - {f.get('severity', 'N/A')}")
|
||||
|
||||
await _save_findings(db, task_id, findings)
|
||||
# 🔥 v2.1: 传递 project_root 用于文件路径验证
|
||||
saved_count = await _save_findings(db, task_id, findings, project_root=project_root)
|
||||
logger.info(f"[AgentTask] Saved {saved_count}/{len(findings)} findings (filtered {len(findings) - saved_count} hallucinations)")
|
||||
|
||||
# 更新任务统计
|
||||
# 🔥 CRITICAL FIX: 在设置完成前再次检查取消状态
|
||||
|
|
@ -457,7 +525,7 @@ async def _execute_agent_task(task_id: str):
|
|||
task.status = AgentTaskStatus.COMPLETED
|
||||
task.completed_at = datetime.now(timezone.utc)
|
||||
task.current_phase = AgentTaskPhase.REPORTING
|
||||
task.findings_count = len(findings)
|
||||
task.findings_count = saved_count # 🔥 v2.1: 使用实际保存的数量(排除幻觉)
|
||||
task.total_iterations = result.iterations
|
||||
task.tool_calls_count = result.tool_calls
|
||||
task.tokens_used = result.tokens_used
|
||||
|
|
@ -882,6 +950,8 @@ async def _initialize_tools(
|
|||
CommandInjectionTestTool, SqlInjectionTestTool, XssTestTool,
|
||||
PathTraversalTestTool, SstiTestTool, DeserializationTestTool,
|
||||
UniversalVulnTestTool,
|
||||
# 🔥 新增:通用代码执行工具 (LLM 驱动的 Fuzzing Harness)
|
||||
RunCodeTool, ExtractFunctionTool,
|
||||
)
|
||||
|
||||
verification_tools = {
|
||||
|
|
@ -910,8 +980,12 @@ async def _initialize_tools(
|
|||
"test_deserialization": DeserializationTestTool(sandbox_manager, project_root),
|
||||
"universal_vuln_test": UniversalVulnTestTool(sandbox_manager, project_root),
|
||||
|
||||
# 报告工具
|
||||
"create_vulnerability_report": CreateVulnerabilityReportTool(),
|
||||
# 🔥 新增:通用代码执行工具 (LLM 驱动的 Fuzzing Harness)
|
||||
"run_code": RunCodeTool(sandbox_manager, project_root),
|
||||
"extract_function": ExtractFunctionTool(project_root),
|
||||
|
||||
# 报告工具 - 🔥 v2.1: 传递 project_root 用于文件验证
|
||||
"create_vulnerability_report": CreateVulnerabilityReportTool(project_root),
|
||||
}
|
||||
|
||||
# Orchestrator 工具(主要是思考工具)
|
||||
|
|
@ -1045,11 +1119,26 @@ async def _collect_project_info(
|
|||
return info
|
||||
|
||||
|
||||
async def _save_findings(db: AsyncSession, task_id: str, findings: List[Dict]) -> None:
|
||||
async def _save_findings(
|
||||
db: AsyncSession,
|
||||
task_id: str,
|
||||
findings: List[Dict],
|
||||
project_root: Optional[str] = None,
|
||||
) -> int:
|
||||
"""
|
||||
保存发现到数据库
|
||||
|
||||
🔥 增强版:支持多种 Agent 输出格式,健壮的字段映射
|
||||
🔥 v2.1: 添加文件路径验证,过滤幻觉发现
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
task_id: 任务ID
|
||||
findings: 发现列表
|
||||
project_root: 项目根目录(用于验证文件路径)
|
||||
|
||||
Returns:
|
||||
int: 实际保存的发现数量
|
||||
"""
|
||||
from app.models.agent_task import VulnerabilityType
|
||||
|
||||
|
|
@ -1057,7 +1146,7 @@ async def _save_findings(db: AsyncSession, task_id: str, findings: List[Dict]) -
|
|||
|
||||
if not findings:
|
||||
logger.warning(f"[SaveFindings] No findings to save for task {task_id}")
|
||||
return
|
||||
return 0
|
||||
|
||||
# 🔥 Case-insensitive mapping preparation
|
||||
severity_map = {
|
||||
|
|
@ -1144,6 +1233,21 @@ async def _save_findings(db: AsyncSession, task_id: str, findings: List[Dict]) -
|
|||
finding.get("location", "").split(":")[0] if ":" in finding.get("location", "") else finding.get("location")
|
||||
)
|
||||
|
||||
# 🔥 v2.1: 文件路径验证 - 过滤幻觉发现
|
||||
if project_root and file_path:
|
||||
# 清理路径(移除可能的行号)
|
||||
clean_path = file_path.split(":")[0].strip() if ":" in file_path else file_path.strip()
|
||||
full_path = os.path.join(project_root, clean_path)
|
||||
|
||||
if not os.path.isfile(full_path):
|
||||
# 尝试作为绝对路径
|
||||
if not (os.path.isabs(clean_path) and os.path.isfile(clean_path)):
|
||||
logger.warning(
|
||||
f"[SaveFindings] 🚫 跳过幻觉发现: 文件不存在 '{file_path}' "
|
||||
f"(title: {finding.get('title', 'N/A')[:50]})"
|
||||
)
|
||||
continue # 跳过这个发现
|
||||
|
||||
# 🔥 Handle line numbers (support multiple formats)
|
||||
line_start = finding.get("line_start") or finding.get("line")
|
||||
if not line_start and ":" in finding.get("location", ""):
|
||||
|
|
@ -1274,6 +1378,8 @@ async def _save_findings(db: AsyncSession, task_id: str, findings: List[Dict]) -
|
|||
logger.error(f"Failed to commit findings: {e}")
|
||||
await db.rollback()
|
||||
|
||||
return saved_count
|
||||
|
||||
|
||||
def _calculate_security_score(findings: List[Dict]) -> float:
|
||||
"""计算安全评分"""
|
||||
|
|
@ -2486,6 +2592,20 @@ async def _get_project_root(
|
|||
await emit(f"❌ 项目目录为空", "error")
|
||||
raise RuntimeError(f"项目目录为空,可能是克隆/解压失败: {base_path}")
|
||||
|
||||
# 🔥 智能检测:如果解压后只有一个子目录(常见于 ZIP 文件),
|
||||
# 则使用那个子目录作为真正的项目根目录
|
||||
# 例如:/tmp/deepaudit/UUID/PHP-Project/ -> 返回 /tmp/deepaudit/UUID/PHP-Project
|
||||
items = os.listdir(base_path)
|
||||
# 过滤掉 macOS 产生的 __MACOSX 目录和隐藏文件
|
||||
real_items = [item for item in items if not item.startswith('__') and not item.startswith('.')]
|
||||
|
||||
if len(real_items) == 1:
|
||||
single_item_path = os.path.join(base_path, real_items[0])
|
||||
if os.path.isdir(single_item_path):
|
||||
logger.info(f"🔍 检测到单层嵌套目录,自动调整项目根目录: {base_path} -> {single_item_path}")
|
||||
await emit(f"🔍 检测到嵌套目录,自动调整为: {real_items[0]}")
|
||||
base_path = single_item_path
|
||||
|
||||
await emit(f"📁 项目准备完成: {base_path}")
|
||||
return base_path
|
||||
|
||||
|
|
@ -3068,15 +3188,53 @@ async def generate_audit_report(
|
|||
md_lines.append("")
|
||||
|
||||
if f.code_snippet:
|
||||
# Detect language from file extension
|
||||
lang = "python"
|
||||
# 🔥 v2.1: 增强语言检测,避免默认 python 标记错误
|
||||
lang = "text" # 默认使用 text 而非 python
|
||||
if f.file_path:
|
||||
ext = f.file_path.split('.')[-1].lower()
|
||||
lang_map = {
|
||||
'py': 'python', 'js': 'javascript', 'ts': 'typescript',
|
||||
'jsx': 'jsx', 'tsx': 'tsx', 'java': 'java', 'go': 'go',
|
||||
'rs': 'rust', 'rb': 'ruby', 'php': 'php', 'c': 'c',
|
||||
'cpp': 'cpp', 'cs': 'csharp', 'sol': 'solidity'
|
||||
# Python
|
||||
'py': 'python', 'pyw': 'python', 'pyi': 'python',
|
||||
# JavaScript/TypeScript
|
||||
'js': 'javascript', 'mjs': 'javascript', 'cjs': 'javascript',
|
||||
'ts': 'typescript', 'mts': 'typescript',
|
||||
'jsx': 'jsx', 'tsx': 'tsx',
|
||||
# Web
|
||||
'html': 'html', 'htm': 'html',
|
||||
'css': 'css', 'scss': 'scss', 'sass': 'sass', 'less': 'less',
|
||||
'vue': 'vue', 'svelte': 'svelte',
|
||||
# Backend
|
||||
'java': 'java', 'kt': 'kotlin', 'kts': 'kotlin',
|
||||
'go': 'go', 'rs': 'rust',
|
||||
'rb': 'ruby', 'erb': 'erb',
|
||||
'php': 'php', 'phtml': 'php',
|
||||
# C-family
|
||||
'c': 'c', 'h': 'c',
|
||||
'cpp': 'cpp', 'cc': 'cpp', 'cxx': 'cpp', 'hpp': 'cpp',
|
||||
'cs': 'csharp',
|
||||
# Shell/Script
|
||||
'sh': 'bash', 'bash': 'bash', 'zsh': 'zsh',
|
||||
'ps1': 'powershell', 'psm1': 'powershell',
|
||||
# Config
|
||||
'json': 'json', 'yaml': 'yaml', 'yml': 'yaml',
|
||||
'toml': 'toml', 'ini': 'ini', 'cfg': 'ini',
|
||||
'xml': 'xml', 'xhtml': 'xml',
|
||||
# Database
|
||||
'sql': 'sql',
|
||||
# Other
|
||||
'md': 'markdown', 'markdown': 'markdown',
|
||||
'sol': 'solidity',
|
||||
'swift': 'swift',
|
||||
'r': 'r', 'R': 'r',
|
||||
'lua': 'lua',
|
||||
'pl': 'perl', 'pm': 'perl',
|
||||
'ex': 'elixir', 'exs': 'elixir',
|
||||
'erl': 'erlang',
|
||||
'hs': 'haskell',
|
||||
'scala': 'scala', 'sc': 'scala',
|
||||
'clj': 'clojure', 'cljs': 'clojure',
|
||||
'dart': 'dart',
|
||||
'groovy': 'groovy', 'gradle': 'groovy',
|
||||
}
|
||||
lang = lang_map.get(ext, 'text')
|
||||
md_lines.append("**漏洞代码:**")
|
||||
|
|
|
|||
|
|
@ -292,18 +292,73 @@ class LLMTestResponse(BaseModel):
|
|||
message: str
|
||||
model: Optional[str] = None
|
||||
response: Optional[str] = None
|
||||
# 调试信息
|
||||
debug: Optional[dict] = None
|
||||
|
||||
|
||||
@router.post("/test-llm", response_model=LLMTestResponse)
|
||||
async def test_llm_connection(
|
||||
request: LLMTestRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""测试LLM连接是否正常"""
|
||||
from app.services.llm.factory import LLMFactory, NATIVE_ONLY_PROVIDERS
|
||||
from app.services.llm.adapters import LiteLLMAdapter, BaiduAdapter, MinimaxAdapter, DoubaoAdapter
|
||||
from app.services.llm.types import LLMConfig, LLMProvider, LLMRequest, LLMMessage, DEFAULT_MODELS
|
||||
|
||||
from app.services.llm.types import LLMConfig, LLMProvider, LLMRequest, LLMMessage, DEFAULT_MODELS, DEFAULT_BASE_URLS
|
||||
import traceback
|
||||
import time
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# 获取用户保存的配置
|
||||
result = await db.execute(
|
||||
select(UserConfig).where(UserConfig.user_id == current_user.id)
|
||||
)
|
||||
user_config_record = result.scalar_one_or_none()
|
||||
|
||||
# 解析用户配置
|
||||
saved_llm_config = {}
|
||||
saved_other_config = {}
|
||||
if user_config_record:
|
||||
if user_config_record.llm_config:
|
||||
saved_llm_config = decrypt_config(
|
||||
json.loads(user_config_record.llm_config),
|
||||
SENSITIVE_LLM_FIELDS
|
||||
)
|
||||
if user_config_record.other_config:
|
||||
saved_other_config = decrypt_config(
|
||||
json.loads(user_config_record.other_config),
|
||||
SENSITIVE_OTHER_FIELDS
|
||||
)
|
||||
|
||||
# 从保存的配置中获取参数(用于调试显示)
|
||||
saved_timeout_ms = saved_llm_config.get('llmTimeout', settings.LLM_TIMEOUT * 1000)
|
||||
saved_temperature = saved_llm_config.get('llmTemperature', settings.LLM_TEMPERATURE)
|
||||
saved_max_tokens = saved_llm_config.get('llmMaxTokens', settings.LLM_MAX_TOKENS)
|
||||
saved_concurrency = saved_other_config.get('llmConcurrency', settings.LLM_CONCURRENCY)
|
||||
saved_gap_ms = saved_other_config.get('llmGapMs', settings.LLM_GAP_MS)
|
||||
saved_max_files = saved_other_config.get('maxAnalyzeFiles', settings.MAX_ANALYZE_FILES)
|
||||
saved_output_lang = saved_other_config.get('outputLanguage', settings.OUTPUT_LANGUAGE)
|
||||
|
||||
debug_info = {
|
||||
"provider": request.provider,
|
||||
"model_requested": request.model,
|
||||
"base_url_requested": request.baseUrl,
|
||||
"api_key_length": len(request.apiKey) if request.apiKey else 0,
|
||||
"api_key_prefix": request.apiKey[:8] + "..." if request.apiKey and len(request.apiKey) > 8 else "(empty)",
|
||||
# 用户保存的配置参数
|
||||
"saved_config": {
|
||||
"timeout_ms": saved_timeout_ms,
|
||||
"temperature": saved_temperature,
|
||||
"max_tokens": saved_max_tokens,
|
||||
"concurrency": saved_concurrency,
|
||||
"gap_ms": saved_gap_ms,
|
||||
"max_analyze_files": saved_max_files,
|
||||
"output_language": saved_output_lang,
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
# 解析provider
|
||||
provider_map = {
|
||||
|
|
@ -319,27 +374,47 @@ async def test_llm_connection(
|
|||
'doubao': LLMProvider.DOUBAO,
|
||||
'ollama': LLMProvider.OLLAMA,
|
||||
}
|
||||
|
||||
|
||||
provider = provider_map.get(request.provider.lower())
|
||||
if not provider:
|
||||
debug_info["error_type"] = "unsupported_provider"
|
||||
return LLMTestResponse(
|
||||
success=False,
|
||||
message=f"不支持的LLM提供商: {request.provider}"
|
||||
message=f"不支持的LLM提供商: {request.provider}",
|
||||
debug=debug_info
|
||||
)
|
||||
|
||||
|
||||
# 获取默认模型
|
||||
model = request.model or DEFAULT_MODELS.get(provider)
|
||||
|
||||
base_url = request.baseUrl or DEFAULT_BASE_URLS.get(provider, "")
|
||||
|
||||
# 测试时使用用户保存的所有配置参数
|
||||
test_timeout = int(saved_timeout_ms / 1000) if saved_timeout_ms else settings.LLM_TIMEOUT
|
||||
test_temperature = saved_temperature if saved_temperature is not None else settings.LLM_TEMPERATURE
|
||||
test_max_tokens = saved_max_tokens if saved_max_tokens else settings.LLM_MAX_TOKENS
|
||||
|
||||
debug_info["model_used"] = model
|
||||
debug_info["base_url_used"] = base_url
|
||||
debug_info["is_native_adapter"] = provider in NATIVE_ONLY_PROVIDERS
|
||||
debug_info["test_params"] = {
|
||||
"timeout": test_timeout,
|
||||
"temperature": test_temperature,
|
||||
"max_tokens": test_max_tokens,
|
||||
}
|
||||
|
||||
print(f"[LLM Test] 开始测试: provider={provider.value}, model={model}, base_url={base_url}, temperature={test_temperature}, timeout={test_timeout}s, max_tokens={test_max_tokens}")
|
||||
|
||||
# 创建配置
|
||||
config = LLMConfig(
|
||||
provider=provider,
|
||||
api_key=request.apiKey,
|
||||
model=model,
|
||||
base_url=request.baseUrl,
|
||||
timeout=30, # 测试使用较短的超时时间
|
||||
max_tokens=50, # 测试使用较少的token
|
||||
timeout=test_timeout,
|
||||
temperature=test_temperature,
|
||||
max_tokens=test_max_tokens,
|
||||
)
|
||||
|
||||
|
||||
# 直接创建新的适配器实例(不使用缓存),确保使用最新的配置
|
||||
if provider in NATIVE_ONLY_PROVIDERS:
|
||||
native_adapter_map = {
|
||||
|
|
@ -348,59 +423,106 @@ async def test_llm_connection(
|
|||
LLMProvider.DOUBAO: DoubaoAdapter,
|
||||
}
|
||||
adapter = native_adapter_map[provider](config)
|
||||
debug_info["adapter_type"] = type(adapter).__name__
|
||||
else:
|
||||
adapter = LiteLLMAdapter(config)
|
||||
|
||||
debug_info["adapter_type"] = "LiteLLMAdapter"
|
||||
# 获取 LiteLLM 实际使用的模型名
|
||||
debug_info["litellm_model"] = getattr(adapter, '_get_litellm_model', lambda: model)() if hasattr(adapter, '_get_litellm_model') else model
|
||||
|
||||
test_request = LLMRequest(
|
||||
messages=[
|
||||
LLMMessage(role="user", content="Say 'Hello' in one word.")
|
||||
],
|
||||
max_tokens=50,
|
||||
temperature=test_temperature,
|
||||
max_tokens=test_max_tokens,
|
||||
)
|
||||
|
||||
|
||||
print(f"[LLM Test] 发送测试请求...")
|
||||
response = await adapter.complete(test_request)
|
||||
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
debug_info["elapsed_time_ms"] = round(elapsed_time * 1000, 2)
|
||||
|
||||
# 验证响应内容
|
||||
if not response or not response.content:
|
||||
debug_info["error_type"] = "empty_response"
|
||||
debug_info["raw_response"] = str(response) if response else None
|
||||
print(f"[LLM Test] 空响应: {response}")
|
||||
return LLMTestResponse(
|
||||
success=False,
|
||||
message="LLM 返回空响应,请检查 API Key 和配置"
|
||||
message="LLM 返回空响应,请检查 API Key 和配置",
|
||||
debug=debug_info
|
||||
)
|
||||
|
||||
|
||||
debug_info["response_length"] = len(response.content)
|
||||
debug_info["usage"] = {
|
||||
"prompt_tokens": getattr(response, 'prompt_tokens', None),
|
||||
"completion_tokens": getattr(response, 'completion_tokens', None),
|
||||
"total_tokens": getattr(response, 'total_tokens', None),
|
||||
}
|
||||
|
||||
print(f"[LLM Test] 成功! 响应: {response.content[:50]}... 耗时: {elapsed_time:.2f}s")
|
||||
|
||||
return LLMTestResponse(
|
||||
success=True,
|
||||
message="LLM连接测试成功",
|
||||
message=f"连接成功 ({elapsed_time:.2f}s)",
|
||||
model=model,
|
||||
response=response.content[:100] if response.content else None
|
||||
response=response.content[:100] if response.content else None,
|
||||
debug=debug_info
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
elapsed_time = time.time() - start_time
|
||||
error_msg = str(e)
|
||||
error_type = type(e).__name__
|
||||
|
||||
debug_info["elapsed_time_ms"] = round(elapsed_time * 1000, 2)
|
||||
debug_info["error_type"] = error_type
|
||||
debug_info["error_message"] = error_msg
|
||||
debug_info["traceback"] = traceback.format_exc()
|
||||
|
||||
# 提取 LLMError 中的 api_response
|
||||
if hasattr(e, 'api_response') and e.api_response:
|
||||
debug_info["api_response"] = e.api_response
|
||||
if hasattr(e, 'status_code') and e.status_code:
|
||||
debug_info["status_code"] = e.status_code
|
||||
|
||||
print(f"[LLM Test] 失败: {error_type}: {error_msg}")
|
||||
print(f"[LLM Test] Traceback:\n{traceback.format_exc()}")
|
||||
|
||||
# 提供更友好的错误信息
|
||||
if "401" in error_msg or "invalid_api_key" in error_msg.lower() or "incorrect api key" in error_msg.lower():
|
||||
return LLMTestResponse(
|
||||
success=False,
|
||||
message="API Key 无效或已过期,请检查后重试"
|
||||
)
|
||||
friendly_message = error_msg
|
||||
|
||||
# 优先检查余额不足(因为某些 API 用 429 表示余额不足)
|
||||
if any(keyword in error_msg for keyword in ["余额不足", "资源包", "充值", "quota", "insufficient", "balance", "402"]):
|
||||
friendly_message = "账户余额不足或配额已用尽,请充值后重试"
|
||||
debug_info["error_category"] = "insufficient_balance"
|
||||
elif "401" in error_msg or "invalid_api_key" in error_msg.lower() or "incorrect api key" in error_msg.lower():
|
||||
friendly_message = "API Key 无效或已过期,请检查后重试"
|
||||
debug_info["error_category"] = "auth_invalid_key"
|
||||
elif "authentication" in error_msg.lower():
|
||||
return LLMTestResponse(
|
||||
success=False,
|
||||
message="认证失败,请检查 API Key 是否正确"
|
||||
)
|
||||
friendly_message = "认证失败,请检查 API Key 是否正确"
|
||||
debug_info["error_category"] = "auth_failed"
|
||||
elif "timeout" in error_msg.lower():
|
||||
return LLMTestResponse(
|
||||
success=False,
|
||||
message="连接超时,请检查网络或 API 地址是否正确"
|
||||
)
|
||||
elif "connection" in error_msg.lower():
|
||||
return LLMTestResponse(
|
||||
success=False,
|
||||
message="无法连接到 API 服务,请检查网络或 API 地址"
|
||||
)
|
||||
|
||||
friendly_message = "连接超时,请检查网络或 API 地址是否正确"
|
||||
debug_info["error_category"] = "timeout"
|
||||
elif "connection" in error_msg.lower() or "connect" in error_msg.lower():
|
||||
friendly_message = "无法连接到 API 服务,请检查网络或 API 地址"
|
||||
debug_info["error_category"] = "connection"
|
||||
elif "rate" in error_msg.lower() and "limit" in error_msg.lower():
|
||||
friendly_message = "API 请求频率超限,请稍后重试"
|
||||
debug_info["error_category"] = "rate_limit"
|
||||
elif "model" in error_msg.lower() and ("not found" in error_msg.lower() or "does not exist" in error_msg.lower()):
|
||||
friendly_message = f"模型 '{debug_info.get('model_used', 'unknown')}' 不存在或无权访问"
|
||||
debug_info["error_category"] = "model_not_found"
|
||||
else:
|
||||
debug_info["error_category"] = "unknown"
|
||||
|
||||
return LLMTestResponse(
|
||||
success=False,
|
||||
message=f"LLM连接测试失败: {error_msg}"
|
||||
message=friendly_message,
|
||||
debug=debug_info
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class EmbeddingProvider(BaseModel):
|
|||
|
||||
class EmbeddingConfig(BaseModel):
|
||||
"""嵌入模型配置"""
|
||||
provider: str = Field(description="提供商: openai, ollama, azure, cohere, huggingface")
|
||||
provider: str = Field(description="提供商: openai, ollama, azure, cohere, huggingface, jina, qwen")
|
||||
model: str = Field(description="模型名称")
|
||||
api_key: Optional[str] = Field(default=None, description="API Key (如需要)")
|
||||
base_url: Optional[str] = Field(default=None, description="自定义 API 端点")
|
||||
|
|
@ -76,8 +76,8 @@ class TestEmbeddingResponse(BaseModel):
|
|||
EMBEDDING_PROVIDERS: List[EmbeddingProvider] = [
|
||||
EmbeddingProvider(
|
||||
id="openai",
|
||||
name="OpenAI",
|
||||
description="OpenAI 官方嵌入模型,高质量、稳定",
|
||||
name="OpenAI (兼容 DeepSeek/Moonshot/智谱 等)",
|
||||
description="OpenAI 官方或兼容 API,填写自定义端点可接入其他服务商",
|
||||
models=[
|
||||
"text-embedding-3-small",
|
||||
"text-embedding-3-large",
|
||||
|
|
@ -152,6 +152,18 @@ EMBEDDING_PROVIDERS: List[EmbeddingProvider] = [
|
|||
requires_api_key=True,
|
||||
default_model="jina-embeddings-v2-base-code",
|
||||
),
|
||||
EmbeddingProvider(
|
||||
id="qwen",
|
||||
name="Qwen (DashScope)",
|
||||
description="阿里云 DashScope Qwen 嵌入模型,兼容 OpenAI embeddings 接口",
|
||||
models=[
|
||||
"text-embedding-v4",
|
||||
"text-embedding-v3",
|
||||
"text-embedding-v2",
|
||||
],
|
||||
requires_api_key=True,
|
||||
default_model="text-embedding-v4",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -397,6 +409,11 @@ def _get_model_dimensions(provider: str, model: str) -> int:
|
|||
"jina-embeddings-v2-base-code": 768,
|
||||
"jina-embeddings-v2-base-en": 768,
|
||||
"jina-embeddings-v2-base-zh": 768,
|
||||
|
||||
# Qwen (DashScope)
|
||||
"text-embedding-v4": 1024, # 支持维度: 2048, 1536, 1024(默认), 768, 512, 256, 128, 64
|
||||
"text-embedding-v3": 1024, # 支持维度: 1024(默认), 768, 512, 256, 128, 64
|
||||
"text-embedding-v2": 1536, # 支持维度: 1536
|
||||
}
|
||||
|
||||
return dimensions_map.get(model, 768)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from app.models.project import Project
|
|||
from app.models.analysis import InstantAnalysis
|
||||
from app.models.user_config import UserConfig
|
||||
from app.services.llm.service import LLMService
|
||||
from app.services.scanner import task_control, is_text_file, should_exclude, get_language_from_path
|
||||
from app.services.scanner import task_control, is_text_file, should_exclude, get_language_from_path, get_analysis_config
|
||||
from app.services.zip_storage import load_project_zip, save_project_zip, has_project_zip
|
||||
from app.core.config import settings
|
||||
|
||||
|
|
@ -93,6 +93,11 @@ async def process_zip_task(task_id: str, file_path: str, db_session_factory, use
|
|||
except:
|
||||
pass
|
||||
|
||||
# 获取分析配置(优先使用用户配置)
|
||||
analysis_config = get_analysis_config(user_config)
|
||||
max_analyze_files = analysis_config['max_analyze_files']
|
||||
llm_gap_ms = analysis_config['llm_gap_ms']
|
||||
|
||||
# 限制文件数量
|
||||
# 如果指定了特定文件,则只分析这些文件
|
||||
target_files = scan_config.get('file_paths', [])
|
||||
|
|
@ -101,13 +106,13 @@ async def process_zip_task(task_id: str, file_path: str, db_session_factory, use
|
|||
normalized_targets = {normalize_path(p) for p in target_files}
|
||||
print(f"🎯 ZIP任务: 指定分析 {len(normalized_targets)} 个文件")
|
||||
files_to_scan = [f for f in files_to_scan if f['path'] in normalized_targets]
|
||||
elif settings.MAX_ANALYZE_FILES > 0:
|
||||
files_to_scan = files_to_scan[:settings.MAX_ANALYZE_FILES]
|
||||
|
||||
elif max_analyze_files > 0:
|
||||
files_to_scan = files_to_scan[:max_analyze_files]
|
||||
|
||||
task.total_files = len(files_to_scan)
|
||||
await db.commit()
|
||||
|
||||
print(f"📊 ZIP任务 {task_id}: 找到 {len(files_to_scan)} 个文件")
|
||||
print(f"📊 ZIP任务 {task_id}: 找到 {len(files_to_scan)} 个文件 (最大文件数: {max_analyze_files}, 请求间隔: {llm_gap_ms}ms)")
|
||||
|
||||
total_issues = 0
|
||||
total_lines = 0
|
||||
|
|
@ -178,12 +183,12 @@ async def process_zip_task(task_id: str, file_path: str, db_session_factory, use
|
|||
print(f"📈 ZIP任务 {task_id}: 进度 {scanned_files}/{len(files_to_scan)}")
|
||||
|
||||
# 请求间隔
|
||||
await asyncio.sleep(settings.LLM_GAP_MS / 1000)
|
||||
|
||||
await asyncio.sleep(llm_gap_ms / 1000)
|
||||
|
||||
except Exception as file_error:
|
||||
failed_files += 1
|
||||
print(f"❌ ZIP任务分析文件失败 ({file_info['path']}): {file_error}")
|
||||
await asyncio.sleep(settings.LLM_GAP_MS / 1000)
|
||||
await asyncio.sleep(llm_gap_ms / 1000)
|
||||
|
||||
# 完成任务
|
||||
avg_quality_score = sum(quality_scores) / len(quality_scores) if quality_scores else 100.0
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class Settings(BaseSettings):
|
|||
# ============ Agent 模块配置 ============
|
||||
|
||||
# 嵌入模型配置(独立于 LLM 配置)
|
||||
EMBEDDING_PROVIDER: str = "openai" # openai, azure, ollama, cohere, huggingface, jina
|
||||
EMBEDDING_PROVIDER: str = "openai" # openai, azure, ollama, cohere, huggingface, jina, qwen
|
||||
EMBEDDING_MODEL: str = "text-embedding-3-small"
|
||||
EMBEDDING_API_KEY: Optional[str] = None # 嵌入模型专用 API Key(留空则使用 LLM_API_KEY)
|
||||
EMBEDDING_BASE_URL: Optional[str] = None # 嵌入模型专用 Base URL(留空使用提供商默认地址)
|
||||
|
|
|
|||
|
|
@ -155,6 +155,24 @@ Thought: [总结所有发现]
|
|||
Final Answer: [JSON 格式的漏洞报告]
|
||||
```
|
||||
|
||||
## ⚠️ 输出格式要求(严格遵守)
|
||||
|
||||
**禁止使用 Markdown 格式标记!** 你的输出必须是纯文本格式:
|
||||
|
||||
✅ 正确:
|
||||
```
|
||||
Thought: 我需要使用 semgrep 扫描代码。
|
||||
Action: semgrep_scan
|
||||
Action Input: {"target_path": ".", "rules": "auto"}
|
||||
```
|
||||
|
||||
❌ 错误(禁止):
|
||||
```
|
||||
**Thought:** 我需要扫描
|
||||
**Action:** semgrep_scan
|
||||
**Action Input:** {...}
|
||||
```
|
||||
|
||||
## Final Answer 格式
|
||||
```json
|
||||
{
|
||||
|
|
@ -193,6 +211,31 @@ Final Answer: [JSON 格式的漏洞报告]
|
|||
3. **上下文分析** - 看到可疑代码要读取上下文,理解完整逻辑
|
||||
4. **自主判断** - 不要机械相信工具输出,要用你的专业知识判断
|
||||
|
||||
## 🚨 知识工具使用警告(防止幻觉!)
|
||||
|
||||
**知识库中的代码示例仅供概念参考,不是实际代码!**
|
||||
|
||||
当你使用 `get_vulnerability_knowledge` 或 `query_security_knowledge` 时:
|
||||
1. **知识示例 ≠ 项目代码** - 知识库的代码示例是通用示例,不是目标项目的代码
|
||||
2. **语言可能不匹配** - 知识库可能返回 Python 示例,但项目可能是 PHP/Rust/Go
|
||||
3. **必须在实际代码中验证** - 你只能报告你在 read_file 中**实际看到**的漏洞
|
||||
4. **禁止推测** - 不要因为知识库说"这种模式常见"就假设项目中存在
|
||||
|
||||
❌ 错误做法(幻觉来源):
|
||||
```
|
||||
1. 查询 auth_bypass 知识 -> 看到 JWT 示例
|
||||
2. 没有在项目中找到 JWT 代码
|
||||
3. 仍然报告 "JWT 认证绕过漏洞" <- 这是幻觉!
|
||||
```
|
||||
|
||||
✅ 正确做法:
|
||||
```
|
||||
1. 查询 auth_bypass 知识 -> 了解认证绕过的概念
|
||||
2. 使用 read_file 读取项目的认证代码
|
||||
3. 只有**实际看到**有问题的代码才报告漏洞
|
||||
4. file_path 必须是你**实际读取过**的文件
|
||||
```
|
||||
|
||||
## ⚠️ 关键约束 - 必须遵守!
|
||||
1. **禁止直接输出 Final Answer** - 你必须先调用工具来分析代码
|
||||
2. **至少调用两个工具** - 使用 smart_scan/semgrep_scan 进行扫描,然后用 read_file 查看代码
|
||||
|
|
@ -265,13 +308,21 @@ class AnalysisAgent(BaseAgent):
|
|||
"""解析 LLM 响应 - 增强版,更健壮地提取思考内容"""
|
||||
step = AnalysisStep(thought="")
|
||||
|
||||
# 🔥 v2.1: 预处理 - 移除 Markdown 格式标记(LLM 有时会输出 **Action:** 而非 Action:)
|
||||
cleaned_response = response
|
||||
cleaned_response = re.sub(r'\*\*Action:\*\*', 'Action:', cleaned_response)
|
||||
cleaned_response = re.sub(r'\*\*Action Input:\*\*', 'Action Input:', cleaned_response)
|
||||
cleaned_response = re.sub(r'\*\*Thought:\*\*', 'Thought:', cleaned_response)
|
||||
cleaned_response = re.sub(r'\*\*Final Answer:\*\*', 'Final Answer:', cleaned_response)
|
||||
cleaned_response = re.sub(r'\*\*Observation:\*\*', 'Observation:', cleaned_response)
|
||||
|
||||
# 🔥 首先尝试提取明确的 Thought 标记
|
||||
thought_match = re.search(r'Thought:\s*(.*?)(?=Action:|Final Answer:|$)', response, re.DOTALL)
|
||||
thought_match = re.search(r'Thought:\s*(.*?)(?=Action:|Final Answer:|$)', cleaned_response, re.DOTALL)
|
||||
if thought_match:
|
||||
step.thought = thought_match.group(1).strip()
|
||||
|
||||
# 🔥 检查是否是最终答案
|
||||
final_match = re.search(r'Final Answer:\s*(.*?)$', response, re.DOTALL)
|
||||
final_match = re.search(r'Final Answer:\s*(.*?)$', cleaned_response, re.DOTALL)
|
||||
if final_match:
|
||||
step.is_final = True
|
||||
answer_text = final_match.group(1).strip()
|
||||
|
|
@ -291,7 +342,7 @@ class AnalysisAgent(BaseAgent):
|
|||
|
||||
# 🔥 如果没有提取到 thought,使用 Final Answer 前的内容作为思考
|
||||
if not step.thought:
|
||||
before_final = response[:response.find('Final Answer:')].strip()
|
||||
before_final = cleaned_response[:cleaned_response.find('Final Answer:')].strip()
|
||||
if before_final:
|
||||
before_final = re.sub(r'^Thought:\s*', '', before_final)
|
||||
step.thought = before_final[:500] if len(before_final) > 500 else before_final
|
||||
|
|
@ -299,21 +350,21 @@ class AnalysisAgent(BaseAgent):
|
|||
return step
|
||||
|
||||
# 🔥 提取 Action
|
||||
action_match = re.search(r'Action:\s*(\w+)', response)
|
||||
action_match = re.search(r'Action:\s*(\w+)', cleaned_response)
|
||||
if action_match:
|
||||
step.action = action_match.group(1).strip()
|
||||
|
||||
# 🔥 如果没有提取到 thought,提取 Action 之前的内容作为思考
|
||||
if not step.thought:
|
||||
action_pos = response.find('Action:')
|
||||
action_pos = cleaned_response.find('Action:')
|
||||
if action_pos > 0:
|
||||
before_action = response[:action_pos].strip()
|
||||
before_action = cleaned_response[:action_pos].strip()
|
||||
before_action = re.sub(r'^Thought:\s*', '', before_action)
|
||||
if before_action:
|
||||
step.thought = before_action[:500] if len(before_action) > 500 else before_action
|
||||
|
||||
# 🔥 提取 Action Input
|
||||
input_match = re.search(r'Action Input:\s*(.*?)(?=Thought:|Action:|Observation:|$)', response, re.DOTALL)
|
||||
input_match = re.search(r'Action Input:\s*(.*?)(?=Thought:|Action:|Observation:|$)', cleaned_response, re.DOTALL)
|
||||
if input_match:
|
||||
input_text = input_match.group(1).strip()
|
||||
input_text = re.sub(r'```json\s*', '', input_text)
|
||||
|
|
@ -452,12 +503,11 @@ class AnalysisAgent(BaseAgent):
|
|||
break
|
||||
|
||||
# 调用 LLM 进行思考和决策(流式输出)
|
||||
# 🔥 增加 max_tokens 到 4096,避免长输出被截断
|
||||
# 🔥 使用用户配置的 temperature 和 max_tokens
|
||||
try:
|
||||
llm_output, tokens_this_round = await self.stream_llm_call(
|
||||
self._conversation_history,
|
||||
temperature=0.1,
|
||||
max_tokens=8192,
|
||||
# 🔥 不传递 temperature 和 max_tokens,使用用户配置
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"[{self.name}] LLM call cancelled")
|
||||
|
|
@ -653,8 +703,7 @@ Final Answer:""",
|
|||
try:
|
||||
summary_output, _ = await self.stream_llm_call(
|
||||
self._conversation_history,
|
||||
temperature=0.1,
|
||||
max_tokens=4096,
|
||||
# 🔥 不传递 temperature 和 max_tokens,使用用户配置
|
||||
)
|
||||
|
||||
if summary_output and summary_output.strip():
|
||||
|
|
|
|||
|
|
@ -838,25 +838,24 @@ class BaseAgent(ABC):
|
|||
Args:
|
||||
messages: 消息列表
|
||||
tools: 可用工具描述
|
||||
|
||||
|
||||
Returns:
|
||||
LLM 响应
|
||||
"""
|
||||
self._iteration += 1
|
||||
|
||||
|
||||
try:
|
||||
# 🔥 不传递 temperature 和 max_tokens,让 LLMService 使用用户配置
|
||||
response = await self.llm_service.chat_completion(
|
||||
messages=messages,
|
||||
temperature=self.config.temperature,
|
||||
max_tokens=self.config.max_tokens,
|
||||
tools=tools,
|
||||
)
|
||||
|
||||
|
||||
if response.get("usage"):
|
||||
self._total_tokens += response["usage"].get("total_tokens", 0)
|
||||
|
||||
|
||||
return response
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LLM call failed: {e}")
|
||||
raise
|
||||
|
|
@ -925,46 +924,46 @@ class BaseAgent(ABC):
|
|||
return messages
|
||||
|
||||
# ============ 统一的流式 LLM 调用 ============
|
||||
|
||||
|
||||
async def stream_llm_call(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: float = 0.1,
|
||||
max_tokens: int = 2048,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
auto_compress: bool = True,
|
||||
) -> Tuple[str, int]:
|
||||
"""
|
||||
统一的流式 LLM 调用方法
|
||||
|
||||
|
||||
所有 Agent 共用此方法,避免重复代码
|
||||
|
||||
|
||||
Args:
|
||||
messages: 消息列表
|
||||
temperature: 温度
|
||||
max_tokens: 最大 token 数
|
||||
temperature: 温度(None 时使用用户配置)
|
||||
max_tokens: 最大 token 数(None 时使用用户配置)
|
||||
auto_compress: 是否自动压缩过长的消息历史
|
||||
|
||||
|
||||
Returns:
|
||||
(完整响应内容, token数量)
|
||||
"""
|
||||
# 🔥 自动压缩过长的消息历史
|
||||
if auto_compress:
|
||||
messages = self.compress_messages_if_needed(messages)
|
||||
|
||||
|
||||
accumulated = ""
|
||||
total_tokens = 0
|
||||
|
||||
|
||||
# 🔥 在开始 LLM 调用前检查取消
|
||||
if self.is_cancelled:
|
||||
logger.info(f"[{self.name}] Cancelled before LLM call")
|
||||
return "", 0
|
||||
|
||||
|
||||
logger.info(f"[{self.name}] 🚀 Starting stream_llm_call, emitting thinking_start...")
|
||||
await self.emit_thinking_start()
|
||||
logger.info(f"[{self.name}] ✅ thinking_start emitted, starting LLM stream...")
|
||||
|
||||
|
||||
try:
|
||||
# 获取流式迭代器
|
||||
# 获取流式迭代器(传入 None 时使用用户配置)
|
||||
stream = self.llm_service.chat_completion_stream(
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ LLM 是真正的大脑,全程参与决策!
|
|||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import List, Dict, Any, Optional
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -241,8 +242,7 @@ class OrchestratorAgent(BaseAgent):
|
|||
try:
|
||||
llm_output, tokens_this_round = await self.stream_llm_call(
|
||||
self._conversation_history,
|
||||
temperature=0.1,
|
||||
max_tokens=8192, # 🔥 增加到 8192,避免截断
|
||||
# 🔥 不传递 temperature 和 max_tokens,使用用户配置
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"[{self.name}] LLM call cancelled")
|
||||
|
|
@ -535,32 +535,39 @@ Action Input: {{"参数": "值"}}
|
|||
|
||||
def _parse_llm_response(self, response: str) -> Optional[AgentStep]:
|
||||
"""解析 LLM 响应"""
|
||||
# 🔥 v2.1: 预处理 - 移除 Markdown 格式标记(LLM 有时会输出 **Action:** 而非 Action:)
|
||||
cleaned_response = response
|
||||
cleaned_response = re.sub(r'\*\*Action:\*\*', 'Action:', cleaned_response)
|
||||
cleaned_response = re.sub(r'\*\*Action Input:\*\*', 'Action Input:', cleaned_response)
|
||||
cleaned_response = re.sub(r'\*\*Thought:\*\*', 'Thought:', cleaned_response)
|
||||
cleaned_response = re.sub(r'\*\*Observation:\*\*', 'Observation:', cleaned_response)
|
||||
|
||||
# 提取 Thought
|
||||
thought_match = re.search(r'Thought:\s*(.*?)(?=Action:|$)', response, re.DOTALL)
|
||||
thought_match = re.search(r'Thought:\s*(.*?)(?=Action:|$)', cleaned_response, re.DOTALL)
|
||||
thought = thought_match.group(1).strip() if thought_match else ""
|
||||
|
||||
|
||||
# 提取 Action
|
||||
action_match = re.search(r'Action:\s*(\w+)', response)
|
||||
action_match = re.search(r'Action:\s*(\w+)', cleaned_response)
|
||||
if not action_match:
|
||||
return None
|
||||
action = action_match.group(1).strip()
|
||||
|
||||
|
||||
# 提取 Action Input
|
||||
input_match = re.search(r'Action Input:\s*(.*?)(?=Thought:|Observation:|$)', response, re.DOTALL)
|
||||
input_match = re.search(r'Action Input:\s*(.*?)(?=Thought:|Observation:|$)', cleaned_response, re.DOTALL)
|
||||
if not input_match:
|
||||
return None
|
||||
|
||||
|
||||
input_text = input_match.group(1).strip()
|
||||
# 移除 markdown 代码块
|
||||
input_text = re.sub(r'```json\s*', '', input_text)
|
||||
input_text = re.sub(r'```\s*', '', input_text)
|
||||
|
||||
|
||||
# 使用增强的 JSON 解析器
|
||||
action_input = AgentJsonParser.parse(
|
||||
input_text,
|
||||
default={"raw": input_text}
|
||||
)
|
||||
|
||||
|
||||
return AgentStep(
|
||||
thought=thought,
|
||||
action=action,
|
||||
|
|
@ -1000,12 +1007,47 @@ Action Input: {{"参数": "值"}}
|
|||
except Exception as e:
|
||||
logger.error(f"Sub-agent dispatch failed: {e}", exc_info=True)
|
||||
return f"## 调度失败\n\n错误: {str(e)}"
|
||||
|
||||
def _normalize_finding(self, finding: Dict[str, Any]) -> Dict[str, Any]:
|
||||
|
||||
def _validate_file_path(self, file_path: str) -> bool:
|
||||
"""
|
||||
🔥 v2.1: 验证文件路径是否真实存在
|
||||
|
||||
Args:
|
||||
file_path: 相对或绝对文件路径(可能包含行号,如 "app.py:36")
|
||||
|
||||
Returns:
|
||||
bool: 文件是否存在
|
||||
"""
|
||||
if not file_path or not file_path.strip():
|
||||
return False
|
||||
|
||||
# 获取项目根目录
|
||||
project_root = self._runtime_context.get("project_root", "")
|
||||
if not project_root:
|
||||
# 没有项目根目录时,无法验证,返回 True 以避免误判
|
||||
return True
|
||||
|
||||
# 清理路径(移除可能的行号)
|
||||
clean_path = file_path.split(":")[0].strip() if ":" in file_path else file_path.strip()
|
||||
|
||||
# 尝试相对路径
|
||||
full_path = os.path.join(project_root, clean_path)
|
||||
if os.path.isfile(full_path):
|
||||
return True
|
||||
|
||||
# 尝试绝对路径
|
||||
if os.path.isabs(clean_path) and os.path.isfile(clean_path):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _normalize_finding(self, finding: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
标准化发现格式
|
||||
|
||||
不同 Agent 可能返回不同格式的发现,这个方法将它们标准化为统一格式
|
||||
|
||||
🔥 v2.1: 添加文件路径验证,返回 None 表示发现无效(幻觉)
|
||||
"""
|
||||
normalized = dict(finding) # 复制原始数据
|
||||
|
||||
|
|
@ -1087,6 +1129,15 @@ Action Input: {{"参数": "值"}}
|
|||
if "impact" not in normalized["description"].lower():
|
||||
normalized["description"] += f"\n\nImpact: {normalized['impact']}"
|
||||
|
||||
# 🔥 v2.1: 验证文件路径存在性
|
||||
file_path = normalized.get("file_path", "")
|
||||
if file_path and not self._validate_file_path(file_path):
|
||||
logger.warning(
|
||||
f"[Orchestrator] 🚫 过滤幻觉发现: 文件不存在 '{file_path}' "
|
||||
f"(title: {normalized.get('title', 'N/A')[:50]})"
|
||||
)
|
||||
return None # 返回 None 表示发现无效
|
||||
|
||||
return normalized
|
||||
|
||||
def _summarize_findings(self) -> str:
|
||||
|
|
|
|||
|
|
@ -80,6 +80,29 @@ Thought: [总结收集到的所有信息]
|
|||
Final Answer: [JSON 格式的结果]
|
||||
```
|
||||
|
||||
## ⚠️ 输出格式要求(严格遵守)
|
||||
|
||||
**禁止使用 Markdown 格式标记!** 你的输出必须是纯文本格式:
|
||||
|
||||
✅ 正确格式:
|
||||
```
|
||||
Thought: 我需要查看项目结构来了解项目组成
|
||||
Action: list_files
|
||||
Action Input: {"directory": "."}
|
||||
```
|
||||
|
||||
❌ 错误格式(禁止使用):
|
||||
```
|
||||
**Thought:** 我需要查看项目结构
|
||||
**Action:** list_files
|
||||
**Action Input:** {"directory": "."}
|
||||
```
|
||||
|
||||
规则:
|
||||
1. 不要在 Thought:、Action:、Action Input:、Final Answer: 前后添加 `**`
|
||||
2. 不要使用其他 Markdown 格式(如 `###`、`*斜体*` 等)
|
||||
3. Action Input 必须是完整的 JSON 对象,不能为空或截断
|
||||
|
||||
## 输出格式
|
||||
|
||||
```
|
||||
|
|
@ -131,6 +154,35 @@ Final Answer: {
|
|||
- `line_start`: 行号
|
||||
- `description`: 详细描述
|
||||
|
||||
## 🚨 防止幻觉(关键!)
|
||||
|
||||
**只报告你实际读取过的文件!**
|
||||
|
||||
1. **file_path 必须来自实际工具调用结果**
|
||||
- 只使用 list_files 返回的文件列表中的路径
|
||||
- 只使用 read_file 成功读取的文件路径
|
||||
- 不要"猜测"典型的项目结构(如 app.py, config.py)
|
||||
|
||||
2. **行号必须来自实际代码**
|
||||
- 只使用 read_file 返回内容中的真实行号
|
||||
- 不要编造行号
|
||||
|
||||
3. **禁止套用模板**
|
||||
- 不要因为是 "Python 项目" 就假设存在 requirements.txt
|
||||
- 不要因为是 "Web 项目" 就假设存在 routes.py 或 views.py
|
||||
|
||||
❌ 错误做法:
|
||||
```
|
||||
list_files 返回: ["main.rs", "lib.rs", "Cargo.toml"]
|
||||
high_risk_areas: ["app.py:36 - 存在安全问题"] <- 这是幻觉!项目根本没有 app.py
|
||||
```
|
||||
|
||||
✅ 正确做法:
|
||||
```
|
||||
list_files 返回: ["main.rs", "lib.rs", "Cargo.toml"]
|
||||
high_risk_areas: ["main.rs:xx - 可能存在问题"] <- 必须使用实际存在的文件
|
||||
```
|
||||
|
||||
## ⚠️ 关键约束 - 必须遵守!
|
||||
1. **禁止直接输出 Final Answer** - 你必须先调用工具来收集项目信息
|
||||
2. **至少调用三个工具** - 使用 rag_query 语义搜索关键入口,read_file 读取文件,list_files 仅查看根目录
|
||||
|
|
@ -208,13 +260,21 @@ class ReconAgent(BaseAgent):
|
|||
"""解析 LLM 响应 - 增强版,更健壮地提取思考内容"""
|
||||
step = ReconStep(thought="")
|
||||
|
||||
# 🔥 v2.1: 预处理 - 移除 Markdown 格式标记(LLM 有时会输出 **Action:** 而非 Action:)
|
||||
cleaned_response = response
|
||||
cleaned_response = re.sub(r'\*\*Action:\*\*', 'Action:', cleaned_response)
|
||||
cleaned_response = re.sub(r'\*\*Action Input:\*\*', 'Action Input:', cleaned_response)
|
||||
cleaned_response = re.sub(r'\*\*Thought:\*\*', 'Thought:', cleaned_response)
|
||||
cleaned_response = re.sub(r'\*\*Final Answer:\*\*', 'Final Answer:', cleaned_response)
|
||||
cleaned_response = re.sub(r'\*\*Observation:\*\*', 'Observation:', cleaned_response)
|
||||
|
||||
# 🔥 首先尝试提取明确的 Thought 标记
|
||||
thought_match = re.search(r'Thought:\s*(.*?)(?=Action:|Final Answer:|$)', response, re.DOTALL)
|
||||
thought_match = re.search(r'Thought:\s*(.*?)(?=Action:|Final Answer:|$)', cleaned_response, re.DOTALL)
|
||||
if thought_match:
|
||||
step.thought = thought_match.group(1).strip()
|
||||
|
||||
# 🔥 检查是否是最终答案
|
||||
final_match = re.search(r'Final Answer:\s*(.*?)$', response, re.DOTALL)
|
||||
final_match = re.search(r'Final Answer:\s*(.*?)$', cleaned_response, re.DOTALL)
|
||||
if final_match:
|
||||
step.is_final = True
|
||||
answer_text = final_match.group(1).strip()
|
||||
|
|
@ -234,7 +294,7 @@ class ReconAgent(BaseAgent):
|
|||
|
||||
# 🔥 如果没有提取到 thought,使用 Final Answer 前的内容作为思考
|
||||
if not step.thought:
|
||||
before_final = response[:response.find('Final Answer:')].strip()
|
||||
before_final = cleaned_response[:cleaned_response.find('Final Answer:')].strip()
|
||||
if before_final:
|
||||
# 移除可能的 Thought: 前缀
|
||||
before_final = re.sub(r'^Thought:\s*', '', before_final)
|
||||
|
|
@ -243,22 +303,22 @@ class ReconAgent(BaseAgent):
|
|||
return step
|
||||
|
||||
# 🔥 提取 Action
|
||||
action_match = re.search(r'Action:\s*(\w+)', response)
|
||||
action_match = re.search(r'Action:\s*(\w+)', cleaned_response)
|
||||
if action_match:
|
||||
step.action = action_match.group(1).strip()
|
||||
|
||||
# 🔥 如果没有提取到 thought,提取 Action 之前的内容作为思考
|
||||
if not step.thought:
|
||||
action_pos = response.find('Action:')
|
||||
action_pos = cleaned_response.find('Action:')
|
||||
if action_pos > 0:
|
||||
before_action = response[:action_pos].strip()
|
||||
before_action = cleaned_response[:action_pos].strip()
|
||||
# 移除可能的 Thought: 前缀
|
||||
before_action = re.sub(r'^Thought:\s*', '', before_action)
|
||||
if before_action:
|
||||
step.thought = before_action[:500] if len(before_action) > 500 else before_action
|
||||
|
||||
# 🔥 提取 Action Input
|
||||
input_match = re.search(r'Action Input:\s*(.*?)(?=Thought:|Action:|Observation:|$)', response, re.DOTALL)
|
||||
input_match = re.search(r'Action Input:\s*(.*?)(?=Thought:|Action:|Observation:|$)', cleaned_response, re.DOTALL)
|
||||
if input_match:
|
||||
input_text = input_match.group(1).strip()
|
||||
input_text = re.sub(r'```json\s*', '', input_text)
|
||||
|
|
@ -358,8 +418,7 @@ class ReconAgent(BaseAgent):
|
|||
try:
|
||||
llm_output, tokens_this_round = await self.stream_llm_call(
|
||||
self._conversation_history,
|
||||
temperature=0.1,
|
||||
max_tokens=8192, # 🔥 增加到 8192,避免截断
|
||||
# 🔥 不传递 temperature 和 max_tokens,使用用户配置
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"[{self.name}] LLM call cancelled")
|
||||
|
|
@ -525,8 +584,7 @@ Final Answer:""",
|
|||
try:
|
||||
summary_output, _ = await self.stream_llm_call(
|
||||
self._conversation_history,
|
||||
temperature=0.1,
|
||||
max_tokens=2048,
|
||||
# 🔥 不传递 temperature 和 max_tokens,使用用户配置
|
||||
)
|
||||
|
||||
if summary_output and summary_output.strip():
|
||||
|
|
|
|||
|
|
@ -32,90 +32,188 @@ VERIFICATION_SYSTEM_PROMPT = """你是 DeepAudit 的漏洞验证 Agent,一个*
|
|||
你是漏洞验证的**大脑**,不是机械验证器。你需要:
|
||||
1. 理解每个漏洞的上下文
|
||||
2. 设计合适的验证策略
|
||||
3. 使用工具获取更多信息
|
||||
3. **编写测试代码进行动态验证**
|
||||
4. 判断漏洞是否真实存在
|
||||
5. 评估实际影响
|
||||
5. 评估实际影响并生成 PoC
|
||||
|
||||
## 核心理念:Fuzzing Harness
|
||||
即使整个项目无法运行,你也应该能够验证漏洞!方法是:
|
||||
1. **提取目标函数** - 从代码中提取存在漏洞的函数
|
||||
2. **构建 Mock** - 模拟函数依赖(数据库、HTTP、文件系统等)
|
||||
3. **编写测试脚本** - 构造各种恶意输入测试函数
|
||||
4. **分析执行结果** - 判断是否触发漏洞
|
||||
|
||||
## 你可以使用的工具
|
||||
|
||||
### 🔥 核心验证工具(优先使用)
|
||||
- **run_code**: 执行你编写的测试代码(支持 Python/PHP/JS/Ruby/Go/Java/Bash)
|
||||
- 用于运行 Fuzzing Harness、PoC 脚本
|
||||
- 你可以完全控制测试逻辑
|
||||
- 参数: code (str), language (str), timeout (int), description (str)
|
||||
|
||||
- **extract_function**: 从源文件提取指定函数代码
|
||||
- 用于获取目标函数,构建 Fuzzing Harness
|
||||
- 参数: file_path (str), function_name (str), include_imports (bool)
|
||||
|
||||
### 文件操作
|
||||
- **read_file**: 读取更多代码上下文
|
||||
- **read_file**: 读取代码文件获取上下文
|
||||
参数: file_path (str), start_line (int), end_line (int)
|
||||
- **list_files**: ⚠️ 仅用于确认文件是否存在,严禁遍历
|
||||
参数: directory (str), pattern (str)
|
||||
|
||||
### 沙箱核心工具
|
||||
- **sandbox_exec**: 在沙箱中执行命令
|
||||
参数: command (str), timeout (int)
|
||||
- **sandbox_http**: 发送 HTTP 请求测试
|
||||
参数: method (str), url (str), data (dict), headers (dict)
|
||||
- **verify_vulnerability**: 自动化漏洞验证
|
||||
参数: vulnerability_type (str), target_url (str), payload (str), expected_pattern (str)
|
||||
### 沙箱工具
|
||||
- **sandbox_exec**: 在沙箱中执行命令(用于验证命令执行类漏洞)
|
||||
- **sandbox_http**: 发送 HTTP 请求(如果有运行的服务)
|
||||
|
||||
### 🔥 多语言代码测试工具 (按语言选择)
|
||||
- **php_test**: 测试 PHP 代码,支持模拟 GET/POST 参数
|
||||
参数: file_path (str), php_code (str), get_params (dict), post_params (dict), timeout (int)
|
||||
示例: {"file_path": "vuln.php", "get_params": {"cmd": "whoami"}}
|
||||
## 🔥 Fuzzing Harness 编写指南
|
||||
|
||||
- **python_test**: 测试 Python 代码,支持模拟 Flask/Django 请求
|
||||
参数: file_path (str), code (str), request_params (dict), form_data (dict), timeout (int)
|
||||
示例: {"code": "import os; os.system(params['cmd'])", "request_params": {"cmd": "id"}}
|
||||
### 原则
|
||||
1. **你是大脑** - 你决定测试策略、payload、检测方法
|
||||
2. **不依赖完整项目** - 提取函数,mock 依赖,隔离测试
|
||||
3. **多种 payload** - 设计多种恶意输入,不要只测一个
|
||||
4. **检测漏洞特征** - 根据漏洞类型设计检测逻辑
|
||||
|
||||
- **javascript_test**: 测试 JavaScript/Node.js 代码
|
||||
参数: file_path (str), code (str), req_query (dict), req_body (dict), timeout (int)
|
||||
示例: {"code": "exec(req.query.cmd)", "req_query": {"cmd": "id"}}
|
||||
### 命令注入 Fuzzing Harness 示例 (Python)
|
||||
```python
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
- **java_test**: 测试 Java 代码,支持模拟 Servlet 请求
|
||||
参数: file_path (str), code (str), request_params (dict), timeout (int)
|
||||
# === Mock 危险函数来检测调用 ===
|
||||
executed_commands = []
|
||||
original_system = os.system
|
||||
|
||||
- **go_test**: 测试 Go 代码
|
||||
参数: file_path (str), code (str), args (list), timeout (int)
|
||||
def mock_system(cmd):
|
||||
print(f"[DETECTED] os.system called: {cmd}")
|
||||
executed_commands.append(cmd)
|
||||
return 0
|
||||
|
||||
- **ruby_test**: 测试 Ruby 代码,支持模拟 Rails 请求
|
||||
参数: file_path (str), code (str), params (dict), timeout (int)
|
||||
os.system = mock_system
|
||||
|
||||
- **shell_test**: 测试 Shell/Bash 脚本
|
||||
参数: file_path (str), code (str), args (list), env (dict), timeout (int)
|
||||
# === 目标函数(从项目代码复制) ===
|
||||
def vulnerable_function(user_input):
|
||||
os.system(f"echo {user_input}")
|
||||
|
||||
- **universal_code_test**: 通用多语言测试工具 (自动检测语言)
|
||||
参数: language (str), file_path (str), code (str), params (dict), timeout (int)
|
||||
# === Fuzzing 测试 ===
|
||||
payloads = [
|
||||
"test", # 正常输入
|
||||
"; id", # 命令连接符
|
||||
"| whoami", # 管道
|
||||
"$(cat /etc/passwd)", # 命令替换
|
||||
"`id`", # 反引号
|
||||
"&& ls -la", # AND 连接
|
||||
]
|
||||
|
||||
### 🔥 漏洞验证专用工具 (按漏洞类型选择,推荐使用)
|
||||
- **test_command_injection**: 专门测试命令注入漏洞
|
||||
参数: target_file (str), param_name (str), test_command (str), language (str)
|
||||
示例: {"target_file": "vuln.php", "param_name": "cmd", "test_command": "whoami"}
|
||||
print("=== Fuzzing Start ===")
|
||||
for payload in payloads:
|
||||
print(f"\\nPayload: {payload}")
|
||||
executed_commands.clear()
|
||||
try:
|
||||
vulnerable_function(payload)
|
||||
if executed_commands:
|
||||
print(f"[VULN] Detected! Commands: {executed_commands}")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] {e}")
|
||||
```
|
||||
|
||||
- **test_sql_injection**: 专门测试 SQL 注入漏洞
|
||||
参数: target_file (str), param_name (str), db_type (str), injection_type (str)
|
||||
示例: {"target_file": "login.php", "param_name": "username", "db_type": "mysql"}
|
||||
### SQL 注入 Fuzzing Harness 示例 (Python)
|
||||
```python
|
||||
# === Mock 数据库 ===
|
||||
class MockCursor:
|
||||
def __init__(self):
|
||||
self.queries = []
|
||||
|
||||
- **test_xss**: 专门测试 XSS 漏洞
|
||||
参数: target_file (str), param_name (str), xss_type (str), context (str)
|
||||
示例: {"target_file": "search.php", "param_name": "q", "xss_type": "reflected"}
|
||||
def execute(self, query, params=None):
|
||||
print(f"[SQL] Query: {query}")
|
||||
print(f"[SQL] Params: {params}")
|
||||
self.queries.append((query, params))
|
||||
|
||||
- **test_path_traversal**: 专门测试路径遍历漏洞
|
||||
参数: target_file (str), param_name (str), target_path (str)
|
||||
示例: {"target_file": "download.php", "param_name": "file", "target_path": "/etc/passwd"}
|
||||
# 检测 SQL 注入特征
|
||||
if params is None and ("'" in query or "OR" in query.upper() or "--" in query):
|
||||
print("[VULN] Possible SQL injection - no parameterized query!")
|
||||
|
||||
- **test_ssti**: 专门测试模板注入漏洞
|
||||
参数: target_file (str), param_name (str), template_engine (str)
|
||||
示例: {"target_file": "render.py", "param_name": "name", "template_engine": "jinja2"}
|
||||
class MockDB:
|
||||
def cursor(self):
|
||||
return MockCursor()
|
||||
|
||||
- **test_deserialization**: 专门测试反序列化漏洞
|
||||
参数: target_file (str), language (str), serialization_format (str)
|
||||
示例: {"target_file": "api.php", "language": "php", "serialization_format": "php_serialize"}
|
||||
# === 目标函数 ===
|
||||
def get_user(db, user_id):
|
||||
cursor = db.cursor()
|
||||
cursor.execute(f"SELECT * FROM users WHERE id = '{user_id}'") # 漏洞!
|
||||
|
||||
- **universal_vuln_test**: 通用漏洞测试工具 (自动选择测试策略)
|
||||
参数: vuln_type (str), target_file (str), param_name (str), additional_params (dict)
|
||||
支持: command_injection, sql_injection, xss, path_traversal, ssti, deserialization
|
||||
# === Fuzzing ===
|
||||
db = MockDB()
|
||||
payloads = ["1", "1'", "1' OR '1'='1", "1'; DROP TABLE users--", "1 UNION SELECT * FROM admin"]
|
||||
|
||||
## 工作方式
|
||||
你将收到一批待验证的漏洞发现。对于每个发现,你需要:
|
||||
for p in payloads:
|
||||
print(f"\\n=== Testing: {p} ===")
|
||||
get_user(db, p)
|
||||
```
|
||||
|
||||
### PHP 命令注入 Fuzzing Harness 示例
|
||||
```php
|
||||
// 注意:php -r 不需要 <?php 标签
|
||||
|
||||
// Mock $_GET
|
||||
$_GET['cmd'] = '; id';
|
||||
$_POST['cmd'] = '; id';
|
||||
$_REQUEST['cmd'] = '; id';
|
||||
|
||||
// 目标代码(从项目复制)
|
||||
$output = shell_exec($_GET['cmd']);
|
||||
echo "Output: " . $output;
|
||||
|
||||
// 如果有输出,说明命令被执行
|
||||
if ($output) {
|
||||
echo "\\n[VULN] Command executed!";
|
||||
}
|
||||
```
|
||||
|
||||
### XSS 检测 Harness 示例 (Python)
|
||||
```python
|
||||
def vulnerable_render(user_input):
|
||||
# 模拟模板渲染
|
||||
return f"<div>Hello, {user_input}!</div>"
|
||||
|
||||
payloads = [
|
||||
"test",
|
||||
"<script>alert(1)</script>",
|
||||
"<img src=x onerror=alert(1)>",
|
||||
"{{7*7}}", # SSTI
|
||||
]
|
||||
|
||||
for p in payloads:
|
||||
output = vulnerable_render(p)
|
||||
print(f"Input: {p}")
|
||||
print(f"Output: {output}")
|
||||
# 检测:payload 是否原样出现在输出中
|
||||
if p in output and ("<" in p or "{{" in p):
|
||||
print("[VULN] XSS - input not escaped!")
|
||||
```
|
||||
|
||||
## 验证策略
|
||||
|
||||
### 对于可执行的漏洞(命令注入、代码注入等)
|
||||
1. 使用 `extract_function` 或 `read_file` 获取目标代码
|
||||
2. 编写 Fuzzing Harness,mock 危险函数来检测调用
|
||||
3. 使用 `run_code` 执行 Harness
|
||||
4. 分析输出,确认漏洞是否触发
|
||||
|
||||
### 对于数据泄露型漏洞(SQL注入、路径遍历等)
|
||||
1. 获取目标代码
|
||||
2. 编写 Harness,mock 数据库/文件系统
|
||||
3. 检查是否能构造恶意查询/路径
|
||||
4. 分析输出
|
||||
|
||||
### 对于配置类漏洞(硬编码密钥等)
|
||||
1. 使用 `read_file` 直接读取配置文件
|
||||
2. 验证敏感信息是否存在
|
||||
3. 评估影响(密钥是否有效、权限范围等)
|
||||
|
||||
## 工作流程
|
||||
你将收到一批待验证的漏洞发现。对于每个发现:
|
||||
|
||||
```
|
||||
Thought: [分析这个漏洞,思考如何验证]
|
||||
Thought: [分析漏洞类型,设计验证策略]
|
||||
Action: [工具名称]
|
||||
Action Input: [JSON 格式的参数]
|
||||
Action Input: [参数]
|
||||
```
|
||||
|
||||
验证完所有发现后,输出:
|
||||
|
|
@ -125,6 +223,29 @@ Thought: [总结验证结果]
|
|||
Final Answer: [JSON 格式的验证报告]
|
||||
```
|
||||
|
||||
## ⚠️ 输出格式要求(严格遵守)
|
||||
|
||||
**禁止使用 Markdown 格式标记!** 你的输出必须是纯文本格式:
|
||||
|
||||
✅ 正确格式:
|
||||
```
|
||||
Thought: 我需要读取 search.php 文件来验证 SQL 注入漏洞。
|
||||
Action: read_file
|
||||
Action Input: {"file_path": "search.php"}
|
||||
```
|
||||
|
||||
❌ 错误格式(禁止使用):
|
||||
```
|
||||
**Thought:** 我需要读取文件
|
||||
**Action:** read_file
|
||||
**Action Input:** {"file_path": "search.php"}
|
||||
```
|
||||
|
||||
规则:
|
||||
1. 不要在 Thought:、Action:、Action Input:、Final Answer: 前后添加 `**`
|
||||
2. 不要使用其他 Markdown 格式(如 `###`、`*斜体*` 等)
|
||||
3. Action Input 必须是完整的 JSON 对象,不能为空或截断
|
||||
|
||||
## Final Answer 格式
|
||||
```json
|
||||
{
|
||||
|
|
@ -139,7 +260,8 @@ Final Answer: [JSON 格式的验证报告]
|
|||
"poc": {
|
||||
"description": "PoC 描述",
|
||||
"steps": ["步骤1", "步骤2"],
|
||||
"payload": "curl 'http://target/vuln.php?cmd=id' 或完整利用代码"
|
||||
"payload": "完整可执行的 PoC 代码或命令",
|
||||
"harness_code": "Fuzzing Harness 代码(如果使用)"
|
||||
},
|
||||
"impact": "实际影响分析",
|
||||
"recommendation": "修复建议"
|
||||
|
|
@ -155,82 +277,49 @@ Final Answer: [JSON 格式的验证报告]
|
|||
```
|
||||
|
||||
## 验证判定标准
|
||||
- **confirmed**: 漏洞确认存在且可利用,有明确证据
|
||||
- **likely**: 高度可能存在漏洞,但无法完全确认
|
||||
- **confirmed**: 漏洞确认存在且可利用,有明确证据(如 Harness 成功触发)
|
||||
- **likely**: 高度可能存在漏洞,代码分析明确但无法动态验证
|
||||
- **uncertain**: 需要更多信息才能判断
|
||||
- **false_positive**: 确认是误报,有明确理由
|
||||
|
||||
## 验证策略建议
|
||||
## 🚨 防止幻觉验证(关键!)
|
||||
|
||||
### 对于命令注入漏洞
|
||||
1. 使用 **test_command_injection** 工具,它会自动构建测试环境
|
||||
2. 或使用对应语言的测试工具 (php_test, python_test 等)
|
||||
3. 检查命令输出是否包含 uid=, root, www-data 等特征
|
||||
**Analysis Agent 可能报告不存在的文件!** 你必须验证:
|
||||
|
||||
### 对于 SQL 注入漏洞
|
||||
1. 使用 **test_sql_injection** 工具
|
||||
2. 提供数据库类型 (mysql, postgresql, sqlite)
|
||||
3. 检查是否能执行 UNION 查询或提取数据
|
||||
1. **文件必须存在** - 使用 read_file 读取发现中指定的文件
|
||||
- 如果 read_file 返回"文件不存在",该发现是 **false_positive**
|
||||
- 不要尝试"猜测"正确的文件路径
|
||||
|
||||
### 对于 XSS 漏洞
|
||||
1. 使用 **test_xss** 工具
|
||||
2. 指定 XSS 类型 (reflected, stored, dom)
|
||||
3. 检查 payload 是否在输出中未转义
|
||||
2. **代码必须匹配** - 发现中的 code_snippet 必须在文件中真实存在
|
||||
- 如果文件内容与描述不符,该发现是 **false_positive**
|
||||
|
||||
### 对于路径遍历漏洞
|
||||
1. 使用 **test_path_traversal** 工具
|
||||
2. 尝试读取 /etc/passwd 或其他已知文件
|
||||
3. 检查是否能访问目标文件
|
||||
3. **不要"填补"缺失信息** - 如果发现缺少关键信息(如文件路径为空),标记为 uncertain
|
||||
|
||||
### 对于模板注入 (SSTI) 漏洞
|
||||
1. 使用 **test_ssti** 工具
|
||||
2. 指定模板引擎 (jinja2, twig, freemarker 等)
|
||||
3. 检查数学表达式是否被执行
|
||||
❌ 错误做法:
|
||||
```
|
||||
发现: "SQL注入在 api/database.py:45"
|
||||
read_file 返回: "文件不存在"
|
||||
判定: confirmed <- 这是错误的!
|
||||
```
|
||||
|
||||
### 对于反序列化漏洞
|
||||
1. 使用 **test_deserialization** 工具
|
||||
2. 指定语言和序列化格式
|
||||
3. 检查是否能执行任意代码
|
||||
✅ 正确做法:
|
||||
```
|
||||
发现: "SQL注入在 api/database.py:45"
|
||||
read_file 返回: "文件不存在"
|
||||
判定: false_positive,理由: "文件 api/database.py 不存在"
|
||||
```
|
||||
|
||||
### 对于其他漏洞
|
||||
1. **上下文分析**: 用 read_file 获取更多代码上下文
|
||||
2. **通用测试**: 使用 universal_vuln_test 或 universal_code_test
|
||||
3. **沙箱测试**: 对高危漏洞用沙箱进行安全测试
|
||||
## ⚠️ 关键约束
|
||||
1. **必须先调用工具验证** - 不允许仅凭已知信息直接判断
|
||||
2. **优先使用 run_code** - 编写 Harness 进行动态验证
|
||||
3. **PoC 必须完整可执行** - poc.payload 应该是可直接运行的代码
|
||||
4. **不要假设环境** - 沙箱中没有运行的服务,需要 mock
|
||||
|
||||
## 重要原则
|
||||
1. **质量优先** - 宁可漏报也不要误报太多
|
||||
2. **深入理解** - 理解代码逻辑,不要表面判断
|
||||
3. **证据支撑** - 判定要有依据
|
||||
4. **安全第一** - 沙箱测试要谨慎
|
||||
5. **🔥 PoC 生成** - 对于 confirmed 和 likely 的漏洞,**必须**生成完整的 PoC:
|
||||
- poc.description: 简要描述这个 PoC 的作用
|
||||
- poc.steps: 详细的复现步骤列表
|
||||
- poc.payload: **完整的**利用代码或命令,例如:
|
||||
- Web漏洞: 完整URL如 `http://target/path?param=<payload>`
|
||||
- 命令注入: 完整的 curl 命令或 HTTP 请求
|
||||
- SQL注入: 完整的利用语句或请求
|
||||
- 代码执行: 可直接运行的利用脚本
|
||||
- ⚠️ payload 字段必须是**可直接复制执行**的完整利用代码,不要只写参数值
|
||||
|
||||
## ⚠️ 关键约束 - 必须遵守!
|
||||
1. **禁止直接输出 Final Answer** - 你必须先调用至少一个工具来验证漏洞
|
||||
2. **每个漏洞至少调用一次工具** - 使用 read_file 读取代码,或使用 test_* 工具测试
|
||||
3. **没有工具调用的验证无效** - 不允许仅凭已知信息直接判断
|
||||
4. **先 Action 后 Final Answer** - 必须先执行工具,获取 Observation,再输出最终结论
|
||||
|
||||
错误示例(禁止):
|
||||
```
|
||||
Thought: 根据已有信息,我认为这是漏洞
|
||||
Final Answer: {...} ❌ 没有调用任何工具!
|
||||
```
|
||||
|
||||
正确示例(必须):
|
||||
```
|
||||
Thought: 我需要先读取 config.php 文件来验证硬编码凭据
|
||||
Action: read_file
|
||||
Action Input: {"file_path": "config.php"}
|
||||
```
|
||||
然后等待 Observation,再继续验证其他发现或输出 Final Answer。
|
||||
1. **你是验证的大脑** - 你决定如何测试,工具只提供执行能力
|
||||
2. **动态验证优先** - 能运行代码验证的就不要仅靠静态分析
|
||||
3. **质量优先** - 宁可漏报也不要误报太多
|
||||
4. **证据支撑** - 每个判定都需要有依据
|
||||
|
||||
现在开始验证漏洞发现!"""
|
||||
|
||||
|
|
@ -284,13 +373,21 @@ class VerificationAgent(BaseAgent):
|
|||
"""解析 LLM 响应 - 增强版,更健壮地提取思考内容"""
|
||||
step = VerificationStep(thought="")
|
||||
|
||||
# 🔥 v2.1: 预处理 - 移除 Markdown 格式标记(LLM 有时会输出 **Action:** 而非 Action:)
|
||||
cleaned_response = response
|
||||
cleaned_response = re.sub(r'\*\*Action:\*\*', 'Action:', cleaned_response)
|
||||
cleaned_response = re.sub(r'\*\*Action Input:\*\*', 'Action Input:', cleaned_response)
|
||||
cleaned_response = re.sub(r'\*\*Thought:\*\*', 'Thought:', cleaned_response)
|
||||
cleaned_response = re.sub(r'\*\*Final Answer:\*\*', 'Final Answer:', cleaned_response)
|
||||
cleaned_response = re.sub(r'\*\*Observation:\*\*', 'Observation:', cleaned_response)
|
||||
|
||||
# 🔥 首先尝试提取明确的 Thought 标记
|
||||
thought_match = re.search(r'Thought:\s*(.*?)(?=Action:|Final Answer:|$)', response, re.DOTALL)
|
||||
thought_match = re.search(r'Thought:\s*(.*?)(?=Action:|Final Answer:|$)', cleaned_response, re.DOTALL)
|
||||
if thought_match:
|
||||
step.thought = thought_match.group(1).strip()
|
||||
|
||||
# 🔥 检查是否是最终答案
|
||||
final_match = re.search(r'Final Answer:\s*(.*?)$', response, re.DOTALL)
|
||||
final_match = re.search(r'Final Answer:\s*(.*?)$', cleaned_response, re.DOTALL)
|
||||
if final_match:
|
||||
step.is_final = True
|
||||
answer_text = final_match.group(1).strip()
|
||||
|
|
@ -310,7 +407,7 @@ class VerificationAgent(BaseAgent):
|
|||
|
||||
# 🔥 如果没有提取到 thought,使用 Final Answer 前的内容作为思考
|
||||
if not step.thought:
|
||||
before_final = response[:response.find('Final Answer:')].strip()
|
||||
before_final = cleaned_response[:cleaned_response.find('Final Answer:')].strip()
|
||||
if before_final:
|
||||
before_final = re.sub(r'^Thought:\s*', '', before_final)
|
||||
step.thought = before_final[:500] if len(before_final) > 500 else before_final
|
||||
|
|
@ -318,30 +415,40 @@ class VerificationAgent(BaseAgent):
|
|||
return step
|
||||
|
||||
# 🔥 提取 Action
|
||||
action_match = re.search(r'Action:\s*(\w+)', response)
|
||||
action_match = re.search(r'Action:\s*(\w+)', cleaned_response)
|
||||
if action_match:
|
||||
step.action = action_match.group(1).strip()
|
||||
|
||||
# 🔥 如果没有提取到 thought,提取 Action 之前的内容作为思考
|
||||
if not step.thought:
|
||||
action_pos = response.find('Action:')
|
||||
action_pos = cleaned_response.find('Action:')
|
||||
if action_pos > 0:
|
||||
before_action = response[:action_pos].strip()
|
||||
before_action = cleaned_response[:action_pos].strip()
|
||||
before_action = re.sub(r'^Thought:\s*', '', before_action)
|
||||
if before_action:
|
||||
step.thought = before_action[:500] if len(before_action) > 500 else before_action
|
||||
|
||||
# 🔥 提取 Action Input
|
||||
input_match = re.search(r'Action Input:\s*(.*?)(?=Thought:|Action:|Observation:|$)', response, re.DOTALL)
|
||||
# 🔥 提取 Action Input - 增强版,处理多种格式
|
||||
input_match = re.search(r'Action Input:\s*(.*?)(?=Thought:|Action:|Observation:|$)', cleaned_response, re.DOTALL)
|
||||
if input_match:
|
||||
input_text = input_match.group(1).strip()
|
||||
input_text = re.sub(r'```json\s*', '', input_text)
|
||||
input_text = re.sub(r'```\s*', '', input_text)
|
||||
# 使用增强的 JSON 解析器
|
||||
step.action_input = AgentJsonParser.parse(
|
||||
input_text,
|
||||
default={"raw_input": input_text}
|
||||
)
|
||||
|
||||
# 🔥 v2.1: 如果 Action Input 为空或只有 **,记录警告
|
||||
if not input_text or input_text == '**' or input_text.strip() == '':
|
||||
logger.warning(f"[Verification] Action Input is empty or malformed: '{input_text}'")
|
||||
step.action_input = {}
|
||||
else:
|
||||
# 使用增强的 JSON 解析器
|
||||
step.action_input = AgentJsonParser.parse(
|
||||
input_text,
|
||||
default={"raw_input": input_text}
|
||||
)
|
||||
elif step.action:
|
||||
# 🔥 v2.1: 有 Action 但没有 Action Input,记录警告
|
||||
logger.warning(f"[Verification] Action '{step.action}' found but no Action Input")
|
||||
step.action_input = {}
|
||||
|
||||
# 🔥 最后的 fallback:如果整个响应没有任何标记,整体作为思考
|
||||
if not step.thought and not step.action and not step.is_final:
|
||||
|
|
@ -548,8 +655,7 @@ class VerificationAgent(BaseAgent):
|
|||
try:
|
||||
llm_output, tokens_this_round = await self.stream_llm_call(
|
||||
self._conversation_history,
|
||||
temperature=0.1,
|
||||
max_tokens=8192, # 🔥 增加到 8192,避免截断
|
||||
# 🔥 不传递 temperature 和 max_tokens,使用用户配置
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"[{self.name}] LLM call cancelled")
|
||||
|
|
@ -583,6 +689,24 @@ class VerificationAgent(BaseAgent):
|
|||
|
||||
# 检查是否完成
|
||||
if step.is_final:
|
||||
# 🔥 强制检查:必须至少调用过一次工具才能完成
|
||||
if self._tool_calls == 0:
|
||||
logger.warning(f"[{self.name}] LLM tried to finish without any tool calls! Forcing tool usage.")
|
||||
await self.emit_thinking("⚠️ 拒绝过早完成:必须先使用工具验证漏洞")
|
||||
self._conversation_history.append({
|
||||
"role": "user",
|
||||
"content": (
|
||||
"⚠️ **系统拒绝**: 你必须先使用工具验证漏洞!\n\n"
|
||||
"不允许在没有调用任何工具的情况下直接输出 Final Answer。\n\n"
|
||||
"请立即使用以下工具之一进行验证:\n"
|
||||
"1. `read_file` - 读取漏洞所在文件的代码\n"
|
||||
"2. `run_code` - 编写并执行 Fuzzing Harness 验证漏洞\n"
|
||||
"3. `extract_function` - 提取目标函数进行分析\n\n"
|
||||
"现在请输出 Thought 和 Action,开始验证第一个漏洞。"
|
||||
),
|
||||
})
|
||||
continue
|
||||
|
||||
await self.emit_llm_decision("完成漏洞验证", "LLM 判断验证已充分")
|
||||
final_result = step.final_answer
|
||||
|
||||
|
|
@ -604,8 +728,39 @@ class VerificationAgent(BaseAgent):
|
|||
# 🔥 发射 LLM 动作决策事件
|
||||
await self.emit_llm_action(step.action, step.action_input or {})
|
||||
|
||||
# 🔥 循环检测:追踪工具调用失败历史
|
||||
start_tool_time = time.time()
|
||||
|
||||
# 🔥 智能循环检测: 追踪重复调用 (无论成功与否)
|
||||
tool_call_key = f"{step.action}:{json.dumps(step.action_input or {}, sort_keys=True)}"
|
||||
|
||||
if not hasattr(self, '_tool_call_counts'):
|
||||
self._tool_call_counts = {}
|
||||
|
||||
self._tool_call_counts[tool_call_key] = self._tool_call_counts.get(tool_call_key, 0) + 1
|
||||
|
||||
# 如果同一操作重复尝试超过3次,强制干预
|
||||
if self._tool_call_counts[tool_call_key] > 3:
|
||||
logger.warning(f"[{self.name}] Detected repetitive tool call loop: {tool_call_key}")
|
||||
observation = (
|
||||
f"⚠️ **系统干预**: 你已经使用完全相同的参数调用了工具 '{step.action}' 超过3次。\n"
|
||||
"请**不要**重复尝试相同的操作。这是无效的。\n"
|
||||
"请尝试:\n"
|
||||
"1. 修改参数 (例如改变 input payload)\n"
|
||||
"2. 使用不同的工具 (例如从 sandbox_exec 换到 php_test)\n"
|
||||
"3. 如果之前的尝试都失败了,请尝试 analyze_file 重新分析代码\n"
|
||||
"4. 如果无法验证,请输出 Final Answer 并标记为 uncertain"
|
||||
)
|
||||
|
||||
# 模拟观察结果,跳过实际执行
|
||||
step.observation = observation
|
||||
await self.emit_llm_observation(observation)
|
||||
self._conversation_history.append({
|
||||
"role": "user",
|
||||
"content": f"Observation:\n{observation}",
|
||||
})
|
||||
continue
|
||||
|
||||
# 🔥 循环检测:追踪工具调用失败历史 (保留原有逻辑用于错误追踪)
|
||||
if not hasattr(self, '_failed_tool_calls'):
|
||||
self._failed_tool_calls = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -313,7 +313,7 @@ class EventManager:
|
|||
try:
|
||||
self._event_queues[task_id].put_nowait(event_data)
|
||||
# 🔥 DEBUG: 记录重要事件被添加到队列
|
||||
if event_type in ["thinking_start", "thinking_end", "dispatch", "task_complete", "task_error"]:
|
||||
if event_type in ["thinking_start", "thinking_end", "dispatch", "task_complete", "task_error", "tool_call", "tool_result", "llm_action"]:
|
||||
logger.info(f"[EventQueue] Added {event_type} to queue for task {task_id}, queue size: {self._event_queues[task_id].qsize()}")
|
||||
elif event_type == "thinking_token":
|
||||
# 每10个token记录一次
|
||||
|
|
@ -508,7 +508,7 @@ class EventManager:
|
|||
|
||||
# 🔥 DEBUG: 记录重要事件被发送
|
||||
event_type = event.get("event_type")
|
||||
if event_type in ["thinking_start", "thinking_end", "dispatch", "task_complete", "task_error"]:
|
||||
if event_type in ["thinking_start", "thinking_end", "dispatch", "task_complete", "task_error", "tool_call", "tool_result", "llm_action"]:
|
||||
logger.info(f"[StreamEvents] Yielding {event_type} (seq={event_sequence}) for task {task_id}")
|
||||
|
||||
yield event
|
||||
|
|
|
|||
|
|
@ -125,8 +125,7 @@ class LLMRouter:
|
|||
{"role": "system", "content": "你是安全审计流程的决策者,负责决定下一步行动。"},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=0.1,
|
||||
max_tokens=200,
|
||||
# 🔥 不传递 temperature 和 max_tokens,使用用户配置
|
||||
)
|
||||
|
||||
content = response.get("content", "")
|
||||
|
|
@ -180,8 +179,7 @@ class LLMRouter:
|
|||
{"role": "system", "content": "你是安全审计流程的决策者,负责决定下一步行动。"},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=0.1,
|
||||
max_tokens=200,
|
||||
# 🔥 不传递 temperature 和 max_tokens,使用用户配置
|
||||
)
|
||||
|
||||
content = response.get("content", "")
|
||||
|
|
@ -227,8 +225,7 @@ class LLMRouter:
|
|||
{"role": "system", "content": "你是安全审计流程的决策者,负责决定下一步行动。"},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=0.1,
|
||||
max_tokens=200,
|
||||
# 🔥 不传递 temperature 和 max_tokens,使用用户配置
|
||||
)
|
||||
|
||||
content = response.get("content", "")
|
||||
|
|
|
|||
|
|
@ -331,8 +331,8 @@ class AgentRunner:
|
|||
self.verification_tools = {
|
||||
**base_tools,
|
||||
# 验证工具 - 移除旧的 vulnerability_validation 和 dataflow_analysis,强制使用沙箱
|
||||
# 🔥 新增:漏洞报告工具(仅Verification可用)
|
||||
"create_vulnerability_report": CreateVulnerabilityReportTool(),
|
||||
# 🔥 新增:漏洞报告工具(仅Verification可用)- v2.1: 传递 project_root
|
||||
"create_vulnerability_report": CreateVulnerabilityReportTool(self.project_root),
|
||||
# 🔥 新增:反思工具
|
||||
"reflect": ReflectTool(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,10 @@ class VulnerabilityKnowledgeInput(BaseModel):
|
|||
...,
|
||||
description="漏洞类型,如: sql_injection, xss, command_injection, path_traversal, ssrf, deserialization, hardcoded_secrets, auth_bypass"
|
||||
)
|
||||
project_language: Optional[str] = Field(
|
||||
None,
|
||||
description="目标项目的主要编程语言(如 python, php, javascript, rust, go),用于过滤相关示例"
|
||||
)
|
||||
|
||||
|
||||
class GetVulnerabilityKnowledgeTool(AgentTool):
|
||||
|
|
@ -165,13 +169,13 @@ class GetVulnerabilityKnowledgeTool(AgentTool):
|
|||
def args_schema(self) -> Type[BaseModel]:
|
||||
return VulnerabilityKnowledgeInput
|
||||
|
||||
async def _execute(self, vulnerability_type: str) -> ToolResult:
|
||||
async def _execute(self, vulnerability_type: str, project_language: Optional[str] = None) -> ToolResult:
|
||||
"""获取漏洞知识"""
|
||||
try:
|
||||
knowledge = await security_knowledge_rag.get_vulnerability_knowledge(
|
||||
vulnerability_type
|
||||
)
|
||||
|
||||
|
||||
if not knowledge:
|
||||
available = security_knowledge_rag.get_all_vulnerability_types()
|
||||
return ToolResult(
|
||||
|
|
@ -179,27 +183,55 @@ class GetVulnerabilityKnowledgeTool(AgentTool):
|
|||
data=f"未找到漏洞类型 '{vulnerability_type}' 的知识。\n\n可用的漏洞类型: {', '.join(available)}",
|
||||
metadata={"available_types": available},
|
||||
)
|
||||
|
||||
|
||||
# 格式化输出
|
||||
output_parts = [
|
||||
f"# {knowledge.get('title', vulnerability_type)}",
|
||||
f"严重程度: {knowledge.get('severity', 'N/A')}",
|
||||
]
|
||||
|
||||
|
||||
if knowledge.get("cwe_ids"):
|
||||
output_parts.append(f"CWE: {', '.join(knowledge['cwe_ids'])}")
|
||||
if knowledge.get("owasp_ids"):
|
||||
output_parts.append(f"OWASP: {', '.join(knowledge['owasp_ids'])}")
|
||||
|
||||
|
||||
# 🔥 v2.2: 添加语言不匹配警告
|
||||
content = knowledge.get("content", "")
|
||||
knowledge_lang = self._detect_code_language(content)
|
||||
|
||||
if project_language and knowledge_lang:
|
||||
project_lang_lower = project_language.lower()
|
||||
if knowledge_lang.lower() != project_lang_lower:
|
||||
output_parts.append("")
|
||||
output_parts.append("=" * 60)
|
||||
output_parts.append(f"⚠️ **重要警告**: 以下示例代码是 {knowledge_lang.upper()} 语言")
|
||||
output_parts.append(f" 你正在审计的项目是 {project_language.upper()} 项目")
|
||||
output_parts.append(" **这些代码示例仅供概念参考,不要直接套用到目标项目!**")
|
||||
output_parts.append(" 请在目标项目中查找该语言特有的等效漏洞模式。")
|
||||
output_parts.append("=" * 60)
|
||||
|
||||
output_parts.append("")
|
||||
output_parts.append(knowledge.get("content", ""))
|
||||
|
||||
output_parts.append(content)
|
||||
|
||||
# 🔥 v2.2: 添加使用指南
|
||||
output_parts.append("")
|
||||
output_parts.append("---")
|
||||
output_parts.append("📌 **使用指南**:")
|
||||
output_parts.append("1. 以上知识仅供参考,你必须在实际代码中验证漏洞是否存在")
|
||||
output_parts.append("2. 不要假设项目中存在示例中的代码模式")
|
||||
output_parts.append("3. 只有在 read_file 读取到的代码中确实存在问题时才报告漏洞")
|
||||
output_parts.append("4. 如果示例语言与项目语言不同,请查找该语言的等效漏洞模式")
|
||||
|
||||
return ToolResult(
|
||||
success=True,
|
||||
data="\n".join(output_parts),
|
||||
metadata=knowledge,
|
||||
metadata={
|
||||
**knowledge,
|
||||
"knowledge_language": knowledge_lang,
|
||||
"project_language": project_language,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get vulnerability knowledge failed: {e}")
|
||||
return ToolResult(
|
||||
|
|
@ -207,6 +239,35 @@ class GetVulnerabilityKnowledgeTool(AgentTool):
|
|||
error=f"获取漏洞知识失败: {str(e)}",
|
||||
)
|
||||
|
||||
def _detect_code_language(self, content: str) -> Optional[str]:
|
||||
"""检测知识内容中的主要代码语言"""
|
||||
# 检测代码块中的语言标记
|
||||
import re
|
||||
code_blocks = re.findall(r'```(\w+)', content)
|
||||
if code_blocks:
|
||||
# 统计最常见的语言
|
||||
from collections import Counter
|
||||
lang_counts = Counter(code_blocks)
|
||||
most_common = lang_counts.most_common(1)
|
||||
if most_common:
|
||||
return most_common[0][0]
|
||||
|
||||
# 基于内容特征检测
|
||||
if "def " in content or "import " in content or "@app.route" in content:
|
||||
return "python"
|
||||
if "<?php" in content or "$_GET" in content or "$_POST" in content:
|
||||
return "php"
|
||||
if "function " in content and ("const " in content or "let " in content):
|
||||
return "javascript"
|
||||
if "func " in content and "package " in content:
|
||||
return "go"
|
||||
if "fn " in content and "let mut" in content:
|
||||
return "rust"
|
||||
if "public class" in content or "private void" in content:
|
||||
return "java"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class ListKnowledgeModulesInput(BaseModel):
|
||||
"""列出知识模块输入"""
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ def build_specialized_prompt(
|
|||
# 导入系统提示词
|
||||
from .system_prompts import (
|
||||
CORE_SECURITY_PRINCIPLES,
|
||||
FILE_VALIDATION_RULES, # 🔥 v2.1
|
||||
VULNERABILITY_PRIORITIES,
|
||||
TOOL_USAGE_GUIDE,
|
||||
MULTI_AGENT_RULES,
|
||||
|
|
@ -234,6 +235,7 @@ __all__ = [
|
|||
"build_specialized_prompt",
|
||||
# 系统提示词
|
||||
"CORE_SECURITY_PRINCIPLES",
|
||||
"FILE_VALIDATION_RULES", # 🔥 v2.1
|
||||
"VULNERABILITY_PRIORITIES",
|
||||
"TOOL_USAGE_GUIDE",
|
||||
"MULTI_AGENT_RULES",
|
||||
|
|
|
|||
|
|
@ -36,6 +36,60 @@ CORE_SECURITY_PRINCIPLES = """
|
|||
</core_security_principles>
|
||||
"""
|
||||
|
||||
# 🔥 v2.1: 文件路径验证规则 - 防止幻觉
|
||||
FILE_VALIDATION_RULES = """
|
||||
<file_validation_rules>
|
||||
## 🔒 文件路径验证规则(强制执行)
|
||||
|
||||
### ⚠️ 严禁幻觉行为
|
||||
|
||||
在报告任何漏洞之前,你**必须**遵守以下规则:
|
||||
|
||||
1. **先验证文件存在**
|
||||
- 在报告漏洞前,必须使用 `read_file` 或 `list_files` 工具确认文件存在
|
||||
- 禁止基于"典型项目结构"或"常见框架模式"猜测文件路径
|
||||
- 禁止假设 `config/database.py`、`app/api.py` 等文件存在
|
||||
|
||||
2. **引用真实代码**
|
||||
- `code_snippet` 必须来自 `read_file` 工具的实际输出
|
||||
- 禁止凭记忆或推测编造代码片段
|
||||
- 行号必须在文件实际行数范围内
|
||||
|
||||
3. **验证行号准确性**
|
||||
- 报告的 `line_start` 和 `line_end` 必须基于实际读取的文件
|
||||
- 如果不确定行号,使用 `read_file` 重新确认
|
||||
|
||||
4. **匹配项目技术栈**
|
||||
- Rust 项目不会有 `.py` 文件(除非明确存在)
|
||||
- 前端项目不会有后端数据库配置
|
||||
- 仔细观察 Recon Agent 返回的技术栈信息
|
||||
|
||||
### ✅ 正确做法示例
|
||||
|
||||
```
|
||||
# 错误 ❌:直接报告未验证的文件
|
||||
Action: create_vulnerability_report
|
||||
Action Input: {"file_path": "config/database.py", ...}
|
||||
|
||||
# 正确 ✅:先读取验证,再报告
|
||||
Action: read_file
|
||||
Action Input: {"file_path": "config/database.py"}
|
||||
# 如果文件存在且包含漏洞代码,再报告
|
||||
Action: create_vulnerability_report
|
||||
Action Input: {"file_path": "config/database.py", "code_snippet": "实际读取的代码", ...}
|
||||
```
|
||||
|
||||
### 🚫 违规后果
|
||||
|
||||
如果报告的文件路径不存在,系统会:
|
||||
1. 拒绝创建漏洞报告
|
||||
2. 记录违规行为
|
||||
3. 要求重新验证
|
||||
|
||||
**记住:宁可漏报,不可误报。质量优于数量。**
|
||||
</file_validation_rules>
|
||||
"""
|
||||
|
||||
# 漏洞优先级和检测策略
|
||||
VULNERABILITY_PRIORITIES = """
|
||||
<vulnerability_priorities>
|
||||
|
|
@ -313,6 +367,7 @@ def build_enhanced_prompt(
|
|||
include_principles: bool = True,
|
||||
include_priorities: bool = True,
|
||||
include_tools: bool = True,
|
||||
include_validation: bool = True, # 🔥 v2.1: 默认包含文件验证规则
|
||||
) -> str:
|
||||
"""
|
||||
构建增强的提示词
|
||||
|
|
@ -322,6 +377,7 @@ def build_enhanced_prompt(
|
|||
include_principles: 是否包含核心原则
|
||||
include_priorities: 是否包含漏洞优先级
|
||||
include_tools: 是否包含工具指南
|
||||
include_validation: 是否包含文件验证规则
|
||||
|
||||
Returns:
|
||||
增强后的提示词
|
||||
|
|
@ -331,6 +387,10 @@ def build_enhanced_prompt(
|
|||
if include_principles:
|
||||
parts.append(CORE_SECURITY_PRINCIPLES)
|
||||
|
||||
# 🔥 v2.1: 添加文件验证规则
|
||||
if include_validation:
|
||||
parts.append(FILE_VALIDATION_RULES)
|
||||
|
||||
if include_priorities:
|
||||
parts.append(VULNERABILITY_PRIORITIES)
|
||||
|
||||
|
|
@ -342,6 +402,7 @@ def build_enhanced_prompt(
|
|||
|
||||
__all__ = [
|
||||
"CORE_SECURITY_PRINCIPLES",
|
||||
"FILE_VALIDATION_RULES", # 🔥 v2.1
|
||||
"VULNERABILITY_PRIORITIES",
|
||||
"TOOL_USAGE_GUIDE",
|
||||
"MULTI_AGENT_RULES",
|
||||
|
|
|
|||
|
|
@ -82,6 +82,9 @@ from .smart_scan_tool import SmartScanTool, QuickAuditTool
|
|||
# 🔥 新增:Kunlun-M 静态代码分析工具 (MIT License)
|
||||
from .kunlun_tool import KunlunMTool, KunlunRuleListTool, KunlunPluginTool
|
||||
|
||||
# 🔥 新增:通用代码执行工具 (LLM 驱动的 Fuzzing Harness)
|
||||
from .run_code import RunCodeTool, ExtractFunctionTool
|
||||
|
||||
__all__ = [
|
||||
# 基础
|
||||
"AgentTool",
|
||||
|
|
@ -164,4 +167,8 @@ __all__ = [
|
|||
"KunlunMTool",
|
||||
"KunlunRuleListTool",
|
||||
"KunlunPluginTool",
|
||||
|
||||
# 🔥 通用代码执行工具 (LLM 驱动的 Fuzzing Harness)
|
||||
"RunCodeTool",
|
||||
"ExtractFunctionTool",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -19,14 +19,74 @@ from .sandbox_tool import SandboxManager
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============ 公共辅助函数 ============
|
||||
|
||||
def _smart_resolve_target_path(
|
||||
target_path: str,
|
||||
project_root: str,
|
||||
tool_name: str = "Tool"
|
||||
) -> tuple[str, str, Optional[str]]:
|
||||
"""
|
||||
智能解析目标路径
|
||||
|
||||
Args:
|
||||
target_path: 用户/Agent 传入的目标路径
|
||||
project_root: 项目根目录(绝对路径)
|
||||
tool_name: 工具名称(用于日志)
|
||||
|
||||
Returns:
|
||||
(safe_target_path, host_check_path, error_msg)
|
||||
- safe_target_path: 容器内使用的安全路径
|
||||
- host_check_path: 宿主机上的检查路径
|
||||
- error_msg: 如果有错误返回错误信息,否则为 None
|
||||
"""
|
||||
# 获取项目根目录名
|
||||
project_dir_name = os.path.basename(project_root.rstrip('/'))
|
||||
|
||||
if target_path in (".", "", "./"):
|
||||
# 扫描整个项目根目录,在容器内对应 /workspace
|
||||
safe_target_path = "."
|
||||
host_check_path = project_root
|
||||
elif target_path == project_dir_name or target_path == f"./{project_dir_name}":
|
||||
# 🔥 智能修复:Agent 可能把项目名当作子目录传入
|
||||
logger.info(f"[{tool_name}] 智能路径修复: '{target_path}' -> '.' (项目根目录名: {project_dir_name})")
|
||||
safe_target_path = "."
|
||||
host_check_path = project_root
|
||||
else:
|
||||
# 相对路径,需要验证是否存在
|
||||
safe_target_path = target_path.lstrip("/") if target_path.startswith("/") else target_path
|
||||
host_check_path = os.path.join(project_root, safe_target_path)
|
||||
|
||||
# 🔥 智能回退:如果路径不存在,尝试扫描整个项目
|
||||
if not os.path.exists(host_check_path):
|
||||
logger.warning(
|
||||
f"[{tool_name}] 路径 '{target_path}' 不存在于项目中,自动回退到扫描整个项目 "
|
||||
f"(project_root={project_root}, project_dir_name={project_dir_name})"
|
||||
)
|
||||
# 回退到扫描整个项目
|
||||
safe_target_path = "."
|
||||
host_check_path = project_root
|
||||
|
||||
# 最终检查
|
||||
if not os.path.exists(host_check_path):
|
||||
error_msg = f"目标路径不存在: {target_path} (完整路径: {host_check_path})"
|
||||
logger.error(f"[{tool_name}] {error_msg}")
|
||||
return safe_target_path, host_check_path, error_msg
|
||||
|
||||
return safe_target_path, host_check_path, None
|
||||
|
||||
|
||||
# ============ Semgrep 工具 ============
|
||||
|
||||
class SemgrepInput(BaseModel):
|
||||
"""Semgrep 扫描输入"""
|
||||
target_path: str = Field(description="要扫描的目录或文件路径(相对于项目根目录)")
|
||||
target_path: str = Field(
|
||||
default=".",
|
||||
description="要扫描的路径。⚠️ 重要:使用 '.' 扫描整个项目(推荐),或使用 'src/' 等子目录。不要使用项目目录名如 'PHP-Project'!"
|
||||
)
|
||||
rules: Optional[str] = Field(
|
||||
default="p/security-audit",
|
||||
description="规则集: p/security-audit, p/owasp-top-ten, p/r2c-security-audit, 或自定义规则文件路径"
|
||||
description="规则集: p/security-audit, p/owasp-top-ten, p/r2c-security-audit"
|
||||
)
|
||||
severity: Optional[str] = Field(
|
||||
default=None,
|
||||
|
|
@ -83,19 +143,20 @@ class SemgrepTool(AgentTool):
|
|||
return """使用 Semgrep 进行静态安全分析。
|
||||
Semgrep 是业界领先的静态分析工具,支持 30+ 种编程语言。
|
||||
|
||||
⚠️ 重要提示:
|
||||
- target_path 使用 '.' 扫描整个项目(推荐)
|
||||
- 或使用子目录如 'src/'、'app/' 等
|
||||
- 不要使用项目目录名(如 'PHP-Project'、'MyApp')!
|
||||
|
||||
可用规则集:
|
||||
- auto: 自动选择最佳规则
|
||||
- p/security-audit: 综合安全审计
|
||||
- p/security-audit: 综合安全审计(推荐)
|
||||
- p/owasp-top-ten: OWASP Top 10 漏洞检测
|
||||
- p/secrets: 密钥泄露检测
|
||||
- p/sql-injection: SQL 注入检测
|
||||
- p/xss: XSS 检测
|
||||
- p/command-injection: 命令注入检测
|
||||
|
||||
使用场景:
|
||||
- 快速全面的代码安全扫描
|
||||
- 检测常见安全漏洞模式
|
||||
- 遵循行业安全标准审计"""
|
||||
- 检测常见安全漏洞模式"""
|
||||
|
||||
@property
|
||||
def args_schema(self):
|
||||
|
|
@ -120,9 +181,12 @@ Semgrep 是业界领先的静态分析工具,支持 30+ 种编程语言。
|
|||
error=error_msg
|
||||
)
|
||||
|
||||
# 构建命令 (相对于 /workspace)
|
||||
# 注意: target_path 是相对于 project_root 的
|
||||
safe_target_path = target_path if not target_path.startswith("/") else target_path.lstrip("/")
|
||||
# 🔥 使用公共函数进行智能路径解析
|
||||
safe_target_path, host_check_path, error_msg = _smart_resolve_target_path(
|
||||
target_path, self.project_root, "Semgrep"
|
||||
)
|
||||
if error_msg:
|
||||
return ToolResult(success=False, data=error_msg, error=error_msg)
|
||||
|
||||
cmd = ["semgrep", "--json", "--quiet"]
|
||||
|
||||
|
|
@ -159,11 +223,16 @@ Semgrep 是业界领先的静态分析工具,支持 30+ 种编程语言。
|
|||
logger.warning(f"[Semgrep] stderr: {result['stderr'][:500]}")
|
||||
|
||||
if not result["success"] and result["exit_code"] != 1: # 1 means findings were found
|
||||
error_msg = result['stderr'][:500] or result['error'] or "未知错误"
|
||||
logger.error(f"[Semgrep] 执行失败: {error_msg}")
|
||||
# 🔥 增强:优先使用 stderr,其次 stdout,最后用 error 字段
|
||||
stdout_preview = result.get('stdout', '')[:500]
|
||||
stderr_preview = result.get('stderr', '')[:500]
|
||||
error_msg = stderr_preview or stdout_preview or result.get('error') or "未知错误"
|
||||
logger.error(f"[Semgrep] 执行失败 (exit_code={result['exit_code']}): {error_msg}")
|
||||
if stdout_preview:
|
||||
logger.error(f"[Semgrep] stdout: {stdout_preview}")
|
||||
return ToolResult(
|
||||
success=False,
|
||||
data=f"Semgrep 执行失败: {error_msg}", # 🔥 修复:设置 data 字段避免 None
|
||||
data=f"Semgrep 执行失败 (exit_code={result['exit_code']}): {error_msg}",
|
||||
error=f"Semgrep 执行失败: {error_msg}",
|
||||
)
|
||||
|
||||
|
|
@ -242,7 +311,10 @@ Semgrep 是业界领先的静态分析工具,支持 30+ 种编程语言。
|
|||
|
||||
class BanditInput(BaseModel):
|
||||
"""Bandit 扫描输入"""
|
||||
target_path: str = Field(default=".", description="要扫描的 Python 目录或文件")
|
||||
target_path: str = Field(
|
||||
default=".",
|
||||
description="要扫描的路径。使用 '.' 扫描整个项目(推荐),不要使用项目目录名!"
|
||||
)
|
||||
severity: str = Field(default="medium", description="最低严重程度: low, medium, high")
|
||||
confidence: str = Field(default="medium", description="最低置信度: low, medium, high")
|
||||
max_results: int = Field(default=50, description="最大返回结果数")
|
||||
|
|
@ -275,16 +347,15 @@ class BanditTool(AgentTool):
|
|||
@property
|
||||
def description(self) -> str:
|
||||
return """使用 Bandit 扫描 Python 代码的安全问题。
|
||||
Bandit 是 Python 专用的安全分析工具,由 OpenStack 安全团队开发。
|
||||
Bandit 是 Python 专用的安全分析工具。
|
||||
|
||||
⚠️ 重要提示: target_path 使用 '.' 扫描整个项目,不要使用项目目录名!
|
||||
|
||||
检测项目:
|
||||
- B101: assert 使用
|
||||
- B102: exec 使用
|
||||
- B103-B108: 文件权限问题
|
||||
- B301-B312: pickle/yaml 反序列化
|
||||
- B501-B508: SSL/TLS 问题
|
||||
- B601-B608: shell/SQL 注入
|
||||
- B701-B703: Jinja2 模板问题
|
||||
- shell/SQL 注入
|
||||
- 硬编码密码
|
||||
- 不安全的反序列化
|
||||
- SSL/TLS 问题
|
||||
|
||||
仅适用于 Python 项目。"""
|
||||
|
||||
|
|
@ -307,7 +378,12 @@ Bandit 是 Python 专用的安全分析工具,由 OpenStack 安全团队开发
|
|||
error_msg = f"Bandit unavailable: {self.sandbox_manager.get_diagnosis()}"
|
||||
return ToolResult(success=False, data=error_msg, error=error_msg)
|
||||
|
||||
safe_target_path = target_path if not target_path.startswith("/") else target_path.lstrip("/")
|
||||
# 🔥 使用公共函数进行智能路径解析
|
||||
safe_target_path, host_check_path, error_msg = _smart_resolve_target_path(
|
||||
target_path, self.project_root, "Bandit"
|
||||
)
|
||||
if error_msg:
|
||||
return ToolResult(success=False, data=error_msg, error=error_msg)
|
||||
|
||||
# 构建命令
|
||||
severity_map = {"low": "l", "medium": "m", "high": "h"}
|
||||
|
|
@ -378,7 +454,10 @@ Bandit 是 Python 专用的安全分析工具,由 OpenStack 安全团队开发
|
|||
|
||||
class GitleaksInput(BaseModel):
|
||||
"""Gitleaks 扫描输入"""
|
||||
target_path: str = Field(default=".", description="要扫描的目录")
|
||||
target_path: str = Field(
|
||||
default=".",
|
||||
description="要扫描的路径。使用 '.' 扫描整个项目(推荐),不要使用项目目录名!"
|
||||
)
|
||||
no_git: bool = Field(default=True, description="不使用 git history,仅扫描文件")
|
||||
max_results: int = Field(default=50, description="最大返回结果数")
|
||||
|
||||
|
|
@ -412,16 +491,14 @@ class GitleaksTool(AgentTool):
|
|||
return """使用 Gitleaks 检测代码中的密钥泄露。
|
||||
Gitleaks 是专业的密钥检测工具,支持 150+ 种密钥类型。
|
||||
|
||||
⚠️ 重要提示: target_path 使用 '.' 扫描整个项目,不要使用项目目录名!
|
||||
|
||||
检测类型:
|
||||
- AWS Access Keys / Secret Keys
|
||||
- GCP API Keys / Service Account Keys
|
||||
- Azure Credentials
|
||||
- GitHub / GitLab Tokens
|
||||
- Private Keys (RSA, SSH, PGP)
|
||||
- Database Connection Strings
|
||||
- AWS/GCP/Azure 凭据
|
||||
- GitHub/GitLab Tokens
|
||||
- 私钥 (RSA, SSH, PGP)
|
||||
- 数据库连接字符串
|
||||
- JWT Secrets
|
||||
- Slack / Discord Tokens
|
||||
- 等等...
|
||||
|
||||
建议在代码审计早期使用此工具。"""
|
||||
|
||||
|
|
@ -443,7 +520,12 @@ Gitleaks 是专业的密钥检测工具,支持 150+ 种密钥类型。
|
|||
error_msg = f"Gitleaks unavailable: {self.sandbox_manager.get_diagnosis()}"
|
||||
return ToolResult(success=False, data=error_msg, error=error_msg)
|
||||
|
||||
safe_target_path = target_path if not target_path.startswith("/") else target_path.lstrip("/")
|
||||
# 🔥 使用公共函数进行智能路径解析
|
||||
safe_target_path, host_check_path, error_msg = _smart_resolve_target_path(
|
||||
target_path, self.project_root, "Gitleaks"
|
||||
)
|
||||
if error_msg:
|
||||
return ToolResult(success=False, data=error_msg, error=error_msg)
|
||||
|
||||
# 🔥 修复:新版 gitleaks 需要使用 --report-path 输出到文件
|
||||
# 使用 /tmp 目录(tmpfs 可写)
|
||||
|
|
@ -813,7 +895,10 @@ class SafetyTool(AgentTool):
|
|||
|
||||
class TruffleHogInput(BaseModel):
|
||||
"""TruffleHog 扫描输入"""
|
||||
target_path: str = Field(default=".", description="要扫描的目录")
|
||||
target_path: str = Field(
|
||||
default=".",
|
||||
description="要扫描的路径。使用 '.' 扫描整个项目(推荐),不要使用项目目录名!"
|
||||
)
|
||||
only_verified: bool = Field(default=False, description="仅显示已验证的密钥")
|
||||
|
||||
|
||||
|
|
@ -839,15 +924,15 @@ class TruffleHogTool(AgentTool):
|
|||
@property
|
||||
def description(self) -> str:
|
||||
return """使用 TruffleHog 进行深度密钥扫描。
|
||||
TruffleHog 可以扫描代码和 Git 历史,并验证密钥是否有效。
|
||||
|
||||
⚠️ 重要提示: target_path 使用 '.' 扫描整个项目,不要使用项目目录名!
|
||||
|
||||
特点:
|
||||
- 支持 700+ 种密钥类型
|
||||
- 可以验证密钥是否仍然有效
|
||||
- 扫描 Git 历史记录
|
||||
- 高精度,低误报
|
||||
|
||||
建议与 Gitleaks 配合使用以获得最佳效果。"""
|
||||
建议与 Gitleaks 配合使用。"""
|
||||
|
||||
@property
|
||||
def args_schema(self):
|
||||
|
|
@ -866,7 +951,12 @@ TruffleHog 可以扫描代码和 Git 历史,并验证密钥是否有效。
|
|||
error_msg = f"TruffleHog unavailable: {self.sandbox_manager.get_diagnosis()}"
|
||||
return ToolResult(success=False, data=error_msg, error=error_msg)
|
||||
|
||||
safe_target_path = target_path if not target_path.startswith("/") else target_path.lstrip("/")
|
||||
# 🔥 使用公共函数进行智能路径解析
|
||||
safe_target_path, host_check_path, error_msg = _smart_resolve_target_path(
|
||||
target_path, self.project_root, "TruffleHog"
|
||||
)
|
||||
if error_msg:
|
||||
return ToolResult(success=False, data=error_msg, error=error_msg)
|
||||
|
||||
cmd = ["trufflehog", "filesystem", safe_target_path, "--json"]
|
||||
if only_verified:
|
||||
|
|
@ -929,7 +1019,10 @@ TruffleHog 可以扫描代码和 Git 历史,并验证密钥是否有效。
|
|||
|
||||
class OSVScannerInput(BaseModel):
|
||||
"""OSV-Scanner 扫描输入"""
|
||||
target_path: str = Field(default=".", description="要扫描的项目目录")
|
||||
target_path: str = Field(
|
||||
default=".",
|
||||
description="要扫描的路径。使用 '.' 扫描整个项目(推荐),不要使用项目目录名!"
|
||||
)
|
||||
|
||||
|
||||
class OSVScannerTool(AgentTool):
|
||||
|
|
@ -954,21 +1047,17 @@ class OSVScannerTool(AgentTool):
|
|||
@property
|
||||
def description(self) -> str:
|
||||
return """使用 OSV-Scanner 扫描开源依赖漏洞。
|
||||
Google 开源的漏洞扫描工具,使用 OSV (Open Source Vulnerabilities) 数据库。
|
||||
Google 开源的漏洞扫描工具。
|
||||
|
||||
⚠️ 重要提示: target_path 使用 '.' 扫描整个项目,不要使用项目目录名!
|
||||
|
||||
支持:
|
||||
- package.json / package-lock.json (npm)
|
||||
- requirements.txt / Pipfile.lock (Python)
|
||||
- go.mod / go.sum (Go)
|
||||
- package.json (npm)
|
||||
- requirements.txt (Python)
|
||||
- go.mod (Go)
|
||||
- Cargo.lock (Rust)
|
||||
- pom.xml (Maven)
|
||||
- Gemfile.lock (Ruby)
|
||||
- composer.lock (PHP)
|
||||
|
||||
特点:
|
||||
- 覆盖多种语言和包管理器
|
||||
- 使用 Google 维护的漏洞数据库
|
||||
- 快速、准确"""
|
||||
- composer.lock (PHP)"""
|
||||
|
||||
@property
|
||||
def args_schema(self):
|
||||
|
|
@ -986,7 +1075,12 @@ Google 开源的漏洞扫描工具,使用 OSV (Open Source Vulnerabilities)
|
|||
error_msg = f"OSV-Scanner unavailable: {self.sandbox_manager.get_diagnosis()}"
|
||||
return ToolResult(success=False, data=error_msg, error=error_msg)
|
||||
|
||||
safe_target_path = target_path if not target_path.startswith("/") else target_path.lstrip("/")
|
||||
# 🔥 使用公共函数进行智能路径解析
|
||||
safe_target_path, host_check_path, error_msg = _smart_resolve_target_path(
|
||||
target_path, self.project_root, "OSV-Scanner"
|
||||
)
|
||||
if error_msg:
|
||||
return ToolResult(success=False, data=error_msg, error=error_msg)
|
||||
|
||||
# OSV-Scanner
|
||||
cmd = ["osv-scanner", "--json", "-r", safe_target_path]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
|
@ -44,20 +45,23 @@ class VulnerabilityReportInput(BaseModel):
|
|||
class CreateVulnerabilityReportTool(AgentTool):
|
||||
"""
|
||||
创建漏洞报告工具
|
||||
|
||||
|
||||
这是正式记录漏洞的唯一方式。只有通过这个工具创建的漏洞才会被计入最终报告。
|
||||
这个设计确保了漏洞报告的规范性和完整性。
|
||||
|
||||
|
||||
通常只有专门的报告Agent或验证Agent才会调用这个工具,
|
||||
确保漏洞在被正式报告之前已经经过了充分的验证。
|
||||
|
||||
🔥 v2.1: 添加文件路径验证,拒绝报告不存在的文件
|
||||
"""
|
||||
|
||||
|
||||
# 存储所有报告的漏洞
|
||||
_vulnerability_reports: List[Dict[str, Any]] = []
|
||||
|
||||
def __init__(self):
|
||||
|
||||
def __init__(self, project_root: Optional[str] = None):
|
||||
super().__init__()
|
||||
self._reports: List[Dict[str, Any]] = []
|
||||
self.project_root = project_root # 🔥 v2.1: 用于文件验证
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
|
@ -125,7 +129,23 @@ class CreateVulnerabilityReportTool(AgentTool):
|
|||
|
||||
if not file_path or not file_path.strip():
|
||||
return ToolResult(success=False, error="文件路径不能为空")
|
||||
|
||||
|
||||
# 🔥 v2.1: 验证文件路径存在性 - 防止幻觉
|
||||
if self.project_root:
|
||||
# 清理路径(移除可能的行号,如 "app.py:36")
|
||||
clean_path = file_path.split(":")[0].strip() if ":" in file_path else file_path.strip()
|
||||
full_path = os.path.join(self.project_root, clean_path)
|
||||
|
||||
if not os.path.isfile(full_path):
|
||||
# 尝试作为绝对路径
|
||||
if not (os.path.isabs(clean_path) and os.path.isfile(clean_path)):
|
||||
logger.warning(f"[ReportTool] 🚫 拒绝报告: 文件不存在 '{file_path}'")
|
||||
return ToolResult(
|
||||
success=False,
|
||||
error=f"无法创建报告:文件 '{file_path}' 在项目中不存在。"
|
||||
f"请先使用 read_file 工具验证文件存在,然后再报告漏洞。"
|
||||
)
|
||||
|
||||
# 验证严重程度
|
||||
valid_severities = ["critical", "high", "medium", "low", "info"]
|
||||
severity = severity.lower()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,513 @@
|
|||
"""
|
||||
通用代码执行工具 - LLM 驱动的漏洞验证
|
||||
|
||||
核心理念:
|
||||
- LLM 是验证的大脑,工具只提供执行能力
|
||||
- 不硬编码 payload、检测规则
|
||||
- LLM 自己决定测试策略、编写测试代码、分析结果
|
||||
|
||||
使用场景:
|
||||
- LLM 编写 Fuzzing Harness 进行局部测试
|
||||
- LLM 构造 PoC 验证漏洞
|
||||
- LLM 编写 mock 代码隔离测试函数
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from typing import Optional, Dict, Any
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .base import AgentTool, ToolResult
|
||||
from .sandbox_tool import SandboxManager, SandboxConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RunCodeInput(BaseModel):
|
||||
"""代码执行输入"""
|
||||
code: str = Field(..., description="要执行的代码")
|
||||
language: str = Field(default="python", description="编程语言: python, php, javascript, ruby, go, java, bash")
|
||||
timeout: int = Field(default=60, description="超时时间(秒),复杂测试可设置更长")
|
||||
description: str = Field(default="", description="简短描述这段代码的目的(用于日志)")
|
||||
|
||||
|
||||
class RunCodeTool(AgentTool):
|
||||
"""
|
||||
通用代码执行工具
|
||||
|
||||
让 LLM 自由编写测试代码,在沙箱中执行。
|
||||
|
||||
LLM 可以:
|
||||
- 编写 Fuzzing Harness 隔离测试单个函数
|
||||
- 构造 mock 对象模拟依赖
|
||||
- 设计各种 payload 进行测试
|
||||
- 分析执行结果判断漏洞
|
||||
|
||||
工具不做任何假设,完全由 LLM 控制测试逻辑。
|
||||
"""
|
||||
|
||||
def __init__(self, sandbox_manager: Optional[SandboxManager] = None, project_root: str = "."):
|
||||
super().__init__()
|
||||
# 使用更宽松的沙箱配置
|
||||
config = SandboxConfig(
|
||||
timeout=120,
|
||||
memory_limit="1g", # 更大内存
|
||||
)
|
||||
self.sandbox_manager = sandbox_manager or SandboxManager(config)
|
||||
self.project_root = project_root
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "run_code"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return """🔥 通用代码执行工具 - 在沙箱中运行你编写的测试代码
|
||||
|
||||
这是你进行漏洞验证的核心工具。你可以:
|
||||
1. 编写 Fuzzing Harness 隔离测试单个函数
|
||||
2. 构造 mock 对象模拟数据库、HTTP 请求等依赖
|
||||
3. 设计各种 payload 进行漏洞测试
|
||||
4. 编写完整的 PoC 验证脚本
|
||||
|
||||
输入:
|
||||
- code: 你编写的测试代码(完整可执行)
|
||||
- language: python, php, javascript, ruby, go, java, bash
|
||||
- timeout: 超时秒数(默认60,复杂测试可设更长)
|
||||
- description: 简短描述代码目的
|
||||
|
||||
支持的语言和执行方式:
|
||||
- python: python3 -c 'code'
|
||||
- php: php -r 'code' (注意:不需要 <?php 标签)
|
||||
- javascript: node -e 'code'
|
||||
- ruby: ruby -e 'code'
|
||||
- go: go run (需写完整 package main)
|
||||
- java: javac + java (需写完整 class)
|
||||
- bash: bash -c 'code'
|
||||
|
||||
示例 - 命令注入 Fuzzing Harness:
|
||||
```python
|
||||
# 提取目标函数并构造测试
|
||||
import os
|
||||
|
||||
# Mock os.system 来检测是否被调用
|
||||
executed_commands = []
|
||||
original_system = os.system
|
||||
def mock_system(cmd):
|
||||
print(f"[DETECTED] os.system called: {cmd}")
|
||||
executed_commands.append(cmd)
|
||||
return 0
|
||||
os.system = mock_system
|
||||
|
||||
# 目标函数(从项目代码复制)
|
||||
def vulnerable_function(user_input):
|
||||
os.system(f"echo {user_input}")
|
||||
|
||||
# Fuzzing 测试
|
||||
payloads = ["; id", "| whoami", "$(cat /etc/passwd)", "`id`"]
|
||||
for payload in payloads:
|
||||
print(f"\\nTesting payload: {payload}")
|
||||
executed_commands.clear()
|
||||
try:
|
||||
vulnerable_function(payload)
|
||||
if executed_commands:
|
||||
print(f"[VULN] Command injection detected!")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
```
|
||||
|
||||
⚠️ 重要提示:
|
||||
- 代码在 Docker 沙箱中执行,与真实环境隔离
|
||||
- 你需要自己 mock 依赖(数据库、HTTP、文件系统等)
|
||||
- 你需要自己设计 payload 和检测逻辑
|
||||
- 你需要自己分析输出判断漏洞是否存在"""
|
||||
|
||||
@property
|
||||
def args_schema(self):
|
||||
return RunCodeInput
|
||||
|
||||
async def _execute(
|
||||
self,
|
||||
code: str,
|
||||
language: str = "python",
|
||||
timeout: int = 60,
|
||||
description: str = "",
|
||||
**kwargs
|
||||
) -> ToolResult:
|
||||
"""执行用户编写的代码"""
|
||||
|
||||
# 初始化沙箱
|
||||
try:
|
||||
await self.sandbox_manager.initialize()
|
||||
except Exception as e:
|
||||
logger.warning(f"Sandbox init failed: {e}")
|
||||
|
||||
if not self.sandbox_manager.is_available:
|
||||
return ToolResult(
|
||||
success=False,
|
||||
error="沙箱环境不可用 (Docker 未运行)",
|
||||
data="请确保 Docker 已启动。如果无法使用沙箱,你可以通过静态分析代码来验证漏洞。"
|
||||
)
|
||||
|
||||
# 构建执行命令
|
||||
language = language.lower().strip()
|
||||
command = self._build_command(code, language)
|
||||
|
||||
if command is None:
|
||||
return ToolResult(
|
||||
success=False,
|
||||
error=f"不支持的语言: {language}",
|
||||
data=f"支持的语言: python, php, javascript, ruby, go, java, bash"
|
||||
)
|
||||
|
||||
# 在沙箱中执行
|
||||
result = await self.sandbox_manager.execute_command(
|
||||
command=command,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
# 格式化输出
|
||||
output_parts = [f"🔬 代码执行结果"]
|
||||
if description:
|
||||
output_parts.append(f"目的: {description}")
|
||||
output_parts.append(f"语言: {language}")
|
||||
output_parts.append(f"退出码: {result['exit_code']}")
|
||||
|
||||
if result.get("stdout"):
|
||||
stdout = result["stdout"]
|
||||
if len(stdout) > 5000:
|
||||
stdout = stdout[:5000] + f"\n... (截断,共 {len(result['stdout'])} 字符)"
|
||||
output_parts.append(f"\n输出:\n```\n{stdout}\n```")
|
||||
|
||||
if result.get("stderr"):
|
||||
stderr = result["stderr"]
|
||||
if len(stderr) > 2000:
|
||||
stderr = stderr[:2000] + "\n... (截断)"
|
||||
output_parts.append(f"\n错误输出:\n```\n{stderr}\n```")
|
||||
|
||||
if result.get("error"):
|
||||
output_parts.append(f"\n执行错误: {result['error']}")
|
||||
|
||||
# 提示 LLM 分析结果
|
||||
output_parts.append("\n---")
|
||||
output_parts.append("请根据上述输出分析漏洞是否存在。")
|
||||
|
||||
return ToolResult(
|
||||
success=result.get("success", False),
|
||||
data="\n".join(output_parts),
|
||||
error=result.get("error"),
|
||||
metadata={
|
||||
"language": language,
|
||||
"exit_code": result.get("exit_code", -1),
|
||||
"stdout_length": len(result.get("stdout", "")),
|
||||
"stderr_length": len(result.get("stderr", "")),
|
||||
}
|
||||
)
|
||||
|
||||
def _build_command(self, code: str, language: str) -> Optional[str]:
|
||||
"""根据语言构建执行命令"""
|
||||
|
||||
# 转义单引号的通用方法
|
||||
def escape_for_shell(s: str) -> str:
|
||||
return s.replace("'", "'\"'\"'")
|
||||
|
||||
if language == "python":
|
||||
escaped = escape_for_shell(code)
|
||||
return f"python3 -c '{escaped}'"
|
||||
|
||||
elif language == "php":
|
||||
# PHP: php -r 不需要 <?php 标签
|
||||
clean_code = code.strip()
|
||||
if clean_code.startswith("<?php"):
|
||||
clean_code = clean_code[5:].strip()
|
||||
if clean_code.startswith("<?"):
|
||||
clean_code = clean_code[2:].strip()
|
||||
if clean_code.endswith("?>"):
|
||||
clean_code = clean_code[:-2].strip()
|
||||
escaped = escape_for_shell(clean_code)
|
||||
return f"php -r '{escaped}'"
|
||||
|
||||
elif language in ["javascript", "js", "node"]:
|
||||
escaped = escape_for_shell(code)
|
||||
return f"node -e '{escaped}'"
|
||||
|
||||
elif language == "ruby":
|
||||
escaped = escape_for_shell(code)
|
||||
return f"ruby -e '{escaped}'"
|
||||
|
||||
elif language == "bash":
|
||||
escaped = escape_for_shell(code)
|
||||
return f"bash -c '{escaped}'"
|
||||
|
||||
elif language == "go":
|
||||
# Go 需要完整的 package main
|
||||
escaped = escape_for_shell(code).replace("\\", "\\\\")
|
||||
return f"echo '{escaped}' > /tmp/main.go && go run /tmp/main.go"
|
||||
|
||||
elif language == "java":
|
||||
# Java 需要完整的 class
|
||||
escaped = escape_for_shell(code).replace("\\", "\\\\")
|
||||
# 提取类名
|
||||
import re
|
||||
class_match = re.search(r'public\s+class\s+(\w+)', code)
|
||||
class_name = class_match.group(1) if class_match else "Test"
|
||||
return f"echo '{escaped}' > /tmp/{class_name}.java && javac /tmp/{class_name}.java && java -cp /tmp {class_name}"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class ExtractFunctionInput(BaseModel):
|
||||
"""函数提取输入"""
|
||||
file_path: str = Field(..., description="源文件路径")
|
||||
function_name: str = Field(..., description="要提取的函数名")
|
||||
include_imports: bool = Field(default=True, description="是否包含 import 语句")
|
||||
|
||||
|
||||
class ExtractFunctionTool(AgentTool):
|
||||
"""
|
||||
函数提取工具
|
||||
|
||||
从源文件中提取指定函数及其依赖,用于构建 Fuzzing Harness
|
||||
"""
|
||||
|
||||
def __init__(self, project_root: str = "."):
|
||||
super().__init__()
|
||||
self.project_root = project_root
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "extract_function"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return """从源文件中提取指定函数的代码
|
||||
|
||||
用于构建 Fuzzing Harness 时获取目标函数代码。
|
||||
|
||||
输入:
|
||||
- file_path: 源文件路径
|
||||
- function_name: 要提取的函数名
|
||||
- include_imports: 是否包含文件开头的 import 语句(默认 true)
|
||||
|
||||
返回:
|
||||
- 函数代码
|
||||
- 相关的 import 语句
|
||||
- 函数参数列表
|
||||
|
||||
示例:
|
||||
{"file_path": "app/api.py", "function_name": "process_command"}"""
|
||||
|
||||
@property
|
||||
def args_schema(self):
|
||||
return ExtractFunctionInput
|
||||
|
||||
async def _execute(
|
||||
self,
|
||||
file_path: str,
|
||||
function_name: str,
|
||||
include_imports: bool = True,
|
||||
**kwargs
|
||||
) -> ToolResult:
|
||||
"""提取函数代码"""
|
||||
import ast
|
||||
import re
|
||||
|
||||
full_path = os.path.join(self.project_root, file_path)
|
||||
if not os.path.exists(full_path):
|
||||
return ToolResult(success=False, error=f"文件不存在: {file_path}")
|
||||
|
||||
with open(full_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
code = f.read()
|
||||
|
||||
# 检测语言
|
||||
ext = os.path.splitext(file_path)[1].lower()
|
||||
|
||||
if ext == ".py":
|
||||
result = self._extract_python(code, function_name, include_imports)
|
||||
elif ext == ".php":
|
||||
result = self._extract_php(code, function_name)
|
||||
elif ext in [".js", ".ts"]:
|
||||
result = self._extract_javascript(code, function_name)
|
||||
else:
|
||||
result = self._extract_generic(code, function_name)
|
||||
|
||||
if result["success"]:
|
||||
output_parts = [f"📦 函数提取结果\n"]
|
||||
output_parts.append(f"文件: {file_path}")
|
||||
output_parts.append(f"函数: {function_name}")
|
||||
|
||||
if result.get("imports"):
|
||||
output_parts.append(f"\n相关 imports:\n```\n{result['imports']}\n```")
|
||||
|
||||
if result.get("parameters"):
|
||||
output_parts.append(f"\n参数: {', '.join(result['parameters'])}")
|
||||
|
||||
output_parts.append(f"\n函数代码:\n```\n{result['code']}\n```")
|
||||
|
||||
output_parts.append("\n---")
|
||||
output_parts.append("你现在可以使用这段代码构建 Fuzzing Harness")
|
||||
|
||||
return ToolResult(
|
||||
success=True,
|
||||
data="\n".join(output_parts),
|
||||
metadata=result
|
||||
)
|
||||
else:
|
||||
return ToolResult(
|
||||
success=False,
|
||||
error=result.get("error", "提取失败"),
|
||||
data=f"无法提取函数 '{function_name}'。你可以使用 read_file 工具直接读取文件,手动定位函数代码。"
|
||||
)
|
||||
|
||||
def _extract_python(self, code: str, function_name: str, include_imports: bool) -> Dict:
|
||||
"""提取 Python 函数"""
|
||||
import ast
|
||||
|
||||
try:
|
||||
tree = ast.parse(code)
|
||||
except SyntaxError:
|
||||
# 降级到正则提取
|
||||
return self._extract_generic(code, function_name)
|
||||
|
||||
# 收集 imports
|
||||
imports = []
|
||||
if include_imports:
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
imports.append(ast.unparse(node))
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
imports.append(ast.unparse(node))
|
||||
|
||||
# 查找函数
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
if node.name == function_name:
|
||||
lines = code.split('\n')
|
||||
func_code = '\n'.join(lines[node.lineno - 1:node.end_lineno])
|
||||
params = [arg.arg for arg in node.args.args]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"code": func_code,
|
||||
"imports": '\n'.join(imports) if imports else None,
|
||||
"parameters": params,
|
||||
"line_start": node.lineno,
|
||||
"line_end": node.end_lineno,
|
||||
}
|
||||
|
||||
return {"success": False, "error": f"未找到函数 '{function_name}'"}
|
||||
|
||||
def _extract_php(self, code: str, function_name: str) -> Dict:
|
||||
"""提取 PHP 函数"""
|
||||
import re
|
||||
|
||||
pattern = rf'function\s+{re.escape(function_name)}\s*\([^)]*\)\s*\{{'
|
||||
match = re.search(pattern, code)
|
||||
|
||||
if not match:
|
||||
return {"success": False, "error": f"未找到函数 '{function_name}'"}
|
||||
|
||||
start_pos = match.start()
|
||||
brace_count = 0
|
||||
end_pos = match.end() - 1
|
||||
|
||||
for i, char in enumerate(code[match.end() - 1:], start=match.end() - 1):
|
||||
if char == '{':
|
||||
brace_count += 1
|
||||
elif char == '}':
|
||||
brace_count -= 1
|
||||
if brace_count == 0:
|
||||
end_pos = i + 1
|
||||
break
|
||||
|
||||
func_code = code[start_pos:end_pos]
|
||||
|
||||
# 提取参数
|
||||
param_match = re.search(r'function\s+\w+\s*\(([^)]*)\)', func_code)
|
||||
params = []
|
||||
if param_match:
|
||||
params_str = param_match.group(1)
|
||||
params = [p.strip().split('=')[0].strip().replace('$', '')
|
||||
for p in params_str.split(',') if p.strip()]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"code": func_code,
|
||||
"parameters": params,
|
||||
}
|
||||
|
||||
def _extract_javascript(self, code: str, function_name: str) -> Dict:
|
||||
"""提取 JavaScript 函数"""
|
||||
import re
|
||||
|
||||
patterns = [
|
||||
rf'function\s+{re.escape(function_name)}\s*\([^)]*\)\s*\{{',
|
||||
rf'(?:const|let|var)\s+{re.escape(function_name)}\s*=\s*function\s*\([^)]*\)\s*\{{',
|
||||
rf'(?:const|let|var)\s+{re.escape(function_name)}\s*=\s*\([^)]*\)\s*=>\s*\{{',
|
||||
rf'async\s+function\s+{re.escape(function_name)}\s*\([^)]*\)\s*\{{',
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, code)
|
||||
if match:
|
||||
start_pos = match.start()
|
||||
brace_count = 0
|
||||
end_pos = match.end() - 1
|
||||
|
||||
for i, char in enumerate(code[match.end() - 1:], start=match.end() - 1):
|
||||
if char == '{':
|
||||
brace_count += 1
|
||||
elif char == '}':
|
||||
brace_count -= 1
|
||||
if brace_count == 0:
|
||||
end_pos = i + 1
|
||||
break
|
||||
|
||||
func_code = code[start_pos:end_pos]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"code": func_code,
|
||||
}
|
||||
|
||||
return {"success": False, "error": f"未找到函数 '{function_name}'"}
|
||||
|
||||
def _extract_generic(self, code: str, function_name: str) -> Dict:
|
||||
"""通用函数提取(正则)"""
|
||||
import re
|
||||
|
||||
# 尝试多种模式
|
||||
patterns = [
|
||||
rf'def\s+{re.escape(function_name)}\s*\([^)]*\)\s*:', # Python
|
||||
rf'function\s+{re.escape(function_name)}\s*\([^)]*\)', # PHP/JS
|
||||
rf'func\s+{re.escape(function_name)}\s*\([^)]*\)', # Go
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, code, re.MULTILINE)
|
||||
if match:
|
||||
start_line = code[:match.start()].count('\n')
|
||||
lines = code.split('\n')
|
||||
|
||||
# 尝试找到函数结束
|
||||
end_line = start_line + 1
|
||||
indent = len(lines[start_line]) - len(lines[start_line].lstrip())
|
||||
|
||||
for i in range(start_line + 1, min(start_line + 100, len(lines))):
|
||||
line = lines[i]
|
||||
if line.strip() and not line.startswith(' ' * (indent + 1)):
|
||||
if not line.strip().startswith('#'):
|
||||
end_line = i
|
||||
break
|
||||
end_line = i + 1
|
||||
|
||||
func_code = '\n'.join(lines[start_line:end_line])
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"code": func_code,
|
||||
}
|
||||
|
||||
return {"success": False, "error": f"未找到函数 '{function_name}'"}
|
||||
|
|
@ -767,7 +767,9 @@ class GoTestTool(BaseLanguageTestTool):
|
|||
param_code = ""
|
||||
if params:
|
||||
args = ["program"] + list(params.values())
|
||||
param_code = f" os.Args = []string{{{', '.join([f'\"{a}\"' for a in args])}}}\n"
|
||||
args_str = ', '.join([f'"{a}"' for a in args])
|
||||
param_code = " os.Args = []string{{{}}}\n".format(args_str)
|
||||
# param_code = f" os.Args = []string{{{', '.join([f'\"{a}\"' for a in args])}}}\n"
|
||||
for key, value in params.items():
|
||||
param_code += f' os.Setenv("{key.upper()}", "{value}")\n'
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from pydantic import BaseModel, Field
|
|||
from dataclasses import dataclass
|
||||
|
||||
from .base import AgentTool, ToolResult
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ logger = logging.getLogger(__name__)
|
|||
@dataclass
|
||||
class SandboxConfig:
|
||||
"""沙箱配置"""
|
||||
image: str = "deepaudit/sandbox:latest"
|
||||
image: str = None # 默认从 settings.SANDBOX_IMAGE 读取
|
||||
memory_limit: str = "512m"
|
||||
cpu_limit: float = 1.0
|
||||
timeout: int = 60
|
||||
|
|
@ -29,6 +30,10 @@ class SandboxConfig:
|
|||
read_only: bool = True
|
||||
user: str = "1000:1000"
|
||||
|
||||
def __post_init__(self):
|
||||
if self.image is None:
|
||||
self.image = settings.SANDBOX_IMAGE
|
||||
|
||||
|
||||
class SandboxManager:
|
||||
"""
|
||||
|
|
@ -108,7 +113,19 @@ class SandboxManager:
|
|||
}
|
||||
|
||||
timeout = timeout or self.config.timeout
|
||||
|
||||
|
||||
# 禁用代理环境变量,防止 Docker 自动注入的代理干扰容器网络
|
||||
no_proxy_env = {
|
||||
"HTTP_PROXY": "",
|
||||
"HTTPS_PROXY": "",
|
||||
"http_proxy": "",
|
||||
"https_proxy": "",
|
||||
"NO_PROXY": "*",
|
||||
"no_proxy": "*",
|
||||
}
|
||||
# 合并用户传入的环境变量(用户变量优先)
|
||||
container_env = {**no_proxy_env, **(env or {})}
|
||||
|
||||
try:
|
||||
# 创建临时目录
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
|
@ -131,7 +148,7 @@ class SandboxManager:
|
|||
"/tmp": "rw,size=100m,mode=1777"
|
||||
},
|
||||
"working_dir": working_dir or "/workspace",
|
||||
"environment": env or {},
|
||||
"environment": container_env,
|
||||
# 安全配置
|
||||
"cap_drop": ["ALL"],
|
||||
"security_opt": ["no-new-privileges:true"],
|
||||
|
|
@ -222,14 +239,22 @@ class SandboxManager:
|
|||
|
||||
timeout = timeout or self.config.timeout
|
||||
|
||||
try:
|
||||
# 🔥 清除代理环境变量的方式:在命令前添加 unset
|
||||
# 因为设置空字符串会导致工具尝试解析空 URI 而出错
|
||||
unset_proxy_prefix = "unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy; "
|
||||
wrapped_command = unset_proxy_prefix + command
|
||||
# 禁用代理环境变量,防止 Docker 自动注入的代理干扰容器网络
|
||||
no_proxy_env = {
|
||||
"HTTP_PROXY": "",
|
||||
"HTTPS_PROXY": "",
|
||||
"http_proxy": "",
|
||||
"https_proxy": "",
|
||||
"NO_PROXY": "*",
|
||||
"no_proxy": "*",
|
||||
}
|
||||
# 合并用户传入的环境变量(用户变量优先)
|
||||
container_env = {**no_proxy_env, **(env or {})}
|
||||
|
||||
# 用户传入的环境变量
|
||||
container_env = env or {}
|
||||
try:
|
||||
# 清除代理环境变量:在命令前添加 unset(双重保险)
|
||||
unset_proxy_prefix = "unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy ALL_PROXY all_proxy 2>/dev/null; "
|
||||
wrapped_command = unset_proxy_prefix + command
|
||||
|
||||
# 准备容器配置
|
||||
container_config = {
|
||||
|
|
@ -247,10 +272,10 @@ class SandboxManager:
|
|||
},
|
||||
"tmpfs": {
|
||||
"/home/sandbox": "rw,size=100m,mode=1777",
|
||||
"/tmp": "rw,size=100m,mode=1777" # 🔥 添加 /tmp 目录供工具写入临时文件
|
||||
"/tmp": "rw,size=100m,mode=1777" # 添加 /tmp 目录供工具写入临时文件
|
||||
},
|
||||
"working_dir": "/workspace",
|
||||
"environment": container_env, # 🔥 用户传入的环境变量
|
||||
"environment": container_env,
|
||||
"cap_drop": ["ALL"],
|
||||
"security_opt": ["no-new-privileges:true"],
|
||||
}
|
||||
|
|
@ -489,12 +514,24 @@ class SandboxTool(AgentTool):
|
|||
在安全隔离的环境中执行代码和命令
|
||||
"""
|
||||
|
||||
# 允许的命令前缀
|
||||
# 允许的命令前缀 - 放宽限制以支持更灵活的测试
|
||||
ALLOWED_COMMANDS = [
|
||||
"python", "python3", "node", "curl", "wget",
|
||||
"cat", "head", "tail", "grep", "find", "ls",
|
||||
"echo", "printf", "test", "id", "whoami",
|
||||
"php", # 🔥 添加 PHP 支持
|
||||
# 编程语言解释器
|
||||
"python", "python3", "node", "php", "ruby", "perl",
|
||||
"go", "java", "javac", "bash", "sh",
|
||||
# 网络工具
|
||||
"curl", "wget", "nc", "netcat",
|
||||
# 文件操作
|
||||
"cat", "head", "tail", "grep", "find", "ls", "wc",
|
||||
"sed", "awk", "cut", "sort", "uniq", "tr", "xargs",
|
||||
# 系统信息(用于验证命令执行)
|
||||
"echo", "printf", "test", "id", "whoami", "uname",
|
||||
"env", "printenv", "pwd", "hostname",
|
||||
# 编码/解码工具
|
||||
"base64", "xxd", "od", "hexdump",
|
||||
# 其他实用工具
|
||||
"timeout", "time", "sleep", "true", "false",
|
||||
"md5sum", "sha256sum", "strings",
|
||||
]
|
||||
|
||||
def __init__(self, sandbox_manager: Optional[SandboxManager] = None):
|
||||
|
|
|
|||
|
|
@ -75,7 +75,8 @@ class BaiduAdapter(BaseLLMAdapter):
|
|||
await self.validate_config()
|
||||
return await self.retry(lambda: self._send_request(request))
|
||||
except Exception as error:
|
||||
self.handle_error(error, "百度文心一言 API调用失败")
|
||||
api_response = getattr(error, 'api_response', None)
|
||||
self.handle_error(error, "百度文心一言 API调用失败", api_response=api_response)
|
||||
|
||||
async def _send_request(self, request: LLMRequest) -> LLMResponse:
|
||||
"""发送请求"""
|
||||
|
|
@ -107,12 +108,19 @@ class BaiduAdapter(BaseLLMAdapter):
|
|||
if response.status_code != 200:
|
||||
error_data = response.json() if response.text else {}
|
||||
error_msg = error_data.get("error_msg", f"HTTP {response.status_code}")
|
||||
raise Exception(f"{error_msg}")
|
||||
|
||||
error_code = error_data.get("error_code", "")
|
||||
api_response = f"[{error_code}] {error_msg}" if error_code else error_msg
|
||||
err = LLMError(error_msg, self.config.provider, response.status_code, api_response=api_response)
|
||||
raise err
|
||||
|
||||
data = response.json()
|
||||
|
||||
|
||||
if "error_code" in data:
|
||||
raise Exception(f"百度API错误: {data.get('error_msg', '未知错误')}")
|
||||
error_msg = data.get('error_msg', '未知错误')
|
||||
error_code = data.get('error_code', '')
|
||||
api_response = f"[{error_code}] {error_msg}"
|
||||
err = LLMError(f"百度API错误: {error_msg}", self.config.provider, api_response=api_response)
|
||||
raise err
|
||||
|
||||
usage = None
|
||||
if "usage" in data:
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ class DoubaoAdapter(BaseLLMAdapter):
|
|||
await self.validate_config()
|
||||
return await self.retry(lambda: self._send_request(request))
|
||||
except Exception as error:
|
||||
self.handle_error(error, "豆包 API调用失败")
|
||||
api_response = getattr(error, 'api_response', None)
|
||||
self.handle_error(error, "豆包 API调用失败", api_response=api_response)
|
||||
|
||||
async def _send_request(self, request: LLMRequest) -> LLMResponse:
|
||||
"""发送请求"""
|
||||
|
|
@ -50,8 +51,12 @@ class DoubaoAdapter(BaseLLMAdapter):
|
|||
|
||||
if response.status_code != 200:
|
||||
error_data = response.json() if response.text else {}
|
||||
error_msg = error_data.get("error", {}).get("message", f"HTTP {response.status_code}")
|
||||
raise Exception(f"{error_msg}")
|
||||
error_obj = error_data.get("error", {})
|
||||
error_msg = error_obj.get("message", f"HTTP {response.status_code}")
|
||||
error_code = error_obj.get("code", "")
|
||||
api_response = f"[{error_code}] {error_msg}" if error_code else error_msg
|
||||
err = LLMError(error_msg, self.config.provider, response.status_code, api_response=api_response)
|
||||
raise err
|
||||
|
||||
data = response.json()
|
||||
choice = data.get("choices", [{}])[0]
|
||||
|
|
|
|||
|
|
@ -109,6 +109,45 @@ class LiteLLMAdapter(BaseLLMAdapter):
|
|||
|
||||
return f"{prefix}/{model}"
|
||||
|
||||
def _extract_api_response(self, error: Exception) -> Optional[str]:
|
||||
"""从异常中提取 API 服务器返回的原始响应信息"""
|
||||
error_str = str(error)
|
||||
|
||||
# 尝试提取 JSON 格式的错误信息
|
||||
import re
|
||||
import json
|
||||
|
||||
# 匹配 {'error': {...}} 或 {"error": {...}} 格式
|
||||
json_pattern = r"\{['\"]error['\"]:\s*\{[^}]+\}\}"
|
||||
match = re.search(json_pattern, error_str)
|
||||
if match:
|
||||
try:
|
||||
# 将单引号替换为双引号以便 JSON 解析
|
||||
json_str = match.group().replace("'", '"')
|
||||
error_obj = json.loads(json_str)
|
||||
if 'error' in error_obj:
|
||||
err = error_obj['error']
|
||||
code = err.get('code', '')
|
||||
message = err.get('message', '')
|
||||
return f"[{code}] {message}" if code else message
|
||||
except:
|
||||
pass
|
||||
|
||||
# 尝试提取 message 字段
|
||||
message_pattern = r"['\"]message['\"]:\s*['\"]([^'\"]+)['\"]"
|
||||
match = re.search(message_pattern, error_str)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
# 尝试从 litellm 异常中获取原始消息
|
||||
if hasattr(error, 'message'):
|
||||
return error.message
|
||||
if hasattr(error, 'llm_provider'):
|
||||
# litellm 异常通常包含原始错误信息
|
||||
return error_str.split(' - ')[-1] if ' - ' in error_str else None
|
||||
|
||||
return None
|
||||
|
||||
def _get_api_base(self) -> Optional[str]:
|
||||
"""获取 API 基础 URL"""
|
||||
# 优先使用用户配置的 base_url
|
||||
|
|
@ -200,20 +239,31 @@ class LiteLLMAdapter(BaseLLMAdapter):
|
|||
# 调用 LiteLLM
|
||||
response = await litellm.acompletion(**kwargs)
|
||||
except litellm.exceptions.AuthenticationError as e:
|
||||
raise LLMError(f"API Key 无效或已过期: {str(e)}", self.config.provider, 401)
|
||||
api_response = self._extract_api_response(e)
|
||||
raise LLMError(f"API Key 无效或已过期", self.config.provider, 401, api_response=api_response)
|
||||
except litellm.exceptions.RateLimitError as e:
|
||||
raise LLMError(f"API 调用频率超限: {str(e)}", self.config.provider, 429)
|
||||
error_msg = str(e)
|
||||
api_response = self._extract_api_response(e)
|
||||
# 区分"余额不足"和"频率超限"
|
||||
if any(keyword in error_msg for keyword in ["余额不足", "资源包", "充值", "quota", "insufficient", "balance"]):
|
||||
raise LLMError(f"账户余额不足或配额已用尽,请充值后重试", self.config.provider, 402, api_response=api_response)
|
||||
raise LLMError(f"API 调用频率超限,请稍后重试", self.config.provider, 429, api_response=api_response)
|
||||
except litellm.exceptions.APIConnectionError as e:
|
||||
raise LLMError(f"无法连接到 API 服务: {str(e)}", self.config.provider)
|
||||
api_response = self._extract_api_response(e)
|
||||
raise LLMError(f"无法连接到 API 服务", self.config.provider, api_response=api_response)
|
||||
except litellm.exceptions.APIError as e:
|
||||
raise LLMError(f"API 错误: {str(e)}", self.config.provider, getattr(e, 'status_code', None))
|
||||
api_response = self._extract_api_response(e)
|
||||
raise LLMError(f"API 错误", self.config.provider, getattr(e, 'status_code', None), api_response=api_response)
|
||||
except Exception as e:
|
||||
# 捕获其他异常并重新抛出
|
||||
error_msg = str(e)
|
||||
api_response = self._extract_api_response(e)
|
||||
if "invalid_api_key" in error_msg.lower() or "incorrect api key" in error_msg.lower():
|
||||
raise LLMError(f"API Key 无效: {error_msg}", self.config.provider, 401)
|
||||
raise LLMError(f"API Key 无效", self.config.provider, 401, api_response=api_response)
|
||||
elif "authentication" in error_msg.lower():
|
||||
raise LLMError(f"认证失败: {error_msg}", self.config.provider, 401)
|
||||
raise LLMError(f"认证失败", self.config.provider, 401, api_response=api_response)
|
||||
elif any(keyword in error_msg for keyword in ["余额不足", "资源包", "充值", "quota", "insufficient", "balance"]):
|
||||
raise LLMError(f"账户余额不足或配额已用尽", self.config.provider, 402, api_response=api_response)
|
||||
raise
|
||||
|
||||
# 解析响应
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ class MinimaxAdapter(BaseLLMAdapter):
|
|||
await self.validate_config()
|
||||
return await self.retry(lambda: self._send_request(request))
|
||||
except Exception as error:
|
||||
self.handle_error(error, "MiniMax API调用失败")
|
||||
api_response = getattr(error, 'api_response', None)
|
||||
self.handle_error(error, "MiniMax API调用失败", api_response=api_response)
|
||||
|
||||
async def _send_request(self, request: LLMRequest) -> LLMResponse:
|
||||
"""发送请求"""
|
||||
|
|
@ -47,15 +48,23 @@ class MinimaxAdapter(BaseLLMAdapter):
|
|||
|
||||
if response.status_code != 200:
|
||||
error_data = response.json() if response.text else {}
|
||||
error_msg = error_data.get("base_resp", {}).get("status_msg", f"HTTP {response.status_code}")
|
||||
raise Exception(f"{error_msg}")
|
||||
|
||||
base_resp = error_data.get("base_resp", {})
|
||||
error_msg = base_resp.get("status_msg", f"HTTP {response.status_code}")
|
||||
error_code = base_resp.get("status_code", "")
|
||||
api_response = f"[{error_code}] {error_msg}" if error_code else error_msg
|
||||
err = LLMError(error_msg, self.config.provider, response.status_code, api_response=api_response)
|
||||
raise err
|
||||
|
||||
data = response.json()
|
||||
|
||||
|
||||
# MiniMax 特殊的错误处理
|
||||
if data.get("base_resp", {}).get("status_code") != 0:
|
||||
error_msg = data.get("base_resp", {}).get("status_msg", "未知错误")
|
||||
raise Exception(f"MiniMax API错误: {error_msg}")
|
||||
base_resp = data.get("base_resp", {})
|
||||
if base_resp.get("status_code") != 0:
|
||||
error_msg = base_resp.get("status_msg", "未知错误")
|
||||
error_code = base_resp.get("status_code", "")
|
||||
api_response = f"[{error_code}] {error_msg}"
|
||||
err = LLMError(f"MiniMax API错误: {error_msg}", self.config.provider, api_response=api_response)
|
||||
raise err
|
||||
|
||||
choice = data.get("choices", [{}])[0]
|
||||
|
||||
|
|
|
|||
|
|
@ -57,17 +57,30 @@ class BaseLLMAdapter(ABC):
|
|||
self.config.provider
|
||||
)
|
||||
|
||||
def handle_error(self, error: Any, context: str = "") -> None:
|
||||
"""处理API错误"""
|
||||
def handle_error(self, error: Any, context: str = "", api_response: str = None) -> None:
|
||||
"""处理API错误
|
||||
|
||||
Args:
|
||||
error: 原始异常
|
||||
context: 错误上下文描述
|
||||
api_response: API 服务器返回的原始响应信息
|
||||
"""
|
||||
message = str(error)
|
||||
status_code = getattr(error, 'status_code', None)
|
||||
|
||||
|
||||
# 如果错误本身已经有 api_response,优先使用
|
||||
if api_response is None:
|
||||
api_response = getattr(error, 'api_response', None)
|
||||
|
||||
# 针对不同错误类型提供更详细的信息
|
||||
if "超时" in message or "timeout" in message.lower():
|
||||
message = f"请求超时 ({self.config.timeout}s)。建议:\n" \
|
||||
f"1. 检查网络连接是否正常\n" \
|
||||
f"2. 尝试增加超时时间\n" \
|
||||
f"3. 验证API端点是否正确"
|
||||
elif any(keyword in message for keyword in ["余额不足", "资源包", "充值", "quota", "insufficient", "balance"]):
|
||||
message = f"账户余额不足或配额已用尽,请充值后重试"
|
||||
status_code = status_code or 402
|
||||
elif status_code == 401 or status_code == 403:
|
||||
message = f"API认证失败。建议:\n" \
|
||||
f"1. 检查API Key是否正确配置\n" \
|
||||
|
|
@ -83,14 +96,15 @@ class BaseLLMAdapter(ABC):
|
|||
f"1. 稍后重试\n" \
|
||||
f"2. 检查服务商状态页面\n" \
|
||||
f"3. 尝试切换其他LLM提供商"
|
||||
|
||||
|
||||
full_message = f"{context}: {message}" if context else message
|
||||
|
||||
|
||||
raise LLMError(
|
||||
full_message,
|
||||
self.config.provider,
|
||||
status_code,
|
||||
error
|
||||
error,
|
||||
api_response=api_response
|
||||
)
|
||||
|
||||
async def retry(self, fn, max_attempts: int = 3, delay: float = 1.0) -> Any:
|
||||
|
|
|
|||
|
|
@ -359,12 +359,14 @@ Please analyze the following code:
|
|||
try:
|
||||
adapter = LLMFactory.create_adapter(self.config)
|
||||
|
||||
# 使用用户配置的 temperature(如果未设置则使用 config 中的默认值)
|
||||
request = LLMRequest(
|
||||
messages=[
|
||||
LLMMessage(role="system", content=system_prompt),
|
||||
LLMMessage(role="user", content=user_prompt)
|
||||
],
|
||||
temperature=0.1,
|
||||
temperature=self.config.temperature,
|
||||
max_tokens=self.config.max_tokens,
|
||||
)
|
||||
|
||||
response = await adapter.complete(request)
|
||||
|
|
@ -401,39 +403,97 @@ Please analyze the following code:
|
|||
logger.error(f"Provider: {self.config.provider.value}, Model: {self.config.model}")
|
||||
# 重新抛出异常,让调用者处理
|
||||
raise
|
||||
|
||||
async def chat_completion_raw(
|
||||
|
||||
async def chat_completion(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: float = 0.1,
|
||||
max_tokens: int = 4096,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
tools: Optional[List[Dict[str, Any]]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
🔥 Agent 使用的原始聊天完成接口(兼容旧接口)
|
||||
|
||||
🔥 Agent 使用的聊天完成接口(支持工具调用)
|
||||
|
||||
Args:
|
||||
messages: 消息列表,格式为 [{"role": "user", "content": "..."}]
|
||||
temperature: 温度参数
|
||||
max_tokens: 最大token数
|
||||
|
||||
temperature: 温度参数(None 时使用用户配置)
|
||||
max_tokens: 最大token数(None 时使用用户配置)
|
||||
tools: 工具描述列表(可选)
|
||||
|
||||
Returns:
|
||||
包含 content 和 usage 的字典
|
||||
包含 content、usage 和 tool_calls 的字典
|
||||
"""
|
||||
# 使用用户配置作为默认值
|
||||
actual_temperature = temperature if temperature is not None else self.config.temperature
|
||||
actual_max_tokens = max_tokens if max_tokens is not None else self.config.max_tokens
|
||||
|
||||
# 转换消息格式
|
||||
llm_messages = [
|
||||
LLMMessage(role=msg["role"], content=msg["content"])
|
||||
for msg in messages
|
||||
]
|
||||
|
||||
|
||||
request = LLMRequest(
|
||||
messages=llm_messages,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
temperature=actual_temperature,
|
||||
max_tokens=actual_max_tokens,
|
||||
tools=tools,
|
||||
)
|
||||
|
||||
|
||||
adapter = LLMFactory.create_adapter(self.config)
|
||||
response = await adapter.complete(request)
|
||||
|
||||
|
||||
result = {
|
||||
"content": response.content,
|
||||
"usage": {
|
||||
"prompt_tokens": response.usage.prompt_tokens if response.usage else 0,
|
||||
"completion_tokens": response.usage.completion_tokens if response.usage else 0,
|
||||
"total_tokens": response.usage.total_tokens if response.usage else 0,
|
||||
},
|
||||
}
|
||||
|
||||
# 添加工具调用信息
|
||||
if response.tool_calls:
|
||||
result["tool_calls"] = response.tool_calls
|
||||
|
||||
return result
|
||||
|
||||
async def chat_completion_raw(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
🔥 Agent 使用的原始聊天完成接口(兼容旧接口)
|
||||
|
||||
Args:
|
||||
messages: 消息列表,格式为 [{"role": "user", "content": "..."}]
|
||||
temperature: 温度参数(None 时使用用户配置)
|
||||
max_tokens: 最大token数(None 时使用用户配置)
|
||||
|
||||
Returns:
|
||||
包含 content 和 usage 的字典
|
||||
"""
|
||||
# 使用用户配置作为默认值
|
||||
actual_temperature = temperature if temperature is not None else self.config.temperature
|
||||
actual_max_tokens = max_tokens if max_tokens is not None else self.config.max_tokens
|
||||
|
||||
# 转换消息格式
|
||||
llm_messages = [
|
||||
LLMMessage(role=msg["role"], content=msg["content"])
|
||||
for msg in messages
|
||||
]
|
||||
|
||||
request = LLMRequest(
|
||||
messages=llm_messages,
|
||||
temperature=actual_temperature,
|
||||
max_tokens=actual_max_tokens,
|
||||
)
|
||||
|
||||
adapter = LLMFactory.create_adapter(self.config)
|
||||
response = await adapter.complete(request)
|
||||
|
||||
return {
|
||||
"content": response.content,
|
||||
"usage": {
|
||||
|
|
@ -446,29 +506,33 @@ Please analyze the following code:
|
|||
async def chat_completion_stream(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: float = 0.1,
|
||||
max_tokens: int = 4096,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
流式聊天完成接口,逐 token 返回
|
||||
|
||||
|
||||
Args:
|
||||
messages: 消息列表
|
||||
temperature: 温度参数
|
||||
max_tokens: 最大token数
|
||||
|
||||
temperature: 温度参数(None 时使用用户配置)
|
||||
max_tokens: 最大token数(None 时使用用户配置)
|
||||
|
||||
Yields:
|
||||
dict: {"type": "token", "content": str} 或 {"type": "done", ...}
|
||||
"""
|
||||
# 使用用户配置作为默认值
|
||||
actual_temperature = temperature if temperature is not None else self.config.temperature
|
||||
actual_max_tokens = max_tokens if max_tokens is not None else self.config.max_tokens
|
||||
|
||||
llm_messages = [
|
||||
LLMMessage(role=msg["role"], content=msg["content"])
|
||||
for msg in messages
|
||||
]
|
||||
|
||||
|
||||
request = LLMRequest(
|
||||
messages=llm_messages,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
temperature=actual_temperature,
|
||||
max_tokens=actual_max_tokens,
|
||||
)
|
||||
|
||||
if self.config.provider in NATIVE_ONLY_PROVIDERS:
|
||||
|
|
@ -869,15 +933,17 @@ Please analyze the following code:
|
|||
|
||||
try:
|
||||
adapter = LLMFactory.create_adapter(self.config)
|
||||
|
||||
|
||||
# 使用用户配置的 temperature 和 max_tokens
|
||||
request = LLMRequest(
|
||||
messages=[
|
||||
LLMMessage(role="system", content=full_system_prompt),
|
||||
LLMMessage(role="user", content=user_prompt)
|
||||
],
|
||||
temperature=0.1,
|
||||
temperature=self.config.temperature,
|
||||
max_tokens=self.config.max_tokens,
|
||||
)
|
||||
|
||||
|
||||
response = await adapter.complete(request)
|
||||
content = response.content
|
||||
|
||||
|
|
|
|||
|
|
@ -79,12 +79,14 @@ class LLMError(Exception):
|
|||
message: str,
|
||||
provider: Optional[LLMProvider] = None,
|
||||
status_code: Optional[int] = None,
|
||||
original_error: Optional[Any] = None
|
||||
original_error: Optional[Any] = None,
|
||||
api_response: Optional[str] = None
|
||||
):
|
||||
super().__init__(message)
|
||||
self.provider = provider
|
||||
self.status_code = status_code
|
||||
self.original_error = original_error
|
||||
self.api_response = api_response # API 服务器返回的原始错误信息
|
||||
|
||||
|
||||
# 各平台默认模型 (2025年最新推荐)
|
||||
|
|
|
|||
|
|
@ -463,6 +463,98 @@ class JinaEmbedding(EmbeddingProvider):
|
|||
return results
|
||||
|
||||
|
||||
class QwenEmbedding(EmbeddingProvider):
|
||||
"""Qwen 嵌入服务(基于阿里云 DashScope embeddings API)"""
|
||||
|
||||
MODELS = {
|
||||
# DashScope Qwen 嵌入模型及其默认维度
|
||||
"text-embedding-v4": 1024, # 支持维度: 2048, 1536, 1024(默认), 768, 512, 256, 128, 64
|
||||
"text-embedding-v3": 1024, # 支持维度: 1024(默认), 768, 512, 256, 128, 64
|
||||
"text-embedding-v2": 1536, # 支持维度: 1536
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
model: str = "text-embedding-v4",
|
||||
):
|
||||
# 优先使用显式传入的 api_key,其次使用 EMBEDDING_API_KEY/QWEN_API_KEY/LLM_API_KEY
|
||||
self.api_key = (
|
||||
api_key
|
||||
or getattr(settings, "EMBEDDING_API_KEY", None)
|
||||
or getattr(settings, "QWEN_API_KEY", None)
|
||||
or settings.LLM_API_KEY
|
||||
)
|
||||
# 🔥 API 密钥验证
|
||||
if not self.api_key:
|
||||
raise ValueError(
|
||||
"Qwen embedding requires API key. "
|
||||
"Set EMBEDDING_API_KEY, QWEN_API_KEY or LLM_API_KEY environment variable."
|
||||
)
|
||||
# DashScope 兼容 OpenAI 的 embeddings 端点
|
||||
self.base_url = base_url or "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
self.model = model
|
||||
self._dimension = self.MODELS.get(model, 1024)
|
||||
|
||||
@property
|
||||
def dimension(self) -> int:
|
||||
return self._dimension
|
||||
|
||||
async def embed_text(self, text: str) -> EmbeddingResult:
|
||||
results = await self.embed_texts([text])
|
||||
return results[0]
|
||||
|
||||
async def embed_texts(self, texts: List[str]) -> List[EmbeddingResult]:
|
||||
if not texts:
|
||||
return []
|
||||
|
||||
# 与 OpenAI 接口保持一致的截断策略
|
||||
max_length = 8191
|
||||
truncated_texts = [text[:max_length] for text in texts]
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": self.model,
|
||||
"input": truncated_texts,
|
||||
"encoding_format": "float",
|
||||
}
|
||||
|
||||
url = f"{self.base_url.rstrip('/')}/embeddings"
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=60) as client:
|
||||
response = await client.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
usage = data.get("usage", {}) or {}
|
||||
total_tokens = usage.get("total_tokens") or usage.get("prompt_tokens") or 0
|
||||
|
||||
results: List[EmbeddingResult] = []
|
||||
for item in data.get("data", []):
|
||||
results.append(EmbeddingResult(
|
||||
embedding=item["embedding"],
|
||||
tokens_used=total_tokens // max(len(texts), 1),
|
||||
model=self.model,
|
||||
))
|
||||
|
||||
return results
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Qwen embedding API error: {e.response.status_code} - {e.response.text}")
|
||||
raise RuntimeError(f"Qwen embedding API failed: {e.response.status_code}") from e
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Qwen embedding network error: {e}")
|
||||
raise RuntimeError(f"Qwen embedding network error: {e}") from e
|
||||
except Exception as e:
|
||||
logger.error(f"Qwen embedding unexpected error: {e}")
|
||||
raise RuntimeError(f"Qwen embedding failed: {e}") from e
|
||||
|
||||
|
||||
class EmbeddingService:
|
||||
"""
|
||||
嵌入服务
|
||||
|
|
@ -539,6 +631,9 @@ class EmbeddingService:
|
|||
elif provider == "jina":
|
||||
return JinaEmbedding(api_key=api_key, base_url=base_url, model=model)
|
||||
|
||||
elif provider == "qwen":
|
||||
return QwenEmbedding(api_key=api_key, base_url=base_url, model=model)
|
||||
|
||||
else:
|
||||
# 默认使用 OpenAI
|
||||
return OpenAIEmbedding(api_key=api_key, base_url=base_url, model=model)
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ class TreeSitterParser:
|
|||
return False
|
||||
|
||||
try:
|
||||
from tree_sitter_languages import get_parser
|
||||
from tree_sitter_language_pack import get_parser
|
||||
|
||||
parser = get_parser(language)
|
||||
self._parsers[language] = parser
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ PDF 报告生成服务 - 专业审计版 (WeasyPrint)
|
|||
"""
|
||||
|
||||
import io
|
||||
import html
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any
|
||||
import math
|
||||
|
|
@ -344,7 +345,9 @@ class ReportGenerator:
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if issue.description %}
|
||||
<div class="issue-desc">{{ issue.description }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if issue.code_snippet %}
|
||||
<div class="code-snippet mono">{{ issue.code_snippet }}</div>
|
||||
|
|
@ -395,31 +398,55 @@ class ReportGenerator:
|
|||
return ""
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def _escape_html(cls, text: str) -> str:
|
||||
"""安全转义 HTML 特殊字符"""
|
||||
if text is None:
|
||||
return None
|
||||
return html.escape(str(text))
|
||||
|
||||
@classmethod
|
||||
def _process_issues(cls, issues: List[Dict]) -> List[Dict]:
|
||||
processed = []
|
||||
order = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3}
|
||||
sorted_issues = sorted(issues, key=lambda x: order.get(x.get('severity', 'low'), 4))
|
||||
|
||||
|
||||
sev_labels = {
|
||||
'critical': 'CRITICAL',
|
||||
'high': 'HIGH',
|
||||
'medium': 'MEDIUM',
|
||||
'low': 'LOW'
|
||||
}
|
||||
|
||||
|
||||
for i in sorted_issues:
|
||||
item = i.copy()
|
||||
item['severity'] = item.get('severity', 'low')
|
||||
item['severity_label'] = sev_labels.get(item['severity'], 'UNKNOWN')
|
||||
item['line'] = item.get('line_number') or item.get('line')
|
||||
|
||||
|
||||
# 确保代码片段存在 (处理可能的字段名差异)
|
||||
code = item.get('code_snippet') or item.get('code') or item.get('context')
|
||||
if isinstance(code, list):
|
||||
code = '\n'.join(code)
|
||||
item['code_snippet'] = code
|
||||
|
||||
item['code_snippet'] = cls._escape_html(code) if code else None
|
||||
|
||||
# 确保 description 不为 None
|
||||
desc = item.get('description')
|
||||
if not desc or desc == 'None':
|
||||
desc = item.get('title', '') # 如果没有描述,使用标题
|
||||
item['description'] = cls._escape_html(desc)
|
||||
|
||||
# 确保 suggestion 不为 None
|
||||
suggestion = item.get('suggestion')
|
||||
if suggestion == 'None' or suggestion is None:
|
||||
item['suggestion'] = None
|
||||
else:
|
||||
item['suggestion'] = cls._escape_html(suggestion)
|
||||
|
||||
# 转义标题和文件路径
|
||||
item['title'] = cls._escape_html(item.get('title', ''))
|
||||
item['file_path'] = cls._escape_html(item.get('file_path'))
|
||||
|
||||
processed.append(item)
|
||||
return processed
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,25 @@ from app.services.llm.service import LLMService
|
|||
from app.core.config import settings
|
||||
|
||||
|
||||
def get_analysis_config(user_config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
获取分析配置参数(优先使用用户配置,然后使用系统配置)
|
||||
|
||||
Returns:
|
||||
包含以下字段的字典:
|
||||
- max_analyze_files: 最大分析文件数
|
||||
- llm_concurrency: LLM 并发数
|
||||
- llm_gap_ms: LLM 请求间隔(毫秒)
|
||||
"""
|
||||
other_config = (user_config or {}).get('otherConfig', {})
|
||||
|
||||
return {
|
||||
'max_analyze_files': other_config.get('maxAnalyzeFiles') or settings.MAX_ANALYZE_FILES,
|
||||
'llm_concurrency': other_config.get('llmConcurrency') or settings.LLM_CONCURRENCY,
|
||||
'llm_gap_ms': other_config.get('llmGapMs') or settings.LLM_GAP_MS,
|
||||
}
|
||||
|
||||
|
||||
# 支持的文本文件扩展名
|
||||
TEXT_EXTENSIONS = [
|
||||
".js", ".ts", ".tsx", ".jsx", ".py", ".java", ".go", ".rs",
|
||||
|
|
@ -385,19 +404,24 @@ async def scan_repo_task(task_id: str, db_session_factory, user_config: dict = N
|
|||
|
||||
print(f"✅ 成功获取分支 {actual_branch} 的文件列表")
|
||||
|
||||
# 获取分析配置(优先使用用户配置)
|
||||
analysis_config = get_analysis_config(user_config)
|
||||
max_analyze_files = analysis_config['max_analyze_files']
|
||||
llm_gap_ms = analysis_config['llm_gap_ms']
|
||||
|
||||
# 限制文件数量
|
||||
# 如果指定了特定文件,则只分析这些文件
|
||||
target_files = (user_config or {}).get('scan_config', {}).get('file_paths', [])
|
||||
if target_files:
|
||||
print(f"🎯 指定分析 {len(target_files)} 个文件")
|
||||
files = [f for f in files if f['path'] in target_files]
|
||||
elif settings.MAX_ANALYZE_FILES > 0:
|
||||
files = files[:settings.MAX_ANALYZE_FILES]
|
||||
|
||||
elif max_analyze_files > 0:
|
||||
files = files[:max_analyze_files]
|
||||
|
||||
task.total_files = len(files)
|
||||
await db.commit()
|
||||
|
||||
print(f"📊 获取到 {len(files)} 个文件,开始分析")
|
||||
print(f"📊 获取到 {len(files)} 个文件,开始分析 (最大文件数: {max_analyze_files}, 请求间隔: {llm_gap_ms}ms)")
|
||||
|
||||
# 4. 分析文件
|
||||
total_issues = 0
|
||||
|
|
@ -536,7 +560,7 @@ async def scan_repo_task(task_id: str, db_session_factory, user_config: dict = N
|
|||
print(f"📈 任务 {task_id}: 进度 {scanned_files}/{len(files)} ({int(scanned_files/len(files)*100)}%)")
|
||||
|
||||
# 请求间隔
|
||||
await asyncio.sleep(settings.LLM_GAP_MS / 1000)
|
||||
await asyncio.sleep(llm_gap_ms / 1000)
|
||||
|
||||
except Exception as file_error:
|
||||
failed_files += 1
|
||||
|
|
@ -546,7 +570,7 @@ async def scan_repo_task(task_id: str, db_session_factory, user_config: dict = N
|
|||
print(f"❌ 分析文件失败 ({file_info['path']}): {file_error}")
|
||||
print(f" 错误类型: {type(file_error).__name__}")
|
||||
print(f" 详细信息: {traceback.format_exc()}")
|
||||
await asyncio.sleep(settings.LLM_GAP_MS / 1000)
|
||||
await asyncio.sleep(llm_gap_ms / 1000)
|
||||
|
||||
# 5. 完成任务
|
||||
avg_quality_score = sum(quality_scores) / len(quality_scores) if quality_scores else 100.0
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "deepaudit-backend"
|
||||
version = "3.0.1"
|
||||
version = "3.0.2"
|
||||
description = "DeepAudit Backend API - AI-Powered Code Security Audit Platform"
|
||||
requires-python = ">=3.11"
|
||||
readme = "README.md"
|
||||
|
|
@ -60,9 +60,9 @@ dependencies = [
|
|||
"chromadb>=0.4.22",
|
||||
|
||||
# ============ Code Parsing ============
|
||||
# tree-sitter-languages 1.10.x 与 tree-sitter 0.22+ 不兼容
|
||||
"tree-sitter==0.21.3",
|
||||
"tree-sitter-languages>=1.10.0",
|
||||
# 使用 tree-sitter-language-pack 替代已弃用的 tree-sitter-languages
|
||||
"tree-sitter>=0.23.0",
|
||||
"tree-sitter-language-pack>=0.4.0",
|
||||
"pygments>=2.17.0",
|
||||
|
||||
# ============ Docker Sandbox ============
|
||||
|
|
|
|||
134
backend/uv.lock
134
backend/uv.lock
|
|
@ -989,7 +989,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "deepaudit-backend"
|
||||
version = "3.0.1"
|
||||
version = "3.0.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
|
|
@ -1035,7 +1035,7 @@ dependencies = [
|
|||
{ name = "sse-starlette" },
|
||||
{ name = "tiktoken" },
|
||||
{ name = "tree-sitter" },
|
||||
{ name = "tree-sitter-languages" },
|
||||
{ name = "tree-sitter-language-pack" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
{ name = "weasyprint" },
|
||||
]
|
||||
|
|
@ -1123,8 +1123,8 @@ requires-dist = [
|
|||
{ name = "sqlalchemy", specifier = ">=2.0.0" },
|
||||
{ name = "sse-starlette", specifier = ">=1.8.2" },
|
||||
{ name = "tiktoken", specifier = ">=0.5.2" },
|
||||
{ name = "tree-sitter", specifier = "==0.21.3" },
|
||||
{ name = "tree-sitter-languages", specifier = ">=1.10.0" },
|
||||
{ name = "tree-sitter", specifier = ">=0.23.0" },
|
||||
{ name = "tree-sitter-language-pack", specifier = ">=0.4.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.23.0" },
|
||||
{ name = "weasyprint", specifier = ">=60.0" },
|
||||
]
|
||||
|
|
@ -5121,54 +5121,104 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.21.3"
|
||||
version = "0.25.2"
|
||||
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
|
||||
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/39/9e/b7cb190aa08e4ea387f2b1531da03efb4b8b033426753c0b97e3698645f6/tree-sitter-0.21.3.tar.gz", hash = "sha256:b5de3028921522365aa864d95b3c41926e0ba6a85ee5bd000e10dc49b0766988", size = 155688, upload-time = "2024-03-26T10:53:35.451Z" }
|
||||
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/66/7c/0350cfc47faadc0d3cf7d8237a4e34032b3014ddf4a12ded9933e1648b55/tree-sitter-0.25.2.tar.gz", hash = "sha256:fe43c158555da46723b28b52e058ad444195afd1db3ca7720c59a254544e9c20", size = 177961, upload-time = "2025-09-25T17:37:59.751Z" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/63/b5/72657d5874d7f0a722c0288f04e5e2bc33d7715b13a858885b6593047dce/tree_sitter-0.21.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54b22c3c2aab3e3639a4b255d9df8455da2921d050c4829b6a5663b057f10db5", size = 133429, upload-time = "2024-03-26T10:52:46.345Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d3/64/c5d397efbb6d0dbed4254cd2ca389ed186a2e1e7e32661059f6eeaaf6424/tree_sitter-0.21.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab6e88c1e2d5e84ff0f9e5cd83f21b8e5074ad292a2cf19df3ba31d94fbcecd4", size = 126088, upload-time = "2024-03-26T10:52:47.759Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ba/88/941669acc140f94e6c6196d6d8676ac4cd57c3b3fbc1ee61bb11c1b2da71/tree_sitter-0.21.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3fd34ed4cd5db445bc448361b5da46a2a781c648328dc5879d768f16a46771", size = 487879, upload-time = "2024-03-26T10:52:49.091Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/29/4e/798154f2846d620bf9fa3bc244e056d4858f2108f834656bf9f1219d4f30/tree_sitter-0.21.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fabc7182f6083269ce3cfcad202fe01516aa80df64573b390af6cd853e8444a1", size = 498776, upload-time = "2024-03-26T10:52:50.709Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/6e/d1/05ea77487bc7a3946d0e80fb6c5cb61515953f5e7a4f6804b98e113ed4b0/tree_sitter-0.21.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f874c3f7d2a2faf5c91982dc7d88ff2a8f183a21fe475c29bee3009773b0558", size = 483348, upload-time = "2024-03-26T10:52:52.267Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/42/fa/bf938e7c6afbc368d503deeda060891c3dba57e2d1166e4b884271f55616/tree_sitter-0.21.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ee61ee3b7a4eedf9d8f1635c68ba4a6fa8c46929601fc48a907c6cfef0cfbcb2", size = 493757, upload-time = "2024-03-26T10:52:54.845Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/1d/a7/98da36a6eab22f5729989c9e0137b1b04cbe368d1e024fccd72c0b00719b/tree_sitter-0.21.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b7256c723642de1c05fbb776b27742204a2382e337af22f4d9e279d77df7aa2", size = 109735, upload-time = "2024-03-26T10:52:57.243Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/81/e1/cceb06eae617a6bf5eeeefa9813d9fd57d89b50f526ce02486a336bcd2a9/tree_sitter-0.21.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:669b3e5a52cb1e37d60c7b16cc2221c76520445bb4f12dd17fd7220217f5abf3", size = 133640, upload-time = "2024-03-26T10:52:59.135Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f6/ce/ac14e5cbb0f30b7bd338122491ee2b8e6c0408cfe26741cbd66fa9b53d35/tree_sitter-0.21.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2aa2a5099a9f667730ff26d57533cc893d766667f4d8a9877e76a9e74f48f0d3", size = 125954, upload-time = "2024-03-26T10:53:00.879Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/c2/df/76dbf830126e566c48db0d1bf2bef3f9d8cac938302a9b0f762ded8206c2/tree_sitter-0.21.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a3e06ae2a517cf6f1abb682974f76fa760298e6d5a3ecf2cf140c70f898adf0", size = 490092, upload-time = "2024-03-26T10:53:03.144Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ec/87/0c3593552cb0d09ab6271d37fc0e6a9476919d2a975661d709d4b3289fc7/tree_sitter-0.21.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af992dfe08b4fefcfcdb40548d0d26d5d2e0a0f2d833487372f3728cd0772b48", size = 502155, upload-time = "2024-03-26T10:53:04.76Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/05/92/b2cb22cf52c18fcc95662897f380cf230c443dfc9196b872aad5948b7bb3/tree_sitter-0.21.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c7cbab1dd9765138505c4a55e2aa857575bac4f1f8a8b0457744a4fefa1288e6", size = 486020, upload-time = "2024-03-26T10:53:06.414Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/4a/ea/69b543538a46d763f3e787234d1617b718ab90f32ffa676ca856f1d9540e/tree_sitter-0.21.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1e66aeb457d1529370fcb0997ae5584c6879e0e662f1b11b2f295ea57e22f54", size = 496348, upload-time = "2024-03-26T10:53:07.939Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/eb/4f/df4ea84476443021707b537217c32147ccccbc3e10c17b216a969991e1b3/tree_sitter-0.21.3-cp312-cp312-win_amd64.whl", hash = "sha256:013c750252dc3bd0e069d82e9658de35ed50eecf31c6586d0de7f942546824c5", size = 109771, upload-time = "2024-03-26T10:53:10.342Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/7c/22/88a1e00b906d26fa8a075dd19c6c3116997cb884bf1b3c023deb065a344d/tree_sitter-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ca72d841215b6573ed0655b3a5cd1133f9b69a6fa561aecad40dca9029d75b", size = 146752, upload-time = "2025-09-25T17:37:24.775Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/57/1c/22cc14f3910017b7a76d7358df5cd315a84fe0c7f6f7b443b49db2e2790d/tree_sitter-0.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc0351cfe5022cec5a77645f647f92a936b38850346ed3f6d6babfbeeeca4d26", size = 137765, upload-time = "2025-09-25T17:37:26.103Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/1c/0c/d0de46ded7d5b34631e0f630d9866dab22d3183195bf0f3b81de406d6622/tree_sitter-0.25.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1799609636c0193e16c38f366bda5af15b1ce476df79ddaae7dd274df9e44266", size = 604643, upload-time = "2025-09-25T17:37:27.398Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/34/38/b735a58c1c2f60a168a678ca27b4c1a9df725d0bf2d1a8a1c571c033111e/tree_sitter-0.25.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e65ae456ad0d210ee71a89ee112ac7e72e6c2e5aac1b95846ecc7afa68a194c", size = 632229, upload-time = "2025-09-25T17:37:28.463Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/32/f6/cda1e1e6cbff5e28d8433578e2556d7ba0b0209d95a796128155b97e7693/tree_sitter-0.25.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:49ee3c348caa459244ec437ccc7ff3831f35977d143f65311572b8ba0a5f265f", size = 629861, upload-time = "2025-09-25T17:37:29.593Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f9/19/427e5943b276a0dd74c2a1f1d7a7393443f13d1ee47dedb3f8127903c080/tree_sitter-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:56ac6602c7d09c2c507c55e58dc7026b8988e0475bd0002f8a386cce5e8e8adc", size = 127304, upload-time = "2025-09-25T17:37:30.549Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/eb/d9/eef856dc15f784d85d1397a17f3ee0f82df7778efce9e1961203abfe376a/tree_sitter-0.25.2-cp311-cp311-win_arm64.whl", hash = "sha256:b3d11a3a3ac89bb8a2543d75597f905a9926f9c806f40fcca8242922d1cc6ad5", size = 113990, upload-time = "2025-09-25T17:37:31.852Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/3c/9e/20c2a00a862f1c2897a436b17edb774e831b22218083b459d0d081c9db33/tree_sitter-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ddabfff809ffc983fc9963455ba1cecc90295803e06e140a4c83e94c1fa3d960", size = 146941, upload-time = "2025-09-25T17:37:34.813Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ef/04/8512e2062e652a1016e840ce36ba1cc33258b0dcc4e500d8089b4054afec/tree_sitter-0.25.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c0c0ab5f94938a23fe81928a21cc0fac44143133ccc4eb7eeb1b92f84748331c", size = 137699, upload-time = "2025-09-25T17:37:36.349Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/47/8a/d48c0414db19307b0fb3bb10d76a3a0cbe275bb293f145ee7fba2abd668e/tree_sitter-0.25.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd12d80d91d4114ca097626eb82714618dcdfacd6a5e0955216c6485c350ef99", size = 607125, upload-time = "2025-09-25T17:37:37.725Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/39/d1/b95f545e9fc5001b8a78636ef942a4e4e536580caa6a99e73dd0a02e87aa/tree_sitter-0.25.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b43a9e4c89d4d0839de27cd4d6902d33396de700e9ff4c5ab7631f277a85ead9", size = 635418, upload-time = "2025-09-25T17:37:38.922Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/de/4d/b734bde3fb6f3513a010fa91f1f2875442cdc0382d6a949005cd84563d8f/tree_sitter-0.25.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbb1706407c0e451c4f8cc016fec27d72d4b211fdd3173320b1ada7a6c74c3ac", size = 631250, upload-time = "2025-09-25T17:37:40.039Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/46/f2/5f654994f36d10c64d50a192239599fcae46677491c8dd53e7579c35a3e3/tree_sitter-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:6d0302550bbe4620a5dc7649517c4409d74ef18558276ce758419cf09e578897", size = 127156, upload-time = "2025-09-25T17:37:41.132Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/67/23/148c468d410efcf0a9535272d81c258d840c27b34781d625f1f627e2e27d/tree_sitter-0.25.2-cp312-cp312-win_arm64.whl", hash = "sha256:0c8b6682cac77e37cfe5cf7ec388844957f48b7bd8d6321d0ca2d852994e10d5", size = 113984, upload-time = "2025-09-25T17:37:42.074Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/8c/67/67492014ce32729b63d7ef318a19f9cfedd855d677de5773476caf771e96/tree_sitter-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0628671f0de69bb279558ef6b640bcfc97864fe0026d840f872728a86cd6b6cd", size = 146926, upload-time = "2025-09-25T17:37:43.041Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/4e/9c/a278b15e6b263e86c5e301c82a60923fa7c59d44f78d7a110a89a413e640/tree_sitter-0.25.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f5ddcd3e291a749b62521f71fc953f66f5fd9743973fd6dd962b092773569601", size = 137712, upload-time = "2025-09-25T17:37:44.039Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/54/9a/423bba15d2bf6473ba67846ba5244b988cd97a4b1ea2b146822162256794/tree_sitter-0.25.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd88fbb0f6c3a0f28f0a68d72df88e9755cf5215bae146f5a1bdc8362b772053", size = 607873, upload-time = "2025-09-25T17:37:45.477Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ed/4c/b430d2cb43f8badfb3a3fa9d6cd7c8247698187b5674008c9d67b2a90c8e/tree_sitter-0.25.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b878e296e63661c8e124177cc3084b041ba3f5936b43076d57c487822426f614", size = 636313, upload-time = "2025-09-25T17:37:46.68Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/9d/27/5f97098dbba807331d666a0997662e82d066e84b17d92efab575d283822f/tree_sitter-0.25.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d77605e0d353ba3fe5627e5490f0fbfe44141bafa4478d88ef7954a61a848dae", size = 631370, upload-time = "2025-09-25T17:37:47.993Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d4/3c/87caaed663fabc35e18dc704cd0e9800a0ee2f22bd18b9cbe7c10799895d/tree_sitter-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:463c032bd02052d934daa5f45d183e0521ceb783c2548501cf034b0beba92c9b", size = 127157, upload-time = "2025-09-25T17:37:48.967Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d5/23/f8467b408b7988aff4ea40946a4bd1a2c1a73d17156a9d039bbaff1e2ceb/tree_sitter-0.25.2-cp313-cp313-win_arm64.whl", hash = "sha256:b3f63a1796886249bd22c559a5944d64d05d43f2be72961624278eff0dcc5cb8", size = 113975, upload-time = "2025-09-25T17:37:49.922Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/07/e3/d9526ba71dfbbe4eba5e51d89432b4b333a49a1e70712aa5590cd22fc74f/tree_sitter-0.25.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65d3c931013ea798b502782acab986bbf47ba2c452610ab0776cf4a8ef150fc0", size = 146776, upload-time = "2025-09-25T17:37:50.898Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/42/97/4bd4ad97f85a23011dd8a535534bb1035c4e0bac1234d58f438e15cff51f/tree_sitter-0.25.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bda059af9d621918efb813b22fb06b3fe00c3e94079c6143fcb2c565eb44cb87", size = 137732, upload-time = "2025-09-25T17:37:51.877Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/b6/19/1e968aa0b1b567988ed522f836498a6a9529a74aab15f09dd9ac1e41f505/tree_sitter-0.25.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eac4e8e4c7060c75f395feec46421eb61212cb73998dbe004b7384724f3682ab", size = 609456, upload-time = "2025-09-25T17:37:52.925Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/48/b6/cf08f4f20f4c9094006ef8828555484e842fc468827ad6e56011ab668dbd/tree_sitter-0.25.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:260586381b23be33b6191a07cea3d44ecbd6c01aa4c6b027a0439145fcbc3358", size = 636772, upload-time = "2025-09-25T17:37:54.647Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/57/e2/d42d55bf56360987c32bc7b16adb06744e425670b823fb8a5786a1cea991/tree_sitter-0.25.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7d2ee1acbacebe50ba0f85fff1bc05e65d877958f00880f49f9b2af38dce1af0", size = 631522, upload-time = "2025-09-25T17:37:55.833Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/03/87/af9604ebe275a9345d88c3ace0cf2a1341aa3f8ef49dd9fc11662132df8a/tree_sitter-0.25.2-cp314-cp314-win_amd64.whl", hash = "sha256:4973b718fcadfb04e59e746abfbb0288694159c6aeecd2add59320c03368c721", size = 130864, upload-time = "2025-09-25T17:37:57.453Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/a6/6e/e64621037357acb83d912276ffd30a859ef117f9c680f2e3cb955f47c680/tree_sitter-0.25.2-cp314-cp314-win_arm64.whl", hash = "sha256:b8d4429954a3beb3e844e2872610d2a4800ba4eb42bb1990c6a4b1949b18459f", size = 117470, upload-time = "2025-09-25T17:37:58.431Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-languages"
|
||||
version = "1.10.2"
|
||||
name = "tree-sitter-c-sharp"
|
||||
version = "0.23.1"
|
||||
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
|
||||
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/22/85/a61c782afbb706a47d990eaee6977e7c2bd013771c5bf5c81c617684f286/tree_sitter_c_sharp-0.23.1.tar.gz", hash = "sha256:322e2cfd3a547a840375276b2aea3335fa6458aeac082f6c60fec3f745c967eb", size = 1317728, upload-time = "2024-11-11T05:25:32.535Z" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/58/04/f6c2df4c53a588ccd88d50851155945cff8cd887bd70c175e00aaade7edf/tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2b612a6e5bd17bb7fa2aab4bb6fc1fba45c94f09cb034ab332e45603b86e32fd", size = 372235, upload-time = "2024-11-11T05:25:19.424Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/99/10/1aa9486f1e28fc22810fa92cbdc54e1051e7f5536a5e5b5e9695f609b31e/tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a8b98f62bc53efcd4d971151950c9b9cd5cbe3bacdb0cd69fdccac63350d83e", size = 419046, upload-time = "2024-11-11T05:25:20.679Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/0f/21/13df29f8fcb9ba9f209b7b413a4764b673dfd58989a0dd67e9c7e19e9c2e/tree_sitter_c_sharp-0.23.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:986e93d845a438ec3c4416401aa98e6a6f6631d644bbbc2e43fcb915c51d255d", size = 415999, upload-time = "2024-11-11T05:25:22.359Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ca/72/fc6846795bcdae2f8aa94cc8b1d1af33d634e08be63e294ff0d6794b1efc/tree_sitter_c_sharp-0.23.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8024e466b2f5611c6dc90321f232d8584893c7fb88b75e4a831992f877616d2", size = 402830, upload-time = "2024-11-11T05:25:24.198Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/fe/3a/b6028c5890ce6653807d5fa88c72232c027c6ceb480dbeb3b186d60e5971/tree_sitter_c_sharp-0.23.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7f9bf876866835492281d336b9e1f9626ab668737f74e914c31d285261507da7", size = 397880, upload-time = "2024-11-11T05:25:25.937Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/47/d2/4facaa34b40f8104d8751746d0e1cd2ddf0beb9f1404b736b97f372bd1f3/tree_sitter_c_sharp-0.23.1-cp39-abi3-win_amd64.whl", hash = "sha256:ae9a9e859e8f44e2b07578d44f9a220d3fa25b688966708af6aa55d42abeebb3", size = 377562, upload-time = "2024-11-11T05:25:27.539Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d8/88/3cf6bd9959d94d1fec1e6a9c530c5f08ff4115a474f62aedb5fedb0f7241/tree_sitter_c_sharp-0.23.1-cp39-abi3-win_arm64.whl", hash = "sha256:c81548347a93347be4f48cb63ec7d60ef4b0efa91313330e69641e49aa5a08c5", size = 375157, upload-time = "2024-11-11T05:25:30.839Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-embedded-template"
|
||||
version = "0.25.0"
|
||||
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
|
||||
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/fd/a7/77729fefab8b1b5690cfc54328f2f629d1c076d16daf32c96ba39d3a3a3a/tree_sitter_embedded_template-0.25.0.tar.gz", hash = "sha256:7d72d5e8a1d1d501a7c90e841b51f1449a90cc240be050e4fb85c22dab991d50", size = 14114, upload-time = "2025-08-29T00:42:51.078Z" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/1f/9d/3e3c8ee0c019d3bace728300a1ca807c03df39e66cc51e9a5e7c9d1e1909/tree_sitter_embedded_template-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fa0d06467199aeb33fb3d6fa0665bf9b7d5a32621ffdaf37fd8249f8a8050649", size = 10266, upload-time = "2025-08-29T00:42:44.148Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/e8/ab/6d4e43b736b2a895d13baea3791dc8ce7245bedf4677df9e7deb22e23a2a/tree_sitter_embedded_template-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc7aacbc2985a5d7e7fe7334f44dffe24c38fb0a8295c4188a04cf21a3d64a73", size = 10650, upload-time = "2025-08-29T00:42:45.147Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/9f/97/ea3d1ea4b320fe66e0468b9f6602966e544c9fe641882484f9105e50ee0c/tree_sitter_embedded_template-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7c88c3dd8b94b3c9efe8ae071ff6b1b936a27ac5f6e651845c3b9631fa4c1c2", size = 18268, upload-time = "2025-08-29T00:42:46.03Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/64/40/0f42ca894a8f7c298cf336080046ccc14c10e8f4ea46d455f640193181b2/tree_sitter_embedded_template-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:025f7ca84218dcd8455efc901bdbcc2689fb694f3a636c0448e322a23d4bc96b", size = 19068, upload-time = "2025-08-29T00:42:46.699Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d0/2a/0b720bcae7c2dd0a44889c09e800a2f8eb08c496dede9f2b97683506c4c3/tree_sitter_embedded_template-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b5dc1aef6ffa3fae621fe037d85dd98948b597afba20df29d779c426be813ee5", size = 18518, upload-time = "2025-08-29T00:42:47.694Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/14/8a/d745071afa5e8bdf5b381cf84c4dc6be6c79dee6af8e0ff07476c3d8e4aa/tree_sitter_embedded_template-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d0a35cfe634c44981a516243bc039874580e02a2990669313730187ce83a5bc6", size = 18267, upload-time = "2025-08-29T00:42:48.635Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/5d/74/728355e594fca140f793f234fdfec195366b6956b35754d00ea97ca18b21/tree_sitter_embedded_template-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:3e05a4ac013d54505e75ae48e1a0e9db9aab19949fe15d9f4c7345b11a84a069", size = 13049, upload-time = "2025-08-29T00:42:49.589Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d8/de/afac475e694d0e626b0808f3c86339c349cd15c5163a6a16a53cc11cf892/tree_sitter_embedded_template-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:2751d402179ac0e83f2065b249d8fe6df0718153f1636bcb6a02bde3e5730db9", size = 11978, upload-time = "2025-08-29T00:42:50.226Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-language-pack"
|
||||
version = "0.13.0"
|
||||
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
|
||||
dependencies = [
|
||||
{ name = "tree-sitter" },
|
||||
{ name = "tree-sitter-c-sharp" },
|
||||
{ name = "tree-sitter-embedded-template" },
|
||||
{ name = "tree-sitter-yaml" },
|
||||
]
|
||||
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/c1/83/d1bc738d6f253f415ee54a8afb99640f47028871436f53f2af637c392c4f/tree_sitter_language_pack-0.13.0.tar.gz", hash = "sha256:032034c5e27b1f6e00730b9e7c2dbc8203b4700d0c681fd019d6defcf61183ec", size = 51353370, upload-time = "2025-11-26T14:01:04.586Z" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/24/6c/c310e958296ce12076bec846c0bb779bc114897b33901c4c51c09bb6b695/tree_sitter_languages-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7eb7d7542b2091c875fe52719209631fca36f8c10fa66970d2c576ae6a1b8289", size = 8884893, upload-time = "2024-02-04T10:28:14.963Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/65/82/183b039abe46d6753357019b4f0484d5b74973ee4675da2f26af5ba8dfdf/tree_sitter_languages-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6b41bcb00974b1c8a1800c7f1bb476a1d15a0463e760ee24872f2d53b08ee424", size = 9724629, upload-time = "2024-02-04T10:28:17.776Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ba/a2/e8272617901f896ae36459ed2a2ff06d9b1ff5e6157d034c5e2c9885c741/tree_sitter_languages-1.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f370cd7845c6c81df05680d5bd96db8a99d32b56f4728c5d05978911130a853", size = 8669175, upload-time = "2024-02-04T10:28:19.819Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/a6/97/2c72765a807ea226759a827324ed6a74382b4ae1b18321c67333199a4622/tree_sitter_languages-1.10.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1dc195c88ef4c72607e112a809a69190e096a2e5ebc6201548b3e05fdd169ad", size = 8584029, upload-time = "2024-02-04T10:28:22.464Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/96/81/ab4eda8dbd3f736fcc9a508bc69232d3b9076cd46b932d9bf9d49b9a1ec9/tree_sitter_languages-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae34ac314a7170be24998a0f994c1ac80761d8d4bd126af27ee53a023d3b849", size = 8422544, upload-time = "2024-02-04T10:28:25.104Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/80/35/9af34d7259399179ecc2a9f8e73a795c1caf3220b01d566c3ddd20ed5e1c/tree_sitter_languages-1.10.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:01b5742d5f5bd675489486b582bd482215880b26dde042c067f8265a6e925d9c", size = 9186540, upload-time = "2024-02-04T10:28:27.322Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/a7/24/3e3d5a83578f9942ab882c9c89e757fd3e98ca7d68f7608c9702d8608a1c/tree_sitter_languages-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ab1cbc46244d34fd16f21edaa20231b2a57f09f092a06ee3d469f3117e6eb954", size = 9166371, upload-time = "2024-02-04T10:28:29.953Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f2/81/7792b474916541081533942598feaabc6e1df993892375a1a3d8f7100483/tree_sitter_languages-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0b1149e7467a4e92b8a70e6005fe762f880f493cf811fc003554b29f04f5e7c8", size = 8945341, upload-time = "2024-02-04T10:28:32.696Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/6d/80/5e9679325e260cce2893b4a97a3914d5ed729024bb9b08a32d9b0d83ef7a/tree_sitter_languages-1.10.2-cp311-cp311-win32.whl", hash = "sha256:049276343962f4696390ee555acc2c1a65873270c66a6cbe5cb0bca83bcdf3c6", size = 8363372, upload-time = "2024-02-04T10:28:34.907Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d9/52/e122dfc6739664c963a62f4b6717853e86295659c8531e2f1842bad9aba5/tree_sitter_languages-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:7f3fdd468a577f04db3b63454d939e26e360229b53c80361920aa1ebf2cd7491", size = 8269020, upload-time = "2024-02-04T10:28:37.43Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/8d/bf/a9bd2d6ecbd053de0a5a50c150105b69c90eb49089f9e1d4fc4937e86adc/tree_sitter_languages-1.10.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c0f4c8b2734c45859edc7fcaaeaab97a074114111b5ba51ab4ec7ed52104763c", size = 8884771, upload-time = "2024-02-04T10:28:39.655Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/14/fb/1f6fe5903aeb7435cc66d4b56621e9a30a4de64420555b999de65b31fcae/tree_sitter_languages-1.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eecd3c1244ac3425b7a82ba9125b4ddb45d953bbe61de114c0334fd89b7fe782", size = 9724562, upload-time = "2024-02-04T10:28:42.275Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/20/6c/1855a65c9d6b50600f7a68e0182153db7cb12ff81fdebd93e87851dfdd8f/tree_sitter_languages-1.10.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15db3c8510bc39a80147ee7421bf4782c15c09581c1dc2237ea89cefbd95b846", size = 8678682, upload-time = "2024-02-04T10:28:44.642Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d0/75/eff180f187ce4dc3e5177b3f8508e0061ea786ac44f409cf69cf24bf31a6/tree_sitter_languages-1.10.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92c6487a6feea683154d3e06e6db68c30e0ae749a7ce4ce90b9e4e46b78c85c7", size = 8595099, upload-time = "2024-02-04T10:28:47.767Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f2/e6/eddc76ad899d77adcb5fca6cdf651eb1d33b4a799456bf303540f6cf8204/tree_sitter_languages-1.10.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2f1cd1d1bdd65332f9c2b67d49dcf148cf1ded752851d159ac3e5ee4f4d260", size = 8433569, upload-time = "2024-02-04T10:28:50.404Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/06/95/a13da048c33a876d0475974484bf66b1fae07226e8654b1365ab549309cd/tree_sitter_languages-1.10.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:976c8039165b8e12f17a01ddee9f4e23ec6e352b165ad29b44d2bf04e2fbe77e", size = 9196003, upload-time = "2024-02-04T10:28:52.466Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ec/13/9e5cb03914d60dd51047ecbfab5400309fbab14bb25014af388f492da044/tree_sitter_languages-1.10.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:dafbbdf16bf668a580902e1620f4baa1913e79438abcce721a50647564c687b9", size = 9175560, upload-time = "2024-02-04T10:28:55.064Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/19/76/25bb32a9be1c476e388835d5c8de5af2920af055e295770003683896cfe2/tree_sitter_languages-1.10.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1aeabd3d60d6d276b73cd8f3739d595b1299d123cc079a317f1a5b3c5461e2ca", size = 8956249, upload-time = "2024-02-04T10:28:57.094Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/52/01/8e2f97a444d25dde1380ec20b338722f733b6cc290524357b1be3dd452ab/tree_sitter_languages-1.10.2-cp312-cp312-win32.whl", hash = "sha256:fab8ee641914098e8933b87ea3d657bea4dd00723c1ee7038b847b12eeeef4f5", size = 8363094, upload-time = "2024-02-04T10:28:59.156Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/47/58/0262e875dd899447476a8ffde7829df3716ffa772990095c65d6de1f053c/tree_sitter_languages-1.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:5e606430d736367e5787fa5a7a0c5a1ec9b85eded0b3596bbc0d83532a40810b", size = 8268983, upload-time = "2024-02-04T10:29:00.987Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/e9/38/aec1f450ae5c4796de8345442f297fcf8912c7d2e00a66d3236ff0f825ed/tree_sitter_language_pack-0.13.0-cp310-abi3-macosx_10_15_universal2.whl", hash = "sha256:0e7eae812b40a2dc8a12eb2f5c55e130eb892706a0bee06215dd76affeb00d07", size = 32991857, upload-time = "2025-11-26T14:00:51.459Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/90/09/11f51c59ede786dccddd2d348d5d24a1d99c54117d00f88b477f5fae4bd5/tree_sitter_language_pack-0.13.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:7fdacf383418a845b20772118fcb53ad245f9c5d409bd07dae16acec65151756", size = 20092989, upload-time = "2025-11-26T14:00:54.202Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/72/9d/644db031047ab1a70fc5cb6a79a4d4067080fac628375b2320752d2d7b58/tree_sitter_language_pack-0.13.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:0d4f261fce387ae040dae7e4d1c1aca63d84c88320afcc0961c123bec0be8377", size = 19952029, upload-time = "2025-11-26T14:00:56.699Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/48/92/5fd749bbb3f5e4538492c77de7bc51a5e479fec6209464ddc25be9153b13/tree_sitter_language_pack-0.13.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:78f369dc4d456c5b08d659939e662c2f9b9fba8c0ec5538a1f973e01edfcf04d", size = 19944614, upload-time = "2025-11-26T14:00:59.381Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/97/59/2287f07723c063475d6657babed0d5569f4b499e393ab51354d529c3e7b5/tree_sitter_language_pack-0.13.0-cp310-abi3-win_amd64.whl", hash = "sha256:1cdbc88a03dacd47bec69e56cc20c48eace1fbb6f01371e89c3ee6a2e8f34db1", size = 16896852, upload-time = "2025-11-26T14:01:01.788Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-yaml"
|
||||
version = "0.7.2"
|
||||
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
|
||||
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/57/b6/941d356ac70c90b9d2927375259e3a4204f38f7499ec6e7e8a95b9664689/tree_sitter_yaml-0.7.2.tar.gz", hash = "sha256:756db4c09c9d9e97c81699e8f941cb8ce4e51104927f6090eefe638ee567d32c", size = 84882, upload-time = "2025-10-07T14:40:36.071Z" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/38/29/c0b8dbff302c49ff4284666ffb6f2f21145006843bb4c3a9a85d0ec0b7ae/tree_sitter_yaml-0.7.2-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:7e269ddcfcab8edb14fbb1f1d34eed1e1e26888f78f94eedfe7cc98c60f8bc9f", size = 43898, upload-time = "2025-10-07T14:40:29.486Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/18/0d/15a5add06b3932b5e4ce5f5e8e179197097decfe82a0ef000952c8b98216/tree_sitter_yaml-0.7.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:0807b7966e23ddf7dddc4545216e28b5a58cdadedcecca86b8d8c74271a07870", size = 44691, upload-time = "2025-10-07T14:40:30.369Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/72/92/c4b896c90d08deb8308fadbad2210fdcc4c66c44ab4292eac4e80acb4b61/tree_sitter_yaml-0.7.2-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1a5c60c98b6c4c037aae023569f020d0c489fad8dc26fdfd5510363c9c29a41", size = 91430, upload-time = "2025-10-07T14:40:31.16Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/89/59/61f1fed31eb6d46ff080b8c0d53658cf29e10263f41ef5fe34768908037a/tree_sitter_yaml-0.7.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88636d19d0654fd24f4f242eaaafa90f6f5ebdba8a62e4b32d251ed156c51a2a", size = 92428, upload-time = "2025-10-07T14:40:31.954Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/e3/62/a33a04d19b7f9a0ded780b9c9fcc6279e37c5d00b89b00425bb807a22cc2/tree_sitter_yaml-0.7.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1d2e8f0bb14aa4537320952d0f9607eef3021d5aada8383c34ebeece17db1e06", size = 90580, upload-time = "2025-10-07T14:40:33.037Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/6c/e7/9525defa7b30792623f56b1fba9bbba361752348875b165b8975b87398fd/tree_sitter_yaml-0.7.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:74ca712c50fc9d7dbc68cb36b4a7811d6e67a5466b5a789f19bf8dd6084ef752", size = 90455, upload-time = "2025-10-07T14:40:33.778Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/4a/d6/8d1e1ace03db3b02e64e91daf21d1347941d1bbecc606a5473a1a605250d/tree_sitter_yaml-0.7.2-cp310-abi3-win_amd64.whl", hash = "sha256:7587b5ca00fc4f9a548eff649697a3b395370b2304b399ceefa2087d8a6c9186", size = 45514, upload-time = "2025-10-07T14:40:34.562Z" },
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d8/c7/dcf3ea1c4f5da9b10353b9af4455d756c92d728a8f58f03c480d3ef0ead5/tree_sitter_yaml-0.7.2-cp310-abi3-win_arm64.whl", hash = "sha256:f63c227b18e7ce7587bce124578f0bbf1f890ac63d3e3cd027417574273642c4", size = 44065, upload-time = "2025-10-07T14:40:35.337Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# =============================================
|
||||
# DeepAudit v3.0.0 生产环境一键部署配置(国内加速版)
|
||||
# DeepAudit v3.0.2 生产环境一键部署配置(国内加速版)
|
||||
# =============================================
|
||||
# 使用南京大学镜像站加速拉取 GHCR 镜像
|
||||
# 部署命令: curl -fsSL https://raw.githubusercontent.com/lintsinghua/DeepAudit/main/docker-compose.prod.cn.yml | docker compose -f - up -d
|
||||
|
|
@ -89,6 +89,13 @@ services:
|
|||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:80"
|
||||
environment:
|
||||
# 禁用代理 - nginx 需要直连后端
|
||||
- HTTP_PROXY=
|
||||
- HTTPS_PROXY=
|
||||
- http_proxy=
|
||||
- https_proxy=
|
||||
- NO_PROXY=*
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# =============================================
|
||||
# DeepAudit v3.0.0 生产环境一键部署配置
|
||||
# DeepAudit v3.0.2 生产环境一键部署配置
|
||||
# =============================================
|
||||
# 使用预构建的 GHCR 镜像,无需本地构建
|
||||
# 部署命令: curl -fsSL https://raw.githubusercontent.com/lintsinghua/DeepAudit/main/docker-compose.prod.yml | docker compose -f - up -d
|
||||
|
|
@ -54,10 +54,15 @@ services:
|
|||
- LLM_MODEL=${LLM_MODEL:-gpt-4o}
|
||||
- LLM_API_KEY=${LLM_API_KEY:-your-api-key-here}
|
||||
- LLM_BASE_URL=${LLM_BASE_URL:-}
|
||||
# 禁用代理
|
||||
# 禁用代理 - 必须同时设置大小写变量
|
||||
- HTTP_PROXY=
|
||||
- HTTPS_PROXY=
|
||||
- http_proxy=
|
||||
- https_proxy=
|
||||
- all_proxy=
|
||||
- ALL_PROXY=
|
||||
- NO_PROXY=*
|
||||
- no_proxy=*
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
|
@ -86,6 +91,13 @@ services:
|
|||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:80"
|
||||
environment:
|
||||
# 禁用代理 - nginx 需要直连后端
|
||||
- HTTP_PROXY=
|
||||
- HTTPS_PROXY=
|
||||
- http_proxy=
|
||||
- https_proxy=
|
||||
- NO_PROXY=*
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# =============================================
|
||||
# DeepAudit v3.0.0 Docker Compose 配置
|
||||
# DeepAudit v3.0.2 Docker Compose 配置
|
||||
# =============================================
|
||||
# 部署: docker compose up -d
|
||||
# 查看日志: docker compose logs -f
|
||||
|
|
@ -41,7 +41,7 @@ services:
|
|||
- ALL_PROXY=
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./backend/app:/app/app:ro # 挂载代码目录,修改后自动生效
|
||||
# - ./backend/app:/app/app:ro # 挂载代码目录,修改后自动生效
|
||||
- backend_uploads:/app/uploads
|
||||
- /var/run/docker.sock:/var/run/docker.sock # 沙箱执行必须
|
||||
ports:
|
||||
|
|
@ -53,6 +53,7 @@ services:
|
|||
- REDIS_URL=redis://redis:6379/0
|
||||
- AGENT_ENABLED=true
|
||||
- SANDBOX_ENABLED=true
|
||||
- SANDBOX_IMAGE=deepaudit/sandbox:latest # 使用本地构建的沙箱镜像
|
||||
# 禁用代理设置,防止容器内无法连接外部 API
|
||||
- HTTP_PROXY=
|
||||
- HTTPS_PROXY=
|
||||
|
|
@ -81,11 +82,17 @@ services:
|
|||
- ALL_PROXY=
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./frontend/dist:/usr/share/nginx/html # 挂载构建产物,本地 pnpm build 后自动生效
|
||||
- ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro # 挂载 nginx 配置
|
||||
# - ./frontend/dist:/usr/share/nginx/html:ro # 挂载构建产物,本地 pnpm build 后自动生效
|
||||
- ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro # 挂载 nginx 配置
|
||||
ports:
|
||||
- "3000:80" # Nginx 监听 80 端口
|
||||
environment:
|
||||
# 禁用代理 - nginx 需要直连后端
|
||||
- HTTP_PROXY=
|
||||
- HTTPS_PROXY=
|
||||
- http_proxy=
|
||||
- https_proxy=
|
||||
- NO_PROXY=*
|
||||
- VITE_API_BASE_URL=/api/v1
|
||||
depends_on:
|
||||
- backend
|
||||
|
|
|
|||
|
|
@ -289,8 +289,8 @@ server {
|
|||
|
||||
| 依赖 | 版本要求 | 说明 |
|
||||
|------|---------|------|
|
||||
| Node.js | 18+ | 前端运行环境 |
|
||||
| Python | 3.13+ | 后端运行环境 |
|
||||
| Node.js | 20+ | 前端运行环境 |
|
||||
| Python | 3.11+ | 后端运行环境 |
|
||||
| PostgreSQL | 15+ | 数据库 |
|
||||
| pnpm | 8+ | 推荐的前端包管理器 |
|
||||
| uv | 最新版 | 推荐的 Python 包管理器 |
|
||||
|
|
|
|||
|
|
@ -291,14 +291,6 @@ LLM_BASE_URL=https://your-proxy.com/v1
|
|||
LLM_MODEL=gpt-4o-mini
|
||||
```
|
||||
|
||||
**常见中转站**:
|
||||
|
||||
| 中转站 | 特点 |
|
||||
|--------|------|
|
||||
| [OpenRouter](https://openrouter.ai/) | 支持多种模型,统一接口 |
|
||||
| [API2D](https://api2d.com/) | 国内访问友好 |
|
||||
| [CloseAI](https://www.closeai-asia.com/) | 价格实惠 |
|
||||
|
||||
---
|
||||
|
||||
## 选择建议
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "deep-audit",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 280 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
|
|
@ -1,5 +1,6 @@
|
|||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import "@/assets/styles/globals.css";
|
||||
import App from "./App.tsx";
|
||||
import { AppWrapper } from "@/components/layout/PageMeta";
|
||||
|
|
@ -9,9 +10,16 @@ import "@/shared/utils/fetchWrapper"; // 初始化fetch拦截器
|
|||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<ErrorBoundary>
|
||||
<AppWrapper>
|
||||
<App />
|
||||
</AppWrapper>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange={false}
|
||||
>
|
||||
<AppWrapper>
|
||||
<App />
|
||||
</AppWrapper>
|
||||
</ThemeProvider>
|
||||
</ErrorBoundary>
|
||||
</StrictMode>
|
||||
);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -22,8 +22,8 @@ export default function AgentModeSelector({
|
|||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Shield className="w-4 h-4 text-violet-400" />
|
||||
<span className="font-mono text-xs font-bold text-gray-400 uppercase tracking-wider">
|
||||
<Shield className="w-4 h-4 text-violet-600 dark:text-violet-400" />
|
||||
<span className="font-mono text-xs font-bold text-muted-foreground uppercase tracking-wider">
|
||||
审计模式
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -34,8 +34,8 @@ export default function AgentModeSelector({
|
|||
className={cn(
|
||||
"relative flex flex-col p-4 border cursor-pointer transition-all rounded",
|
||||
value === "fast"
|
||||
? "border-amber-500/50 bg-amber-950/30"
|
||||
: "border-gray-700 hover:border-gray-600 bg-gray-900/30",
|
||||
? "border-amber-500/50 bg-amber-50 dark:bg-amber-950/30"
|
||||
: "border-border hover:border-border bg-muted/50",
|
||||
disabled && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
|
|
@ -54,25 +54,25 @@ export default function AgentModeSelector({
|
|||
"p-1.5 rounded border",
|
||||
value === "fast"
|
||||
? "bg-amber-500/20 border-amber-500/50"
|
||||
: "bg-gray-800 border-gray-700"
|
||||
: "bg-muted border-border"
|
||||
)}>
|
||||
<Zap className={cn(
|
||||
"w-4 h-4",
|
||||
value === "fast" ? "text-amber-400" : "text-gray-500"
|
||||
value === "fast" ? "text-amber-600 dark:text-amber-400" : "text-muted-foreground"
|
||||
)} />
|
||||
</div>
|
||||
<span className={cn(
|
||||
"font-bold text-sm font-mono uppercase",
|
||||
value === "fast" ? "text-amber-300" : "text-gray-400"
|
||||
value === "fast" ? "text-amber-700 dark:text-amber-300" : "text-muted-foreground"
|
||||
)}>
|
||||
快速审计
|
||||
</span>
|
||||
{value === "fast" && (
|
||||
<CheckCircle2 className="w-4 h-4 text-amber-400 ml-auto" />
|
||||
<CheckCircle2 className="w-4 h-4 text-amber-600 dark:text-amber-400 ml-auto" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ul className="text-xs text-gray-500 space-y-1 mb-3 font-mono">
|
||||
<ul className="text-xs text-muted-foreground space-y-1 mb-3 font-mono">
|
||||
<li className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
速度快(分钟级)
|
||||
|
|
@ -81,14 +81,14 @@ export default function AgentModeSelector({
|
|||
<Code className="w-3 h-3" />
|
||||
逐文件 LLM 分析
|
||||
</li>
|
||||
<li className="flex items-center gap-1 text-gray-600">
|
||||
<li className="flex items-center gap-1 text-muted-foreground">
|
||||
<Shield className="w-3 h-3" />
|
||||
无漏洞验证
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="mt-auto pt-2 border-t border-gray-800">
|
||||
<span className="text-[10px] uppercase tracking-wider text-gray-600 font-bold font-mono">
|
||||
<div className="mt-auto pt-2 border-t border-border">
|
||||
<span className="text-xs uppercase tracking-wider text-muted-foreground font-bold font-mono">
|
||||
适合: CI/CD 集成、日常检查
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -99,8 +99,8 @@ export default function AgentModeSelector({
|
|||
className={cn(
|
||||
"relative flex flex-col p-4 border cursor-pointer transition-all rounded",
|
||||
value === "agent"
|
||||
? "border-violet-500/50 bg-violet-950/30"
|
||||
: "border-gray-700 hover:border-gray-600 bg-gray-900/30",
|
||||
? "border-violet-500/50 bg-violet-50 dark:bg-violet-950/30"
|
||||
: "border-border hover:border-border bg-muted/50",
|
||||
disabled && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
|
|
@ -115,7 +115,7 @@ export default function AgentModeSelector({
|
|||
/>
|
||||
|
||||
{/* 推荐标签 */}
|
||||
<div className="absolute -top-2 -right-2 px-2 py-0.5 bg-violet-600 text-white text-[10px] font-bold uppercase font-mono rounded shadow-[0_0_10px_rgba(139,92,246,0.5)]">
|
||||
<div className="absolute -top-2 -right-2 px-2 py-0.5 bg-violet-600 text-white text-xs font-bold uppercase font-mono rounded shadow-[0_0_10px_rgba(139,92,246,0.5)]">
|
||||
推荐
|
||||
</div>
|
||||
|
||||
|
|
@ -124,25 +124,25 @@ export default function AgentModeSelector({
|
|||
"p-1.5 rounded border",
|
||||
value === "agent"
|
||||
? "bg-violet-500/20 border-violet-500/50"
|
||||
: "bg-gray-800 border-gray-700"
|
||||
: "bg-muted border-border"
|
||||
)}>
|
||||
<Bot className={cn(
|
||||
"w-4 h-4",
|
||||
value === "agent" ? "text-violet-400" : "text-gray-500"
|
||||
value === "agent" ? "text-violet-600 dark:text-violet-400" : "text-muted-foreground"
|
||||
)} />
|
||||
</div>
|
||||
<span className={cn(
|
||||
"font-bold text-sm font-mono uppercase",
|
||||
value === "agent" ? "text-violet-300" : "text-gray-400"
|
||||
value === "agent" ? "text-violet-700 dark:text-violet-300" : "text-muted-foreground"
|
||||
)}>
|
||||
Agent 审计
|
||||
</span>
|
||||
{value === "agent" && (
|
||||
<CheckCircle2 className="w-4 h-4 text-violet-400 ml-auto" />
|
||||
<CheckCircle2 className="w-4 h-4 text-violet-600 dark:text-violet-400 ml-auto" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ul className="text-xs text-gray-500 space-y-1 mb-3 font-mono">
|
||||
<ul className="text-xs text-muted-foreground space-y-1 mb-3 font-mono">
|
||||
<li className="flex items-center gap-1">
|
||||
<Bot className="w-3 h-3" />
|
||||
AI Agent 自主分析
|
||||
|
|
@ -153,15 +153,15 @@ export default function AgentModeSelector({
|
|||
</li>
|
||||
<li className={cn(
|
||||
"flex items-center gap-1",
|
||||
value === "agent" ? "text-violet-400 font-medium" : "text-gray-500"
|
||||
value === "agent" ? "text-violet-600 dark:text-violet-400 font-medium" : "text-muted-foreground"
|
||||
)}>
|
||||
<Shield className="w-3 h-3" />
|
||||
沙箱漏洞验证
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="mt-auto pt-2 border-t border-gray-800">
|
||||
<span className="text-[10px] uppercase tracking-wider text-gray-600 font-bold font-mono">
|
||||
<div className="mt-auto pt-2 border-t border-border">
|
||||
<span className="text-xs uppercase tracking-wider text-muted-foreground font-bold font-mono">
|
||||
适合: 发版前审计、深度安全评估
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -170,9 +170,9 @@ export default function AgentModeSelector({
|
|||
|
||||
{/* 模式说明 */}
|
||||
{value === "agent" && (
|
||||
<div className="p-3 bg-violet-950/30 border border-violet-500/30 text-xs text-violet-300 rounded font-mono">
|
||||
<p className="font-bold mb-1 uppercase text-violet-400">Agent 审计模式说明:</p>
|
||||
<ul className="list-disc list-inside space-y-0.5 text-violet-300/80">
|
||||
<div className="p-3 bg-violet-50 dark:bg-violet-950/30 border border-violet-500/30 text-xs text-violet-700 dark:text-violet-300 rounded font-mono">
|
||||
<p className="font-bold mb-1 uppercase text-violet-700 dark:text-violet-400">Agent 审计模式说明:</p>
|
||||
<ul className="list-disc list-inside space-y-0.5 text-violet-600 dark:text-violet-300/80">
|
||||
<li>AI Agent 会自主规划审计策略</li>
|
||||
<li>使用 RAG 技术进行代码语义检索</li>
|
||||
<li>在 Docker 沙箱中验证发现的漏洞</li>
|
||||
|
|
|
|||
|
|
@ -232,16 +232,16 @@ export default function CreateAgentTaskDialog({
|
|||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="!w-[min(90vw,520px)] !max-w-none max-h-[85vh] flex flex-col p-0 gap-0 bg-[#0c0c12] border border-gray-800 rounded-lg">
|
||||
<DialogContent className="!w-[min(90vw,520px)] !max-w-none max-h-[85vh] flex flex-col p-0 gap-0 cyber-dialog border border-border rounded-lg">
|
||||
{/* Header */}
|
||||
<DialogHeader className="px-5 py-4 border-b border-gray-800 flex-shrink-0 bg-gray-900/50">
|
||||
<DialogTitle className="flex items-center gap-3 font-mono text-white">
|
||||
<DialogHeader className="px-5 py-4 border-b border-border flex-shrink-0 bg-muted">
|
||||
<DialogTitle className="flex items-center gap-3 font-mono text-foreground">
|
||||
<div className="p-2 bg-primary/20 rounded border border-primary/30">
|
||||
<Bot className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-base font-bold uppercase tracking-wider">New Agent Audit</span>
|
||||
<p className="text-xs text-gray-500 font-normal mt-0.5">
|
||||
<p className="text-xs text-muted-foreground font-normal mt-0.5">
|
||||
AI-Powered Security Analysis
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -252,17 +252,17 @@ export default function CreateAgentTaskDialog({
|
|||
{/* 项目选择 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-mono font-bold uppercase text-gray-400">
|
||||
<span className="text-xs font-mono font-bold uppercase text-muted-foreground">
|
||||
Select Project
|
||||
</span>
|
||||
<Badge className="cyber-badge-muted font-mono text-[10px]">
|
||||
<Badge className="cyber-badge-muted font-mono text-xs">
|
||||
{filteredProjects.length} available
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-600" />
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search projects..."
|
||||
value={searchTerm}
|
||||
|
|
@ -272,13 +272,13 @@ export default function CreateAgentTaskDialog({
|
|||
</div>
|
||||
|
||||
{/* 项目列表 */}
|
||||
<ScrollArea className="h-[200px] border border-gray-800 rounded bg-gray-900/30">
|
||||
<ScrollArea className="h-[200px] border border-border rounded bg-muted/50">
|
||||
{loadingProjects ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-primary" />
|
||||
</div>
|
||||
) : filteredProjects.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-600 font-mono">
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground font-mono">
|
||||
<Package className="w-8 h-8 mb-2 opacity-50" />
|
||||
<span className="text-sm">{searchTerm ? "No matches" : "No projects"}</span>
|
||||
</div>
|
||||
|
|
@ -302,9 +302,9 @@ export default function CreateAgentTaskDialog({
|
|||
<div className="space-y-4">
|
||||
{/* 仓库项目:分支选择 */}
|
||||
{isRepositoryProject(selectedProject) && (
|
||||
<div className="flex items-center gap-3 p-3 border border-gray-800 rounded bg-blue-950/20">
|
||||
<div className="flex items-center gap-3 p-3 border border-border rounded bg-blue-950/20">
|
||||
<GitBranch className="w-5 h-5 text-blue-400" />
|
||||
<span className="font-mono text-sm text-gray-400 w-16">Branch</span>
|
||||
<span className="font-mono text-sm text-muted-foreground w-16">Branch</span>
|
||||
{loadingBranches ? (
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-blue-400" />
|
||||
|
|
@ -315,9 +315,9 @@ export default function CreateAgentTaskDialog({
|
|||
<SelectTrigger className="flex-1 h-9 cyber-input">
|
||||
<SelectValue placeholder="Select branch" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#0c0c12] border-gray-700">
|
||||
<SelectContent className="cyber-dialog border-border">
|
||||
{branches.map((b) => (
|
||||
<SelectItem key={b} value={b} className="font-mono text-white">
|
||||
<SelectItem key={b} value={b} className="font-mono text-foreground">
|
||||
{b}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
|
@ -329,27 +329,27 @@ export default function CreateAgentTaskDialog({
|
|||
|
||||
{/* ZIP 项目:文件选择 */}
|
||||
{isZipProject(selectedProject) && (
|
||||
<div className="p-3 border border-gray-800 rounded bg-amber-950/20 space-y-3">
|
||||
<div className="p-3 border border-border rounded bg-amber-950/20 space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Package className="w-5 h-5 text-amber-400" />
|
||||
<span className="font-mono text-sm text-gray-400 uppercase font-bold">ZIP File</span>
|
||||
<span className="font-mono text-sm text-muted-foreground uppercase font-bold">ZIP File</span>
|
||||
</div>
|
||||
|
||||
{storedZipInfo?.has_file && (
|
||||
<div
|
||||
className={`p-2 rounded border cursor-pointer transition-colors ${useStoredZip
|
||||
? 'border-emerald-500/50 bg-emerald-950/30'
|
||||
: 'border-gray-700 hover:border-gray-600 bg-gray-900/30'
|
||||
: 'border-border hover:border-border bg-muted/50'
|
||||
}`}
|
||||
onClick={() => setUseStoredZip(true)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-3 h-3 rounded-full border-2 ${useStoredZip ? 'border-emerald-500 bg-emerald-500' : 'border-gray-600'
|
||||
<div className={`w-3 h-3 rounded-full border-2 ${useStoredZip ? 'border-emerald-500 bg-emerald-500' : 'border-border'
|
||||
}`} />
|
||||
<span className="text-sm text-white font-mono">
|
||||
<span className="text-sm text-foreground font-mono">
|
||||
{storedZipInfo.original_filename}
|
||||
</span>
|
||||
<Badge className="cyber-badge-success text-[10px]">
|
||||
<Badge className="cyber-badge-success text-xs">
|
||||
Stored
|
||||
</Badge>
|
||||
</div>
|
||||
|
|
@ -359,14 +359,14 @@ export default function CreateAgentTaskDialog({
|
|||
<div
|
||||
className={`p-2 rounded border cursor-pointer transition-colors ${!useStoredZip && zipFile
|
||||
? 'border-amber-500/50 bg-amber-950/30'
|
||||
: 'border-gray-700 hover:border-gray-600 bg-gray-900/30'
|
||||
: 'border-border hover:border-border bg-muted/50'
|
||||
}`}
|
||||
>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<div className={`w-3 h-3 rounded-full border-2 ${!useStoredZip && zipFile ? 'border-amber-500 bg-amber-500' : 'border-gray-600'
|
||||
<div className={`w-3 h-3 rounded-full border-2 ${!useStoredZip && zipFile ? 'border-amber-500 bg-amber-500' : 'border-border'
|
||||
}`} />
|
||||
<Upload className="w-4 h-4 text-gray-500" />
|
||||
<span className="text-sm text-gray-400 font-mono">
|
||||
<Upload className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground font-mono">
|
||||
{zipFile ? zipFile.name : "Upload new file..."}
|
||||
</span>
|
||||
<input
|
||||
|
|
@ -382,7 +382,7 @@ export default function CreateAgentTaskDialog({
|
|||
|
||||
{/* 高级选项 */}
|
||||
<Collapsible open={showAdvanced} onOpenChange={setShowAdvanced}>
|
||||
<CollapsibleTrigger className="flex items-center gap-2 text-xs font-mono text-gray-500 hover:text-gray-300 transition-colors">
|
||||
<CollapsibleTrigger className="flex items-center gap-2 text-xs font-mono text-muted-foreground hover:text-foreground transition-colors">
|
||||
<ChevronRight className={`w-4 h-4 transition-transform ${showAdvanced ? "rotate-90" : ""}`} />
|
||||
<Settings2 className="w-4 h-4" />
|
||||
<span className="uppercase font-bold">Advanced Options</span>
|
||||
|
|
@ -396,12 +396,12 @@ export default function CreateAgentTaskDialog({
|
|||
const canSelectFiles = isRepo || (isZip && useStoredZip && hasStoredZip);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-3 border border-dashed border-gray-700 rounded bg-gray-900/30">
|
||||
<div className="flex items-center justify-between p-3 border border-dashed border-border rounded bg-muted/50">
|
||||
<div>
|
||||
<p className="font-mono text-xs uppercase font-bold text-gray-500">
|
||||
<p className="font-mono text-xs uppercase font-bold text-muted-foreground">
|
||||
Scan Scope
|
||||
</p>
|
||||
<p className="text-sm text-white font-mono font-bold mt-1">
|
||||
<p className="text-sm text-foreground font-mono font-bold mt-1">
|
||||
{selectedFiles
|
||||
? `${selectedFiles.length} files selected`
|
||||
: "All files"}
|
||||
|
|
@ -434,9 +434,9 @@ export default function CreateAgentTaskDialog({
|
|||
})()}
|
||||
|
||||
{/* 排除模式 */}
|
||||
<div className="p-3 border border-dashed border-gray-700 rounded bg-gray-900/30 space-y-3">
|
||||
<div className="p-3 border border-dashed border-border rounded bg-muted/50 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-xs uppercase font-bold text-gray-500">
|
||||
<span className="font-mono text-xs uppercase font-bold text-muted-foreground">
|
||||
Exclude Patterns
|
||||
</span>
|
||||
<button
|
||||
|
|
@ -452,7 +452,7 @@ export default function CreateAgentTaskDialog({
|
|||
{excludePatterns.map((p) => (
|
||||
<Badge
|
||||
key={p}
|
||||
className="bg-gray-800 text-gray-300 border-0 font-mono text-xs cursor-pointer hover:bg-rose-900/50 hover:text-rose-400"
|
||||
className="bg-muted text-foreground border-0 font-mono text-xs cursor-pointer hover:bg-rose-900/50 hover:text-rose-400"
|
||||
onClick={() => setExcludePatterns((prev) => prev.filter((x) => x !== p))}
|
||||
>
|
||||
{p} ×
|
||||
|
|
@ -481,12 +481,12 @@ export default function CreateAgentTaskDialog({
|
|||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex-shrink-0 flex justify-end gap-3 px-5 py-4 bg-gray-900/50 border-t border-gray-800">
|
||||
<div className="flex-shrink-0 flex justify-end gap-3 px-5 py-4 bg-muted border-t border-border">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={creating}
|
||||
className="px-4 h-10 font-mono text-gray-400 hover:text-white hover:bg-gray-800"
|
||||
className="px-4 h-10 font-mono text-muted-foreground hover:text-foreground hover:bg-muted"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
@ -539,7 +539,7 @@ function ProjectItem({
|
|||
<div
|
||||
className={`flex items-center gap-3 p-3 cursor-pointer rounded transition-all ${selected
|
||||
? "bg-primary/10 border border-primary/50"
|
||||
: "hover:bg-gray-800/50 border border-transparent"
|
||||
: "hover:bg-muted border border-transparent"
|
||||
}`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
|
|
@ -553,11 +553,11 @@ function ProjectItem({
|
|||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`font-mono text-sm truncate ${selected ? 'text-white font-bold' : 'text-gray-300'}`}>
|
||||
<span className={`font-mono text-sm truncate ${selected ? 'text-foreground font-bold' : 'text-foreground'}`}>
|
||||
{project.name}
|
||||
</span>
|
||||
<Badge
|
||||
className={`text-[10px] px-1 py-0 font-mono ${isRepo
|
||||
className={`text-xs px-1 py-0 font-mono ${isRepo
|
||||
? "bg-blue-500/20 text-blue-400 border-blue-500/30"
|
||||
: "bg-amber-500/20 text-amber-400 border-amber-500/30"
|
||||
}`}
|
||||
|
|
@ -566,7 +566,7 @@ function ProjectItem({
|
|||
</Badge>
|
||||
</div>
|
||||
{project.description && (
|
||||
<p className="text-xs text-gray-600 mt-0.5 font-mono truncate">
|
||||
<p className="text-xs text-muted-foreground mt-0.5 font-mono truncate">
|
||||
{project.description}
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ export default function EmbeddingConfigPanel() {
|
|||
<div className="flex items-center justify-center min-h-[300px]">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="loading-spinner mx-auto" />
|
||||
<p className="text-gray-500 font-mono text-sm uppercase tracking-wider">加载配置中...</p>
|
||||
<p className="text-muted-foreground font-mono text-sm uppercase tracking-wider">加载配置中...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -202,26 +202,26 @@ export default function EmbeddingConfigPanel() {
|
|||
<div className="cyber-card p-4 border-primary/30">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Server className="w-4 h-4 text-primary" />
|
||||
<span className="font-mono font-bold text-sm uppercase text-gray-300">当前配置</span>
|
||||
<span className="font-mono font-bold text-sm uppercase text-foreground">当前配置</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-gray-900/50 p-3 rounded-lg border border-gray-800">
|
||||
<p className="text-xs text-gray-500 uppercase mb-1">提供商</p>
|
||||
<div className="bg-muted p-3 rounded-lg border border-border">
|
||||
<p className="text-xs text-muted-foreground uppercase mb-1">提供商</p>
|
||||
<Badge className="bg-primary/20 text-primary border-primary/50 font-mono">
|
||||
{currentConfig.provider}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 p-3 rounded-lg border border-gray-800">
|
||||
<p className="text-xs text-gray-500 uppercase mb-1">模型</p>
|
||||
<p className="font-mono text-sm text-gray-300 truncate">{currentConfig.model}</p>
|
||||
<div className="bg-muted p-3 rounded-lg border border-border">
|
||||
<p className="text-xs text-muted-foreground uppercase mb-1">模型</p>
|
||||
<p className="font-mono text-sm text-foreground truncate">{currentConfig.model}</p>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 p-3 rounded-lg border border-gray-800">
|
||||
<p className="text-xs text-gray-500 uppercase mb-1">向量维度</p>
|
||||
<p className="font-mono text-sm text-gray-300">{currentConfig.dimensions}</p>
|
||||
<div className="bg-muted p-3 rounded-lg border border-border">
|
||||
<p className="text-xs text-muted-foreground uppercase mb-1">向量维度</p>
|
||||
<p className="font-mono text-sm text-foreground">{currentConfig.dimensions}</p>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 p-3 rounded-lg border border-gray-800">
|
||||
<p className="text-xs text-gray-500 uppercase mb-1">批处理大小</p>
|
||||
<p className="font-mono text-sm text-gray-300">{currentConfig.batch_size}</p>
|
||||
<div className="bg-muted p-3 rounded-lg border border-border">
|
||||
<p className="text-xs text-muted-foreground uppercase mb-1">批处理大小</p>
|
||||
<p className="font-mono text-sm text-foreground">{currentConfig.batch_size}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -231,12 +231,12 @@ export default function EmbeddingConfigPanel() {
|
|||
<div className="cyber-card p-6 space-y-6">
|
||||
{/* 提供商选择 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold text-gray-500 uppercase">嵌入模型提供商</Label>
|
||||
<Label className="text-xs font-bold text-muted-foreground uppercase">嵌入模型提供商</Label>
|
||||
<Select value={selectedProvider} onValueChange={handleProviderChange}>
|
||||
<SelectTrigger className="h-12 cyber-input">
|
||||
<SelectValue placeholder="选择提供商" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#0c0c12] border-gray-700">
|
||||
<SelectContent className="cyber-dialog border-border">
|
||||
{providers.map((provider) => (
|
||||
<SelectItem key={provider.id} value={provider.id} className="font-mono">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -253,7 +253,7 @@ export default function EmbeddingConfigPanel() {
|
|||
</Select>
|
||||
|
||||
{selectedProviderInfo && (
|
||||
<p className="text-xs text-gray-500 flex items-center gap-1">
|
||||
<p className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<Info className="w-3 h-3 text-sky-400" />
|
||||
{selectedProviderInfo.description}
|
||||
</p>
|
||||
|
|
@ -263,7 +263,7 @@ export default function EmbeddingConfigPanel() {
|
|||
{/* 模型选择/输入 */}
|
||||
{selectedProviderInfo && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold text-gray-500 uppercase">模型</Label>
|
||||
<Label className="text-xs font-bold text-muted-foreground uppercase">模型</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={selectedModel}
|
||||
|
|
@ -273,7 +273,7 @@ export default function EmbeddingConfigPanel() {
|
|||
/>
|
||||
{selectedProviderInfo.models.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
<span className="text-xs text-gray-500">预设模型:</span>
|
||||
<span className="text-xs text-muted-foreground">预设模型:</span>
|
||||
{selectedProviderInfo.models.map((model) => (
|
||||
<button
|
||||
key={model}
|
||||
|
|
@ -282,7 +282,7 @@ export default function EmbeddingConfigPanel() {
|
|||
className={`px-2 py-1 text-xs font-mono rounded border transition-colors ${
|
||||
selectedModel === model
|
||||
? "bg-primary/20 border-primary/50 text-primary"
|
||||
: "bg-gray-800/50 border-gray-700 text-gray-400 hover:border-gray-600 hover:text-gray-300"
|
||||
: "bg-muted border-border text-muted-foreground hover:border-border hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{model}
|
||||
|
|
@ -296,7 +296,7 @@ export default function EmbeddingConfigPanel() {
|
|||
{/* API Key */}
|
||||
{selectedProviderInfo?.requires_api_key && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold text-gray-500 uppercase">
|
||||
<Label className="text-xs font-bold text-muted-foreground uppercase">
|
||||
API Key
|
||||
<span className="text-rose-400 ml-1">*</span>
|
||||
</Label>
|
||||
|
|
@ -307,7 +307,7 @@ export default function EmbeddingConfigPanel() {
|
|||
placeholder="输入 API Key"
|
||||
className="h-10 cyber-input"
|
||||
/>
|
||||
<p className="text-xs text-gray-600">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
API Key 将安全存储,不会显示在页面上
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -315,8 +315,8 @@ export default function EmbeddingConfigPanel() {
|
|||
|
||||
{/* 自定义端点 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold text-gray-500 uppercase">
|
||||
自定义 API 端点 <span className="text-gray-600">(可选)</span>
|
||||
<Label className="text-xs font-bold text-muted-foreground uppercase">
|
||||
自定义 API 端点 <span className="text-muted-foreground">(可选)</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="url"
|
||||
|
|
@ -335,14 +335,14 @@ export default function EmbeddingConfigPanel() {
|
|||
}
|
||||
className="h-10 cyber-input"
|
||||
/>
|
||||
<p className="text-xs text-gray-600">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
用于 API 代理或自托管服务
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 批处理大小 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold text-gray-500 uppercase">批处理大小</Label>
|
||||
<Label className="text-xs font-bold text-muted-foreground uppercase">批处理大小</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={batchSize}
|
||||
|
|
@ -351,7 +351,7 @@ export default function EmbeddingConfigPanel() {
|
|||
max={500}
|
||||
className="h-10 cyber-input w-32"
|
||||
/>
|
||||
<p className="text-xs text-gray-600">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
每批嵌入的文本数量,建议 50-100
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -379,14 +379,14 @@ export default function EmbeddingConfigPanel() {
|
|||
{testResult.success ? "测试成功" : "测试失败"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">{testResult.message}</p>
|
||||
<p className="text-sm text-muted-foreground">{testResult.message}</p>
|
||||
{testResult.success && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-800 text-xs text-gray-500 space-y-1 font-mono">
|
||||
<div>向量维度: <span className="text-gray-300">{testResult.dimensions}</span></div>
|
||||
<div>延迟: <span className="text-gray-300">{testResult.latency_ms}ms</span></div>
|
||||
<div className="mt-3 pt-3 border-t border-border text-xs text-muted-foreground space-y-1 font-mono">
|
||||
<div>向量维度: <span className="text-foreground">{testResult.dimensions}</span></div>
|
||||
<div>延迟: <span className="text-foreground">{testResult.latency_ms}ms</span></div>
|
||||
{testResult.sample_embedding && (
|
||||
<div className="truncate">
|
||||
示例向量: <span className="text-gray-400">[{testResult.sample_embedding.slice(0, 5).map((v) => v.toFixed(4)).join(", ")}...]</span>
|
||||
示例向量: <span className="text-muted-foreground">[{testResult.sample_embedding.slice(0, 5).map((v) => v.toFixed(4)).join(", ")}...]</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -395,7 +395,7 @@ export default function EmbeddingConfigPanel() {
|
|||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex items-center gap-3 pt-4 border-t border-gray-800 border-dashed">
|
||||
<div className="flex items-center gap-3 pt-4 border-t border-border border-dashed">
|
||||
<Button
|
||||
onClick={handleTest}
|
||||
disabled={testing || !selectedProvider || !selectedModel}
|
||||
|
|
@ -434,17 +434,36 @@ export default function EmbeddingConfigPanel() {
|
|||
</div>
|
||||
|
||||
{/* 说明 */}
|
||||
<div className="bg-gray-900/50 border border-gray-800 p-4 rounded-lg text-xs space-y-2">
|
||||
<p className="font-bold uppercase text-gray-400 flex items-center gap-2">
|
||||
<div className="bg-muted border border-border p-4 rounded-lg text-xs space-y-3">
|
||||
<p className="font-bold uppercase text-muted-foreground flex items-center gap-2">
|
||||
<Info className="w-4 h-4 text-sky-400" />
|
||||
关于嵌入模型
|
||||
</p>
|
||||
<ul className="text-gray-500 space-y-1 ml-6">
|
||||
<ul className="text-muted-foreground space-y-1 ml-6">
|
||||
<li>• 嵌入模型用于 Agent 审计的代码语义搜索 (RAG)</li>
|
||||
<li>• 与分析使用的 LLM 独立配置,互不影响</li>
|
||||
<li>• 推荐使用 <span className="text-gray-300">OpenAI text-embedding-3-small</span> 或本地 <span className="text-gray-300">Ollama</span></li>
|
||||
<li>• 推荐使用 <span className="text-foreground">OpenAI text-embedding-3-small</span> 或本地 <span className="text-foreground">Ollama</span></li>
|
||||
<li>• 向量维度影响存储空间和检索精度</li>
|
||||
</ul>
|
||||
|
||||
{/* OpenAI 兼容 API 引导 */}
|
||||
<div className="mt-3 pt-3 border-t border-border/50">
|
||||
<p className="font-bold text-amber-400 flex items-center gap-2 mb-2">
|
||||
<Zap className="w-4 h-4" />
|
||||
使用 OpenAI 兼容 API
|
||||
</p>
|
||||
<p className="text-muted-foreground mb-2">
|
||||
许多服务商提供 OpenAI 兼容的 API,可以直接使用 <span className="text-foreground">openai</span> 作为提供商:
|
||||
</p>
|
||||
<ul className="text-muted-foreground space-y-1 ml-4">
|
||||
<li>• <span className="text-foreground">DeepSeek</span>: 端点填写 <code className="text-primary bg-primary/10 px-1 rounded">https://api.deepseek.com/v1</code></li>
|
||||
<li>• <span className="text-foreground">Moonshot</span>: 端点填写 <code className="text-primary bg-primary/10 px-1 rounded">https://api.moonshot.cn/v1</code></li>
|
||||
<li>• <span className="text-foreground">智谱 GLM</span>: 端点填写 <code className="text-primary bg-primary/10 px-1 rounded">https://open.bigmodel.cn/api/paas/v4</code></li>
|
||||
</ul>
|
||||
<p className="text-muted-foreground mt-2 text-[11px]">
|
||||
提示:选择 openai 提供商,填入对应服务的 API Key 和自定义端点即可
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -305,16 +305,16 @@ export default function CreateTaskDialog({
|
|||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="!w-[min(90vw,520px)] !max-w-none max-h-[85vh] flex flex-col p-0 gap-0 bg-[#0c0c12] border border-gray-800 rounded-lg">
|
||||
<DialogContent className="!w-[min(90vw,520px)] !max-w-none max-h-[85vh] flex flex-col p-0 gap-0 cyber-dialog border border-border rounded-lg">
|
||||
{/* Header */}
|
||||
<DialogHeader className="px-5 py-4 border-b border-gray-800 flex-shrink-0 bg-gray-900/50">
|
||||
<DialogTitle className="flex items-center gap-3 font-mono text-white">
|
||||
<DialogHeader className="px-5 py-4 border-b border-border flex-shrink-0 bg-muted">
|
||||
<DialogTitle className="flex items-center gap-3 font-mono text-foreground">
|
||||
<div className="p-2 bg-primary/20 rounded border border-primary/30">
|
||||
<Shield className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-base font-bold uppercase tracking-wider">开始代码审计</span>
|
||||
<p className="text-xs text-gray-500 font-normal mt-0.5">
|
||||
<p className="text-xs text-muted-foreground font-normal mt-0.5">
|
||||
Code Security Analysis
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -325,17 +325,17 @@ export default function CreateTaskDialog({
|
|||
{/* 项目选择 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-mono font-bold uppercase text-gray-400">
|
||||
<span className="text-sm font-mono font-bold uppercase text-muted-foreground">
|
||||
选择项目
|
||||
</span>
|
||||
<Badge className="cyber-badge-muted font-mono text-[10px]">
|
||||
<Badge className="cyber-badge-muted font-mono text-xs">
|
||||
{filteredProjects.length} 个
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-600" />
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索项目..."
|
||||
value={searchTerm}
|
||||
|
|
@ -345,13 +345,13 @@ export default function CreateTaskDialog({
|
|||
</div>
|
||||
|
||||
{/* 项目列表 */}
|
||||
<ScrollArea className="h-[180px] border border-gray-800 rounded bg-gray-900/30">
|
||||
<ScrollArea className="h-[180px] border border-border rounded bg-muted/50">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-primary" />
|
||||
</div>
|
||||
) : filteredProjects.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-600 font-mono">
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground font-mono">
|
||||
<Package className="w-8 h-8 mb-2 opacity-50" />
|
||||
<span className="text-sm">
|
||||
{searchTerm ? "未找到" : "暂无项目"}
|
||||
|
|
@ -384,27 +384,27 @@ export default function CreateTaskDialog({
|
|||
{/* 配置区域 */}
|
||||
{selectedProject && (
|
||||
<div className="space-y-4">
|
||||
<span className="text-xs font-mono font-bold uppercase text-gray-400">
|
||||
<span className="text-sm font-mono font-bold uppercase text-muted-foreground">
|
||||
配置
|
||||
</span>
|
||||
|
||||
{isRepositoryProject(selectedProject) ? (
|
||||
<div className="flex items-center gap-3 p-3 border border-gray-800 rounded bg-blue-950/20">
|
||||
<GitBranch className="w-5 h-5 text-blue-400" />
|
||||
<span className="font-mono text-sm text-gray-400 w-12">
|
||||
<div className="flex items-center gap-3 p-3 border border-border rounded bg-blue-50 dark:bg-blue-950/20">
|
||||
<GitBranch className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
<span className="font-mono text-base text-muted-foreground w-12">
|
||||
分支
|
||||
</span>
|
||||
{loadingBranches ? (
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-blue-400" />
|
||||
<span className="text-sm text-blue-400 font-mono">加载中...</span>
|
||||
<Loader2 className="w-4 h-4 animate-spin text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-sm text-blue-600 dark:text-blue-400 font-mono">加载中...</span>
|
||||
</div>
|
||||
) : (
|
||||
<Select value={branch} onValueChange={setBranch}>
|
||||
<SelectTrigger className="h-9 flex-1 cyber-input">
|
||||
<SelectValue placeholder="选择分支" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#0c0c12] border-gray-700">
|
||||
<SelectContent className="cyber-dialog border-border">
|
||||
{branches.map((b) => (
|
||||
<SelectItem key={b} value={b} className="font-mono">
|
||||
{b}
|
||||
|
|
@ -438,19 +438,19 @@ export default function CreateTaskDialog({
|
|||
|
||||
{/* 规则集和提示词选择 - 仅快速扫描模式显示 */}
|
||||
{auditMode !== "agent" && (
|
||||
<div className="p-3 border border-gray-800 rounded bg-violet-950/20 space-y-3">
|
||||
<div className="p-3 border border-border rounded bg-violet-50 dark:bg-violet-950/20 space-y-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Zap className="w-4 h-4 text-violet-400" />
|
||||
<span className="font-mono text-sm font-bold text-violet-300 uppercase">审计配置</span>
|
||||
<Zap className="w-4 h-4 text-violet-600 dark:text-violet-400" />
|
||||
<span className="font-mono text-sm font-bold text-violet-700 dark:text-violet-300 uppercase">审计配置</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-mono font-bold text-gray-500 mb-1 uppercase">规则集</label>
|
||||
<label className="block text-xs font-mono font-bold text-muted-foreground mb-1 uppercase">规则集</label>
|
||||
<Select value={selectedRuleSetId} onValueChange={setSelectedRuleSetId}>
|
||||
<SelectTrigger className="h-9 cyber-input text-xs">
|
||||
<SelectValue placeholder="选择规则集" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#0c0c12] border-gray-700">
|
||||
<SelectContent className="cyber-dialog border-border">
|
||||
{ruleSets.map((rs) => (
|
||||
<SelectItem key={rs.id} value={rs.id} className="font-mono text-xs">
|
||||
{rs.name} {rs.is_default && '(默认)'} ({rs.enabled_rules_count})
|
||||
|
|
@ -460,12 +460,12 @@ export default function CreateTaskDialog({
|
|||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-mono font-bold text-gray-500 mb-1 uppercase">提示词模板</label>
|
||||
<label className="block text-xs font-mono font-bold text-muted-foreground mb-1 uppercase">提示词模板</label>
|
||||
<Select value={selectedPromptTemplateId} onValueChange={setSelectedPromptTemplateId}>
|
||||
<SelectTrigger className="h-9 cyber-input text-xs">
|
||||
<SelectValue placeholder="选择提示词模板" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#0c0c12] border-gray-700">
|
||||
<SelectContent className="cyber-dialog border-border">
|
||||
{promptTemplates.map((pt) => (
|
||||
<SelectItem key={pt.id} value={pt.id} className="font-mono text-xs">
|
||||
{pt.name} {pt.is_default && '(默认)'}
|
||||
|
|
@ -480,7 +480,7 @@ export default function CreateTaskDialog({
|
|||
|
||||
{/* 高级选项 */}
|
||||
<Collapsible open={showAdvanced} onOpenChange={setShowAdvanced}>
|
||||
<CollapsibleTrigger className="flex items-center gap-2 text-xs font-mono text-gray-500 hover:text-gray-300 transition-colors">
|
||||
<CollapsibleTrigger className="flex items-center gap-2 text-xs font-mono text-muted-foreground hover:text-foreground transition-colors">
|
||||
<ChevronRight
|
||||
className={`w-4 h-4 transition-transform ${showAdvanced ? "rotate-90" : ""}`}
|
||||
/>
|
||||
|
|
@ -489,9 +489,9 @@ export default function CreateTaskDialog({
|
|||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="mt-3 space-y-3">
|
||||
{/* 排除模式 */}
|
||||
<div className="p-3 border border-dashed border-gray-700 rounded bg-gray-900/30 space-y-3">
|
||||
<div className="p-3 border border-dashed border-border rounded bg-muted/50 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-xs uppercase font-bold text-gray-500">
|
||||
<span className="font-mono text-xs uppercase font-bold text-muted-foreground">
|
||||
排除模式
|
||||
</span>
|
||||
<button
|
||||
|
|
@ -507,7 +507,7 @@ export default function CreateTaskDialog({
|
|||
{excludePatterns.map((p) => (
|
||||
<Badge
|
||||
key={p}
|
||||
className="bg-gray-800 text-gray-300 border-0 font-mono text-xs cursor-pointer hover:bg-rose-900/50 hover:text-rose-400"
|
||||
className="bg-muted text-foreground border-0 font-mono text-xs cursor-pointer hover:bg-rose-100 dark:hover:bg-rose-900/50 hover:text-rose-600 dark:hover:text-rose-400"
|
||||
onClick={() =>
|
||||
setExcludePatterns((prev) =>
|
||||
prev.filter((x) => x !== p)
|
||||
|
|
@ -518,12 +518,12 @@ export default function CreateTaskDialog({
|
|||
</Badge>
|
||||
))}
|
||||
{excludePatterns.length === 0 && (
|
||||
<span className="text-xs text-gray-600 font-mono">无排除模式</span>
|
||||
<span className="text-xs text-muted-foreground font-mono">无排除模式</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<span className="text-xs text-gray-600 font-mono mr-1">快捷添加:</span>
|
||||
<span className="text-xs text-muted-foreground font-mono mr-1">快捷添加:</span>
|
||||
{[".test.", ".spec.", ".min.", "coverage/", "docs/", ".md"].map((pattern) => (
|
||||
<button
|
||||
key={pattern}
|
||||
|
|
@ -534,7 +534,7 @@ export default function CreateTaskDialog({
|
|||
setExcludePatterns((prev) => [...prev, pattern]);
|
||||
}
|
||||
}}
|
||||
className="text-xs font-mono px-1.5 py-0.5 border border-gray-700 bg-gray-800 hover:bg-gray-700 text-gray-400 hover:text-white disabled:opacity-40 disabled:cursor-not-allowed rounded"
|
||||
className="text-xs font-mono px-1.5 py-0.5 border border-border bg-muted hover:bg-muted text-muted-foreground hover:text-foreground disabled:opacity-40 disabled:cursor-not-allowed rounded"
|
||||
>
|
||||
+{pattern}
|
||||
</button>
|
||||
|
|
@ -565,12 +565,12 @@ export default function CreateTaskDialog({
|
|||
const canSelectFiles = isRepo || (isZip && useStored && hasStoredZip);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-3 border border-dashed border-gray-700 rounded bg-gray-900/30">
|
||||
<div className="flex items-center justify-between p-3 border border-dashed border-border rounded bg-muted/50">
|
||||
<div>
|
||||
<p className="font-mono text-xs uppercase font-bold text-gray-500">
|
||||
<p className="font-mono text-xs uppercase font-bold text-muted-foreground">
|
||||
扫描范围
|
||||
</p>
|
||||
<p className="text-sm font-bold text-white mt-1">
|
||||
<p className="text-sm font-bold text-foreground mt-1">
|
||||
{selectedFiles
|
||||
? `已选 ${selectedFiles.length} 个文件`
|
||||
: "全部文件"}
|
||||
|
|
@ -582,7 +582,7 @@ export default function CreateTaskDialog({
|
|||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setSelectedFiles(undefined)}
|
||||
className="h-8 text-xs text-rose-400 hover:bg-rose-900/30 hover:text-rose-300"
|
||||
className="h-8 text-xs text-rose-600 dark:text-rose-400 hover:bg-rose-100 dark:hover:bg-rose-900/30 hover:text-rose-700 dark:hover:text-rose-300"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
|
|
@ -608,12 +608,12 @@ export default function CreateTaskDialog({
|
|||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex-shrink-0 flex justify-end gap-3 px-5 py-4 bg-gray-900/50 border-t border-gray-800">
|
||||
<div className="flex-shrink-0 flex justify-end gap-3 px-5 py-4 bg-muted border-t border-border">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={creating}
|
||||
className="px-4 h-10 font-mono text-gray-400 hover:text-white hover:bg-gray-800"
|
||||
className="px-4 h-10 font-mono text-muted-foreground hover:text-foreground hover:bg-muted"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
|
|
@ -670,39 +670,39 @@ function ProjectCard({
|
|||
<div
|
||||
className={`flex items-center gap-3 p-3 cursor-pointer rounded transition-all ${selected
|
||||
? "bg-primary/10 border border-primary/50"
|
||||
: "hover:bg-gray-800/50 border border-transparent"
|
||||
: "hover:bg-muted border border-transparent"
|
||||
}`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selected}
|
||||
className="border-gray-600 data-[state=checked]:bg-primary data-[state=checked]:border-primary"
|
||||
className="border-border data-[state=checked]:bg-primary data-[state=checked]:border-primary"
|
||||
/>
|
||||
|
||||
<div className={`p-1.5 rounded ${isRepo ? "bg-blue-500/20" : "bg-amber-500/20"}`}>
|
||||
{isRepo ? (
|
||||
<Globe className="w-4 h-4 text-blue-400" />
|
||||
<Globe className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
) : (
|
||||
<Package className="w-4 h-4 text-amber-400" />
|
||||
<Package className="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 overflow-hidden">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`font-mono text-sm truncate ${selected ? 'text-white font-bold' : 'text-gray-300'}`}>
|
||||
<span className={`font-mono text-base truncate ${selected ? 'text-foreground font-bold' : 'text-foreground'}`}>
|
||||
{project.name}
|
||||
</span>
|
||||
<Badge
|
||||
className={`text-[10px] px-1 py-0 font-mono ${isRepo
|
||||
? "bg-blue-500/20 text-blue-400 border-blue-500/30"
|
||||
: "bg-amber-500/20 text-amber-400 border-amber-500/30"
|
||||
className={`text-xs px-1 py-0 font-mono ${isRepo
|
||||
? "bg-blue-500/20 text-blue-600 dark:text-blue-400 border-blue-500/30"
|
||||
: "bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30"
|
||||
}`}
|
||||
>
|
||||
{isRepo ? "REPO" : "ZIP"}
|
||||
</Badge>
|
||||
</div>
|
||||
{project.description && (
|
||||
<p className="text-xs text-gray-600 mt-0.5 font-mono line-clamp-2" title={project.description}>
|
||||
<p className="text-sm text-muted-foreground mt-0.5 font-mono line-clamp-2" title={project.description}>
|
||||
{project.description}
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -722,9 +722,9 @@ function ZipUploadCard({
|
|||
}) {
|
||||
if (zipState.loading) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 p-3 border border-gray-800 rounded bg-blue-950/20">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-blue-400" />
|
||||
<span className="text-sm font-mono text-blue-400">
|
||||
<div className="flex items-center gap-3 p-3 border border-border rounded bg-blue-50 dark:bg-blue-950/20">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-sm font-mono text-blue-600 dark:text-blue-400">
|
||||
检查文件中...
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -733,16 +733,16 @@ function ZipUploadCard({
|
|||
|
||||
if (zipState.storedZipInfo?.has_file) {
|
||||
return (
|
||||
<div className="p-3 border border-gray-800 rounded bg-emerald-950/20 space-y-3">
|
||||
<div className="p-3 border border-border rounded bg-emerald-50 dark:bg-emerald-950/20 space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-1.5 bg-emerald-500/20 rounded">
|
||||
<Package className="w-4 h-4 text-emerald-400" />
|
||||
<Package className="w-4 h-4 text-emerald-600 dark:text-emerald-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-bold text-emerald-300 font-mono">
|
||||
<p className="text-sm font-bold text-emerald-700 dark:text-emerald-300 font-mono">
|
||||
{zipState.storedZipInfo.original_filename}
|
||||
</p>
|
||||
<p className="text-xs text-emerald-500 font-mono">
|
||||
<p className="text-xs text-emerald-600 dark:text-emerald-500 font-mono">
|
||||
{zipState.storedZipInfo.file_size &&
|
||||
formatFileSize(zipState.storedZipInfo.file_size)}
|
||||
{zipState.storedZipInfo.uploaded_at &&
|
||||
|
|
@ -759,7 +759,7 @@ function ZipUploadCard({
|
|||
onChange={() => zipState.switchToStored()}
|
||||
className="w-4 h-4 accent-emerald-500"
|
||||
/>
|
||||
<span className="text-emerald-300">使用此文件</span>
|
||||
<span className="text-emerald-700 dark:text-emerald-300">使用此文件</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer font-mono text-sm">
|
||||
<input
|
||||
|
|
@ -768,7 +768,7 @@ function ZipUploadCard({
|
|||
onChange={() => zipState.switchToUpload()}
|
||||
className="w-4 h-4 accent-emerald-500"
|
||||
/>
|
||||
<span className="text-emerald-300">上传新文件</span>
|
||||
<span className="text-emerald-700 dark:text-emerald-300">上传新文件</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
|
@ -812,13 +812,13 @@ function ZipUploadCard({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="p-3 border border-dashed border-amber-500/50 rounded bg-amber-950/20">
|
||||
<div className="p-3 border border-dashed border-amber-500/50 rounded bg-amber-50 dark:bg-amber-950/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-1.5 bg-amber-500/20 rounded">
|
||||
<Upload className="w-4 h-4 text-amber-400" />
|
||||
<Upload className="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-bold text-amber-300 font-mono uppercase">
|
||||
<p className="text-sm font-bold text-amber-700 dark:text-amber-300 font-mono uppercase">
|
||||
上传 ZIP 文件
|
||||
</p>
|
||||
<div className="flex gap-2 items-center mt-2">
|
||||
|
|
@ -855,7 +855,7 @@ function ZipUploadCard({
|
|||
)}
|
||||
</div>
|
||||
{zipState.zipFile && (
|
||||
<p className="text-xs text-amber-400 mt-2 font-mono">
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400 mt-2 font-mono">
|
||||
已选: {zipState.zipFile.name} (
|
||||
{formatFileSize(zipState.zipFile.size)})
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import {
|
|||
RotateCcw,
|
||||
RefreshCw,
|
||||
Terminal,
|
||||
ChevronsUpDown,
|
||||
ChevronsDownUp,
|
||||
} from "lucide-react";
|
||||
import { api } from "@/shared/config/database";
|
||||
import { toast } from "sonner";
|
||||
|
|
@ -72,7 +74,7 @@ const getFileIcon = (path: string) => {
|
|||
if (configExts.includes(ext)) {
|
||||
return <FileJson className="w-4 h-4 text-amber-400" />;
|
||||
}
|
||||
return <File className="w-4 h-4 text-gray-500" />;
|
||||
return <File className="w-4 h-4 text-muted-foreground" />;
|
||||
};
|
||||
|
||||
// 获取文件扩展名
|
||||
|
|
@ -251,6 +253,21 @@ export default function FileSelectionDialog({
|
|||
});
|
||||
}, []);
|
||||
|
||||
const handleExpandAll = useCallback(() => {
|
||||
const folders = new Set<string>();
|
||||
filteredFiles.forEach((f) => {
|
||||
const parts = f.path.split("/");
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
folders.add(parts.slice(0, i).join("/"));
|
||||
}
|
||||
});
|
||||
setExpandedFolders(folders);
|
||||
}, [filteredFiles]);
|
||||
|
||||
const handleCollapseAll = useCallback(() => {
|
||||
setExpandedFolders(new Set());
|
||||
}, []);
|
||||
|
||||
const handleSelectAll = () => {
|
||||
setSelectedFiles(new Set(filteredFiles.map((f) => f.path)));
|
||||
};
|
||||
|
|
@ -326,7 +343,7 @@ export default function FileSelectionDialog({
|
|||
items.push(
|
||||
<div key={`folder-${folder.path}`}>
|
||||
<div
|
||||
className="flex items-center space-x-2 p-2 hover:bg-gray-800/50 border border-transparent hover:border-gray-700 cursor-pointer transition-colors rounded"
|
||||
className="flex items-center space-x-2 p-2 hover:bg-muted border border-transparent hover:border-border cursor-pointer transition-colors rounded"
|
||||
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
||||
>
|
||||
<button
|
||||
|
|
@ -334,12 +351,12 @@ export default function FileSelectionDialog({
|
|||
e.stopPropagation();
|
||||
handleExpandFolder(folder.path);
|
||||
}}
|
||||
className="p-0.5 hover:bg-gray-700 rounded"
|
||||
className="p-0.5 hover:bg-muted rounded"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-500" />
|
||||
<ChevronDown className="w-4 h-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-500" />
|
||||
<ChevronRight className="w-4 h-4 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
|
|
@ -352,7 +369,7 @@ export default function FileSelectionDialog({
|
|||
}
|
||||
}}
|
||||
onCheckedChange={() => handleToggleFolder(folder.path)}
|
||||
className="border-gray-600 data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=indeterminate]:bg-gray-500"
|
||||
className="border-border data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=indeterminate]:bg-background0"
|
||||
/>
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
|
|
@ -361,12 +378,12 @@ export default function FileSelectionDialog({
|
|||
<Folder className="w-4 h-4 text-amber-400" />
|
||||
)}
|
||||
<span
|
||||
className="text-sm font-mono font-medium flex-1 text-gray-300"
|
||||
className="text-sm font-mono font-medium flex-1 text-foreground"
|
||||
onClick={() => handleExpandFolder(folder.path)}
|
||||
>
|
||||
{folder.name}
|
||||
</span>
|
||||
<Badge className="cyber-badge-muted font-mono text-[10px]">
|
||||
<Badge className="cyber-badge-muted font-mono text-xs">
|
||||
{
|
||||
filteredFiles.filter((f) =>
|
||||
f.path.startsWith(folder.path + "/")
|
||||
|
|
@ -387,7 +404,7 @@ export default function FileSelectionDialog({
|
|||
items.push(
|
||||
<div
|
||||
key={`file-${file.path}`}
|
||||
className="flex items-center space-x-3 p-2 hover:bg-gray-800/50 border border-transparent hover:border-gray-700 cursor-pointer transition-colors rounded"
|
||||
className="flex items-center space-x-3 p-2 hover:bg-muted border border-transparent hover:border-border cursor-pointer transition-colors rounded"
|
||||
style={{ paddingLeft: `${depth * 16 + 32}px` }}
|
||||
onClick={() => handleToggleFile(file.path)}
|
||||
>
|
||||
|
|
@ -395,18 +412,18 @@ export default function FileSelectionDialog({
|
|||
<Checkbox
|
||||
checked={selectedFiles.has(file.path)}
|
||||
onCheckedChange={() => handleToggleFile(file.path)}
|
||||
className="border-gray-600 data-[state=checked]:bg-primary data-[state=checked]:border-primary"
|
||||
className="border-border data-[state=checked]:bg-primary data-[state=checked]:border-primary"
|
||||
/>
|
||||
</div>
|
||||
{getFileIcon(file.path)}
|
||||
<span
|
||||
className="text-sm font-mono flex-1 min-w-0 truncate text-gray-300"
|
||||
className="text-sm font-mono flex-1 min-w-0 truncate text-foreground"
|
||||
title={file.path}
|
||||
>
|
||||
{fileName}
|
||||
</span>
|
||||
{file.size > 0 && (
|
||||
<Badge className="cyber-badge-muted font-mono text-[10px] flex-shrink-0">
|
||||
<Badge className="cyber-badge-muted font-mono text-xs flex-shrink-0">
|
||||
{formatSize(file.size)}
|
||||
</Badge>
|
||||
)}
|
||||
|
|
@ -422,24 +439,24 @@ export default function FileSelectionDialog({
|
|||
return filteredFiles.map((file) => (
|
||||
<div
|
||||
key={file.path}
|
||||
className="flex items-center space-x-3 p-2 hover:bg-gray-800/50 border border-transparent hover:border-gray-700 cursor-pointer transition-colors rounded"
|
||||
className="flex items-center space-x-3 p-2 hover:bg-muted border border-transparent hover:border-border cursor-pointer transition-colors rounded"
|
||||
onClick={() => handleToggleFile(file.path)}
|
||||
>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
checked={selectedFiles.has(file.path)}
|
||||
onCheckedChange={() => handleToggleFile(file.path)}
|
||||
className="border-gray-600 data-[state=checked]:bg-primary data-[state=checked]:border-primary"
|
||||
className="border-border data-[state=checked]:bg-primary data-[state=checked]:border-primary"
|
||||
/>
|
||||
</div>
|
||||
{getFileIcon(file.path)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-mono truncate text-gray-300" title={file.path}>
|
||||
<p className="text-sm font-mono truncate text-foreground" title={file.path}>
|
||||
{file.path}
|
||||
</p>
|
||||
</div>
|
||||
{file.size > 0 && (
|
||||
<Badge className="cyber-badge-muted font-mono text-[10px] flex-shrink-0">
|
||||
<Badge className="cyber-badge-muted font-mono text-xs flex-shrink-0">
|
||||
{formatSize(file.size)}
|
||||
</Badge>
|
||||
)}
|
||||
|
|
@ -449,11 +466,11 @@ export default function FileSelectionDialog({
|
|||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="!max-w-[1000px] !w-[95vw] max-h-[85vh] flex flex-col cyber-card p-0 bg-[#0c0c12] !fixed">
|
||||
<DialogContent className="!max-w-[1000px] !w-[95vw] max-h-[85vh] flex flex-col cyber-card p-0 cyber-dialog !fixed">
|
||||
<DialogHeader className="cyber-card-header flex-shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<FolderOpen className="w-5 h-5 text-primary" />
|
||||
<DialogTitle className="text-lg font-bold uppercase tracking-wider text-white">
|
||||
<DialogTitle className="text-lg font-bold uppercase tracking-wider text-foreground">
|
||||
选择要审计的文件
|
||||
</DialogTitle>
|
||||
</div>
|
||||
|
|
@ -469,7 +486,7 @@ export default function FileSelectionDialog({
|
|||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* 搜索框 */}
|
||||
<div className="relative flex-1 min-w-[200px]">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 w-4 h-4" />
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
||||
<Input
|
||||
placeholder="搜索文件..."
|
||||
value={searchTerm}
|
||||
|
|
@ -481,11 +498,11 @@ export default function FileSelectionDialog({
|
|||
{/* 文件类型筛选 */}
|
||||
{fileTypes.length > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Filter className="w-4 h-4 text-gray-500" />
|
||||
<Filter className="w-4 h-4 text-muted-foreground" />
|
||||
<select
|
||||
value={filterType}
|
||||
onChange={(e) => setFilterType(e.target.value)}
|
||||
className="h-9 px-2 cyber-input font-mono text-sm bg-[#0a0a0f]"
|
||||
className="h-9 px-2 py-1 border border-border rounded font-mono text-xs cyber-bg-elevated text-foreground"
|
||||
>
|
||||
<option value="">全部类型</option>
|
||||
{fileTypes.slice(0, 10).map(([ext, count]) => (
|
||||
|
|
@ -498,20 +515,21 @@ export default function FileSelectionDialog({
|
|||
)}
|
||||
|
||||
{/* 视图切换 */}
|
||||
<div className="flex border border-gray-700 rounded overflow-hidden">
|
||||
<div className="flex border border-border rounded overflow-hidden">
|
||||
<button
|
||||
onClick={() => setViewMode("tree")}
|
||||
className={`px-3 py-1.5 text-xs font-mono uppercase ${viewMode === "tree" ? "bg-primary text-white" : "bg-gray-800 text-gray-400 hover:bg-gray-700"}`}
|
||||
className={`px-3 py-1.5 text-xs font-mono uppercase ${viewMode === "tree" ? "bg-primary text-foreground" : "bg-muted text-muted-foreground hover:bg-muted"}`}
|
||||
>
|
||||
树形
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode("flat")}
|
||||
className={`px-3 py-1.5 text-xs font-mono uppercase border-l border-gray-700 ${viewMode === "flat" ? "bg-primary text-white" : "bg-gray-800 text-gray-400 hover:bg-gray-700"}`}
|
||||
className={`px-3 py-1.5 text-xs font-mono uppercase border-l border-border ${viewMode === "flat" ? "bg-primary text-foreground" : "bg-muted text-muted-foreground hover:bg-muted"}`}
|
||||
>
|
||||
列表
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
|
|
@ -544,6 +562,28 @@ export default function FileSelectionDialog({
|
|||
<RefreshCw className="w-3 h-3 mr-1" />
|
||||
反选
|
||||
</Button>
|
||||
{viewMode === "tree" && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleExpandAll}
|
||||
className="h-8 px-3 cyber-btn-outline font-mono text-xs"
|
||||
>
|
||||
<ChevronDown className="w-3 h-3 mr-1" />
|
||||
展开
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCollapseAll}
|
||||
className="h-8 px-3 cyber-btn-outline font-mono text-xs"
|
||||
>
|
||||
<ChevronRight className="w-3 h-3 mr-1" />
|
||||
折叠
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{(searchTerm || filterType) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -552,14 +592,14 @@ export default function FileSelectionDialog({
|
|||
setSearchTerm("");
|
||||
setFilterType("");
|
||||
}}
|
||||
className="h-8 px-3 cyber-btn-outline font-mono text-xs text-gray-400"
|
||||
className="h-8 px-3 cyber-btn-outline font-mono text-xs text-muted-foreground"
|
||||
>
|
||||
<RotateCcw className="w-3 h-3 mr-1" />
|
||||
重置筛选
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm font-mono text-gray-500">
|
||||
<div className="text-sm font-mono text-muted-foreground">
|
||||
{searchTerm || filterType ? (
|
||||
<span>
|
||||
筛选: {filteredFiles.length}/{files.length} 个文件,
|
||||
|
|
@ -574,7 +614,7 @@ export default function FileSelectionDialog({
|
|||
</div>
|
||||
|
||||
{/* 文件列表 */}
|
||||
<div className="border border-gray-800 bg-[#0a0a0f] relative h-[450px] overflow-hidden rounded">
|
||||
<div className="border border-border cyber-bg-elevated relative h-[450px] overflow-hidden rounded">
|
||||
{loading ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="loading-spinner" />
|
||||
|
|
@ -588,7 +628,7 @@ export default function FileSelectionDialog({
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-gray-500">
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-muted-foreground">
|
||||
<FileText className="w-12 h-12 mb-2 opacity-20" />
|
||||
<p className="font-mono text-sm">
|
||||
{searchTerm || filterType
|
||||
|
|
@ -600,8 +640,8 @@ export default function FileSelectionDialog({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="p-5 border-t border-gray-800 bg-gray-900/50 flex-shrink-0 flex justify-between">
|
||||
<div className="text-xs font-mono text-gray-600 flex items-center gap-2">
|
||||
<DialogFooter className="p-5 border-t border-border bg-muted flex-shrink-0 flex justify-between">
|
||||
<div className="text-xs font-mono text-muted-foreground flex items-center gap-2">
|
||||
<Terminal className="w-3 h-3" />
|
||||
提示:点击文件夹可展开/折叠,点击文件夹复选框可批量选择
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -419,26 +419,26 @@ export default function TerminalProgressDialog({
|
|||
const getLogColor = (type: LogEntry["type"]) => {
|
||||
switch (type) {
|
||||
case "success":
|
||||
return "text-emerald-400";
|
||||
return "text-emerald-600 dark:text-emerald-400";
|
||||
case "error":
|
||||
return "text-rose-400";
|
||||
return "text-rose-600 dark:text-rose-400";
|
||||
case "warning":
|
||||
return "text-amber-400";
|
||||
return "text-amber-600 dark:text-amber-400";
|
||||
default:
|
||||
return "text-gray-400";
|
||||
return "text-muted-foreground";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogPortal>
|
||||
<DialogOverlay className="bg-black/85 backdrop-blur-md" />
|
||||
<DialogOverlay className="bg-black/50 dark:bg-black/85 backdrop-blur-md" />
|
||||
<DialogPrimitive.Content
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%]",
|
||||
"w-[95vw] max-w-[1000px] h-[85vh] max-h-[700px]",
|
||||
"bg-[#08090d] border border-[#1a2535] rounded overflow-hidden",
|
||||
"shadow-[0_0_60px_rgba(0,0,0,0.8),inset_0_1px_0_rgba(255,255,255,0.02)]",
|
||||
"bg-white dark:bg-[#08090d] border border-slate-200 dark:border-[#1a2535] rounded overflow-hidden",
|
||||
"shadow-xl dark:shadow-[0_0_60px_rgba(0,0,0,0.8),inset_0_1px_0_rgba(255,255,255,0.02)]",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
||||
|
|
@ -454,44 +454,43 @@ export default function TerminalProgressDialog({
|
|||
</DialogPrimitive.Description>
|
||||
</VisuallyHidden.Root>
|
||||
|
||||
{/* Scanline overlay */}
|
||||
<div className="absolute inset-0 pointer-events-none z-20 opacity-30"
|
||||
{/* Scanline overlay - only in dark mode */}
|
||||
<div className="absolute inset-0 pointer-events-none z-20 opacity-0 dark:opacity-30"
|
||||
style={{
|
||||
backgroundImage: "repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.1) 2px, rgba(0,0,0,0.1) 4px)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 bg-[#0a0c10] border-b border-[#1a2535]"
|
||||
style={{ backgroundImage: "linear-gradient(90deg, rgba(255, 95, 31, 0.05) 0%, transparent 50%, rgba(14, 181, 196, 0.05) 100%)" }}>
|
||||
<div className="flex items-center justify-between px-4 py-3 bg-slate-50 dark:cyber-bg-elevated border-b border-slate-200 dark:border-[#1a2535]">
|
||||
<div className="flex items-center gap-3">
|
||||
<Terminal className="w-5 h-5 text-primary" style={{ filter: "drop-shadow(0 0 8px rgba(255, 95, 31, 0.5))" }} />
|
||||
<Terminal className="w-5 h-5 text-primary" />
|
||||
<div>
|
||||
<span className="text-lg font-bold uppercase tracking-[0.15em] text-[#f0e6d3]" style={{ textShadow: "0 0 20px rgba(255, 95, 31, 0.3)" }}>AUDIT_TERMINAL</span>
|
||||
<span className="text-[10px] text-[#5a6577] ml-2 tracking-wider">v3.0</span>
|
||||
<span className="text-lg font-bold uppercase tracking-[0.15em] text-slate-800 dark:text-[#f0e6d3]">AUDIT_TERMINAL</span>
|
||||
<span className="text-xs text-slate-500 dark:text-[#5a6577] ml-2 tracking-wider">v3.0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{/* 状态指示灯 */}
|
||||
<div className="flex items-center gap-2.5 px-3 py-1.5 bg-[#060810] rounded border border-[#1a2535]">
|
||||
<div className="flex items-center gap-2.5 px-3 py-1.5 bg-slate-100 dark:bg-[#060810] rounded border border-slate-200 dark:border-[#1a2535]">
|
||||
<div className={`w-2.5 h-2.5 rounded-full transition-all duration-300 ${!isCompleted && !isFailed && !isCancelled
|
||||
? 'bg-[#3dd68c] shadow-[0_0_10px_rgba(61,214,140,0.7)] animate-pulse'
|
||||
: 'bg-[#3a4555]'}`} />
|
||||
? 'bg-emerald-500 dark:bg-[#3dd68c] shadow-[0_0_10px_rgba(61,214,140,0.7)] animate-pulse'
|
||||
: 'bg-slate-300 dark:bg-[#3a4555]'}`} />
|
||||
<div className={`w-2.5 h-2.5 rounded-full transition-all duration-300 ${isFailed
|
||||
? 'bg-[#f87171] shadow-[0_0_10px_rgba(248,113,113,0.7)]'
|
||||
: 'bg-[#3a4555]'}`} />
|
||||
? 'bg-rose-500 dark:bg-[#f87171] shadow-[0_0_10px_rgba(248,113,113,0.7)]'
|
||||
: 'bg-slate-300 dark:bg-[#3a4555]'}`} />
|
||||
<div className={`w-2.5 h-2.5 rounded-full transition-all duration-300 ${isCompleted
|
||||
? 'bg-[#22d3ee] shadow-[0_0_10px_rgba(34,211,238,0.7)]'
|
||||
: 'bg-[#3a4555]'}`} />
|
||||
? 'bg-cyan-500 dark:bg-[#22d3ee] shadow-[0_0_10px_rgba(34,211,238,0.7)]'
|
||||
: 'bg-slate-300 dark:bg-[#3a4555]'}`} />
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="w-8 h-8 flex items-center justify-center hover:bg-[#e53935]/20 rounded transition-all duration-200 group"
|
||||
className="w-8 h-8 flex items-center justify-center hover:bg-rose-100 dark:hover:bg-[#e53935]/20 rounded transition-all duration-200 group"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
<XIcon className="w-5 h-5 text-[#6a7587] group-hover:text-[#f87171] transition-colors" />
|
||||
<XIcon className="w-5 h-5 text-slate-500 dark:text-[#6a7587] group-hover:text-rose-500 dark:group-hover:text-[#f87171] transition-colors" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -499,22 +498,21 @@ export default function TerminalProgressDialog({
|
|||
{/* Main Content */}
|
||||
<div className="flex h-[calc(100%-56px)]">
|
||||
{/* Left Sidebar - Task Info */}
|
||||
<div className="w-48 p-4 border-r border-[#1a2535] bg-[#060810] flex flex-col gap-4">
|
||||
<div className="w-48 p-4 border-r border-slate-200 dark:border-[#1a2535] bg-slate-50 dark:bg-[#060810] flex flex-col gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-[9px] font-bold text-[#5a6577] uppercase tracking-[0.15em]">Task ID</div>
|
||||
<div className="text-xs font-mono text-primary truncate bg-[#0a0c10] p-2.5 rounded border border-[#1a2535]"
|
||||
style={{ textShadow: "0 0 10px rgba(255, 95, 31, 0.3)" }}>
|
||||
<div className="text-xs font-bold text-slate-500 dark:text-[#5a6577] uppercase tracking-[0.15em]">Task ID</div>
|
||||
<div className="text-xs font-mono text-primary truncate bg-white dark:cyber-bg-elevated p-2.5 rounded border border-slate-200 dark:border-[#1a2535]">
|
||||
{taskId?.slice(0, 8)}...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-[9px] font-bold text-[#5a6577] uppercase tracking-[0.15em]">Type</div>
|
||||
<div className="flex items-center gap-2 bg-[#0a0c10] p-2.5 rounded border border-[#1a2535]">
|
||||
<div className="text-xs font-bold text-slate-500 dark:text-[#5a6577] uppercase tracking-[0.15em]">Type</div>
|
||||
<div className="flex items-center gap-2 bg-white dark:cyber-bg-elevated p-2.5 rounded border border-slate-200 dark:border-[#1a2535]">
|
||||
{taskType === 'repository'
|
||||
? <Cpu className="w-3.5 h-3.5 text-[#22d3ee]" style={{ filter: "drop-shadow(0 0 6px rgba(34, 211, 238, 0.5))" }} />
|
||||
: <HardDrive className="w-3.5 h-3.5 text-[#fbbf24]" style={{ filter: "drop-shadow(0 0 6px rgba(251, 191, 36, 0.5))" }} />}
|
||||
<span className="text-xs font-bold text-[#d0d8e8] uppercase tracking-wider">{taskType}</span>
|
||||
? <Cpu className="w-3.5 h-3.5 text-cyan-600 dark:text-[#22d3ee]" />
|
||||
: <HardDrive className="w-3.5 h-3.5 text-amber-600 dark:text-[#fbbf24]" />}
|
||||
<span className="text-xs font-bold text-slate-700 dark:text-[#d0d8e8] uppercase tracking-wider">{taskType}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -522,7 +520,7 @@ export default function TerminalProgressDialog({
|
|||
|
||||
{/* Status Badge */}
|
||||
<div className="space-y-2">
|
||||
<div className="text-[9px] font-bold text-[#5a6577] uppercase tracking-[0.15em]">Status</div>
|
||||
<div className="text-xs font-bold text-slate-500 dark:text-[#5a6577] uppercase tracking-[0.15em]">Status</div>
|
||||
{isCancelled ? (
|
||||
<Badge className="w-full justify-center cyber-badge-warning">CANCELLED</Badge>
|
||||
) : isCompleted ? (
|
||||
|
|
@ -538,18 +536,17 @@ export default function TerminalProgressDialog({
|
|||
{/* Terminal Screen */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Terminal Output */}
|
||||
<div className="flex-1 bg-[#050608] p-4 overflow-y-auto font-mono text-sm custom-scrollbar relative">
|
||||
{/* Grid background */}
|
||||
<div className="absolute inset-0 cyber-grid-subtle pointer-events-none opacity-40" />
|
||||
<div className="flex-1 bg-slate-100 dark:bg-[#050608] p-4 overflow-y-auto font-mono text-sm custom-scrollbar relative">
|
||||
{/* Grid background - only in dark mode */}
|
||||
<div className="absolute inset-0 cyber-grid-subtle pointer-events-none opacity-0 dark:opacity-40" />
|
||||
|
||||
<div className="relative z-10 space-y-0.5 pb-10">
|
||||
{logs.map((log) => (
|
||||
<div key={log.id} className="flex items-start gap-3 hover:bg-[#ffffff]/[0.03] px-2 py-0.5 transition-colors group rounded">
|
||||
<span className="text-[#4a5565] text-xs flex-shrink-0 w-20 font-mono">
|
||||
<div key={log.id} className="flex items-start gap-3 hover:bg-slate-200/50 dark:hover:bg-[#ffffff]/[0.03] px-2 py-0.5 transition-colors group rounded">
|
||||
<span className="text-slate-500 dark:text-[#4a5565] text-xs flex-shrink-0 w-20 font-mono">
|
||||
{log.timestamp}
|
||||
</span>
|
||||
<span className={`${getLogColor(log.type)} flex-1 font-mono text-sm`}
|
||||
style={{ textShadow: log.type === 'success' ? '0 0 8px rgba(61, 214, 140, 0.3)' : log.type === 'error' ? '0 0 8px rgba(248, 113, 113, 0.3)' : log.type === 'warning' ? '0 0 8px rgba(251, 191, 36, 0.3)' : 'none' }}>
|
||||
<span className={`${getLogColor(log.type)} flex-1 font-mono text-sm`}>
|
||||
{log.message}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -557,8 +554,8 @@ export default function TerminalProgressDialog({
|
|||
|
||||
{!isCompleted && !isFailed && !isCancelled && (
|
||||
<div className="flex items-center gap-3 mt-4 px-2">
|
||||
<span className="text-[#4a5565] text-xs w-20 font-mono">{currentTime}</span>
|
||||
<span className="text-primary animate-pulse font-bold" style={{ textShadow: "0 0 10px rgba(255, 95, 31, 0.5)" }}>_</span>
|
||||
<span className="text-slate-500 dark:text-[#4a5565] text-xs w-20 font-mono">{currentTime}</span>
|
||||
<span className="text-primary animate-pulse font-bold">_</span>
|
||||
</div>
|
||||
)}
|
||||
<div ref={logsEndRef} />
|
||||
|
|
@ -566,8 +563,8 @@ export default function TerminalProgressDialog({
|
|||
</div>
|
||||
|
||||
{/* Bottom Controls */}
|
||||
<div className="h-14 px-4 border-t border-[#1a2535] bg-[#0a0c10]/90 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-xs text-[#6a7587] font-mono tracking-wide">
|
||||
<div className="h-14 px-4 border-t border-slate-200 dark:border-[#1a2535] bg-slate-50 dark:cyber-bg-elevated/90 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-600 dark:text-[#6a7587] font-mono tracking-wide">
|
||||
<Activity className="w-3.5 h-3.5" />
|
||||
<span>
|
||||
{isCompleted ? "TASK COMPLETED" : isFailed ? "TASK FAILED" : isCancelled ? "TASK CANCELLED" : "EXECUTING..."}
|
||||
|
|
@ -580,7 +577,7 @@ export default function TerminalProgressDialog({
|
|||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
className="h-8 bg-transparent border-[#fbbf24]/40 text-[#fbbf24] hover:bg-[#fbbf24]/10 hover:border-[#fbbf24]/60 font-mono uppercase tracking-wider text-[10px]"
|
||||
className="h-8 bg-transparent border-amber-500/40 text-amber-600 dark:text-[#fbbf24] hover:bg-amber-50 dark:hover:bg-[#fbbf24]/10 hover:border-amber-500/60 font-mono uppercase tracking-wider text-xs"
|
||||
>
|
||||
<AlertTriangle className="w-3 h-3 mr-1.5" />
|
||||
取消任务
|
||||
|
|
@ -592,7 +589,7 @@ export default function TerminalProgressDialog({
|
|||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => window.open('/logs', '_blank')}
|
||||
className="h-8 bg-transparent border-[#6a7587]/40 text-[#a8b0c0] hover:bg-[#1a2030]/50 hover:border-[#6a7587]/60 font-mono uppercase tracking-wider text-[10px]"
|
||||
className="h-8 bg-transparent border-slate-300 dark:border-[#6a7587]/40 text-slate-600 dark:text-[#a8b0c0] hover:bg-slate-100 dark:hover:bg-[#1a2030]/50 hover:border-slate-400 dark:hover:border-[#6a7587]/60 font-mono uppercase tracking-wider text-xs"
|
||||
>
|
||||
<Activity className="w-3 h-3 mr-1.5" />
|
||||
查看日志
|
||||
|
|
@ -603,7 +600,7 @@ export default function TerminalProgressDialog({
|
|||
<Button
|
||||
size="sm"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="h-8 cyber-btn-primary font-mono uppercase tracking-wider text-[10px]"
|
||||
className="h-8 cyber-btn-primary font-mono uppercase tracking-wider text-xs"
|
||||
>
|
||||
<CheckCircle2 className="w-3 h-3 mr-1.5" />
|
||||
确认
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default function AdvancedOptions({
|
|||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label className="text-base font-bold uppercase">扫描配置</Label>
|
||||
<p className="text-sm text-gray-500 mt-1 font-bold">
|
||||
<p className="text-sm text-muted-foreground mt-1 font-bold">
|
||||
配置代码扫描的详细参数
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -83,11 +83,11 @@ export default function AdvancedOptions({
|
|||
>
|
||||
<SelectTrigger
|
||||
id="analysis_depth"
|
||||
className="retro-input h-10 rounded-none border-2 border-black shadow-none focus:ring-0"
|
||||
className="retro-input h-10 rounded-none border-2 border-border shadow-none focus:ring-0"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-none border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<SelectContent className="rounded-none border-2 border-border shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<SelectItem value="basic" className="font-mono">
|
||||
基础 (快速)
|
||||
</SelectItem>
|
||||
|
|
@ -104,16 +104,16 @@ export default function AdvancedOptions({
|
|||
</div>
|
||||
|
||||
{/* 分析范围 */}
|
||||
<div className="space-y-2 border-t-2 border-dashed border-gray-300 pt-4">
|
||||
<div className="space-y-2 border-t-2 border-dashed border-border pt-4">
|
||||
<Label className="font-bold uppercase">分析范围</Label>
|
||||
<div className="flex items-center justify-between p-3 border-2 border-black bg-white">
|
||||
<div className="flex items-center justify-between p-3 border-2 border-border bg-background">
|
||||
<div>
|
||||
<p className="text-sm font-bold uppercase">
|
||||
{hasSelectedFiles
|
||||
? `已选择 ${scanConfig.file_paths!.length} 个文件`
|
||||
: "全量扫描 (所有文件)"}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 font-bold">
|
||||
<p className="text-xs text-muted-foreground font-bold">
|
||||
{hasSelectedFiles
|
||||
? "仅分析选中的文件"
|
||||
: "分析项目中的所有代码文件"}
|
||||
|
|
@ -126,7 +126,7 @@ export default function AdvancedOptions({
|
|||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onUpdate({ file_paths: undefined })}
|
||||
className="retro-btn bg-white text-red-600 hover:bg-red-50 h-8"
|
||||
className="retro-btn bg-background text-red-600 hover:bg-red-50 h-8"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
|
|
@ -136,7 +136,7 @@ export default function AdvancedOptions({
|
|||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onOpenFileSelection}
|
||||
className="retro-btn bg-white text-black hover:bg-gray-50 h-8"
|
||||
className="retro-btn bg-background text-foreground hover:bg-background h-8"
|
||||
>
|
||||
{hasSelectedFiles ? "修改选择" : "选择文件"}
|
||||
</Button>
|
||||
|
|
@ -162,15 +162,15 @@ function CheckboxOption({
|
|||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center space-x-3 p-3 border-2 border-black bg-white">
|
||||
<div className="flex items-center space-x-3 p-3 border-2 border-border bg-background">
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onCheckedChange={(c) => onChange(!!c)}
|
||||
className="rounded-none border-2 border-black data-[state=checked]:bg-primary data-[state=checked]:text-white"
|
||||
className="rounded-none border-2 border-border data-[state=checked]:bg-primary data-[state=checked]:text-foreground"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-bold uppercase">{label}</p>
|
||||
<p className="text-xs text-gray-500 font-bold">{description}</p>
|
||||
<p className="text-xs text-muted-foreground font-bold">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -178,7 +178,7 @@ function CheckboxOption({
|
|||
|
||||
function DepthExplanation() {
|
||||
return (
|
||||
<div className="bg-amber-50 border-2 border-black p-4 shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<div className="bg-amber-50 border-2 border-border p-4 shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<div className="flex items-start space-x-3">
|
||||
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
|
||||
<div className="text-sm font-mono">
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export default function ExcludePatterns({
|
|||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-base font-bold uppercase">排除模式</Label>
|
||||
<p className="text-sm text-gray-500 mt-1 font-bold">
|
||||
<p className="text-sm text-muted-foreground mt-1 font-bold">
|
||||
选择要从审计中排除的文件和目录模式
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -46,16 +46,16 @@ export default function ExcludePatterns({
|
|||
{COMMON_EXCLUDE_PATTERNS.map((pattern) => (
|
||||
<div
|
||||
key={pattern.value}
|
||||
className="flex items-center space-x-3 p-3 border-2 border-black bg-white hover:bg-gray-50 transition-all"
|
||||
className="flex items-center space-x-3 p-3 border-2 border-border bg-background hover:bg-background transition-all"
|
||||
>
|
||||
<Checkbox
|
||||
checked={patterns.includes(pattern.value)}
|
||||
onCheckedChange={() => onToggle(pattern.value)}
|
||||
className="rounded-none border-2 border-black data-[state=checked]:bg-primary data-[state=checked]:text-white"
|
||||
className="rounded-none border-2 border-border data-[state=checked]:bg-primary data-[state=checked]:text-foreground"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-bold uppercase">{pattern.label}</p>
|
||||
<p className="text-xs text-gray-500 font-bold">
|
||||
<p className="text-xs text-muted-foreground font-bold">
|
||||
{pattern.description}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -103,7 +103,7 @@ function CustomPatternInput({ onAdd }: { onAdd: (pattern: string) => void }) {
|
|||
.previousElementSibling as HTMLInputElement;
|
||||
handleAdd(input);
|
||||
}}
|
||||
className="retro-btn bg-white text-black h-10"
|
||||
className="retro-btn bg-background text-foreground h-10"
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
|
|
@ -127,7 +127,7 @@ function SelectedPatterns({
|
|||
<Badge
|
||||
key={pattern}
|
||||
variant="secondary"
|
||||
className="cursor-pointer hover:bg-red-100 hover:text-red-800 rounded-none border-2 border-black bg-gray-100 text-black font-mono font-bold"
|
||||
className="cursor-pointer hover:bg-red-100 hover:text-red-800 rounded-none border-2 border-border bg-muted text-foreground font-mono font-bold"
|
||||
onClick={() => onRemove(pattern)}
|
||||
>
|
||||
{pattern} ×
|
||||
|
|
|
|||
|
|
@ -39,14 +39,14 @@ export default function ProjectSelector({
|
|||
</Label>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs rounded-none border-black font-mono"
|
||||
className="text-xs rounded-none border-border font-mono"
|
||||
>
|
||||
{filteredProjects.length} 个可用项目
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-black w-4 h-4" />
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-foreground w-4 h-4" />
|
||||
<Input
|
||||
placeholder="搜索项目名称..."
|
||||
value={searchTerm}
|
||||
|
|
@ -91,7 +91,7 @@ function ProjectCard({
|
|||
className={`cursor-pointer transition-all border-2 p-4 relative ${
|
||||
isSelected
|
||||
? "border-primary bg-blue-50 shadow-[4px_4px_0px_0px_rgba(37,99,235,1)] translate-x-[-2px] translate-y-[-2px]"
|
||||
: "border-black bg-white hover:bg-gray-50 hover:shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:translate-x-[-2px] hover:translate-y-[-2px]"
|
||||
: "border-border bg-background hover:bg-background hover:shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:translate-x-[-2px] hover:translate-y-[-2px]"
|
||||
}`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
|
|
@ -101,11 +101,11 @@ function ProjectCard({
|
|||
{project.name}
|
||||
</h4>
|
||||
{project.description && (
|
||||
<p className="text-xs text-gray-600 mt-1 line-clamp-2 font-mono">
|
||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-2 font-mono">
|
||||
{project.description}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center space-x-4 mt-2 text-xs text-gray-500 font-mono font-bold">
|
||||
<div className="flex items-center space-x-4 mt-2 text-xs text-muted-foreground font-mono font-bold">
|
||||
<span
|
||||
className={`px-1.5 py-0.5 ${isRepo ? "bg-blue-100 text-blue-700" : "bg-amber-100 text-amber-700"}`}
|
||||
>
|
||||
|
|
@ -122,8 +122,8 @@ function ProjectCard({
|
|||
</div>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className="w-5 h-5 bg-primary border-2 border-black flex items-center justify-center">
|
||||
<div className="w-2 h-2 bg-white" />
|
||||
<div className="w-5 h-5 bg-primary border-2 border-border flex items-center justify-center">
|
||||
<div className="w-2 h-2 bg-background" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -141,7 +141,7 @@ function LoadingSpinner() {
|
|||
|
||||
function EmptyState({ hasSearch }: { hasSearch: boolean }) {
|
||||
return (
|
||||
<div className="col-span-2 text-center py-8 text-gray-500 font-mono">
|
||||
<div className="col-span-2 text-center py-8 text-muted-foreground font-mono">
|
||||
<FileText className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">
|
||||
{hasSearch ? "未找到匹配的项目" : "暂无可用项目"}
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ export function DatabaseManager() {
|
|||
<div className="cyber-card p-0">
|
||||
<div className="cyber-card-header">
|
||||
<Activity className="w-5 h-5 text-emerald-400" />
|
||||
<h3 className="text-lg font-bold uppercase tracking-wider text-white">数据库健康检查</h3>
|
||||
<h3 className="text-lg font-bold uppercase tracking-wider text-foreground">数据库健康检查</h3>
|
||||
<div className="ml-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -238,16 +238,16 @@ export function DatabaseManager() {
|
|||
<AlertCircle className="h-5 w-5 text-rose-400" />
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold uppercase text-sm text-gray-400">状态:</span>
|
||||
<span className="font-bold uppercase text-sm text-muted-foreground">状态:</span>
|
||||
{getHealthStatusBadge(health.status)}
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
数据库连接:
|
||||
<span className={health.database_connected ? 'text-emerald-400' : 'text-rose-400'}>
|
||||
{health.database_connected ? '正常' : '异常'}
|
||||
</span>
|
||||
<span className="mx-2">|</span>
|
||||
总记录数:<span className="text-white">{health.total_records.toLocaleString()}</span>
|
||||
总记录数:<span className="text-foreground">{health.total_records.toLocaleString()}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -292,7 +292,7 @@ export function DatabaseManager() {
|
|||
<div className="cyber-card p-0">
|
||||
<div className="cyber-card-header">
|
||||
<Database className="w-5 h-5 text-violet-400" />
|
||||
<h3 className="text-lg font-bold uppercase tracking-wider text-white">详细数据统计</h3>
|
||||
<h3 className="text-lg font-bold uppercase tracking-wider text-foreground">详细数据统计</h3>
|
||||
<div className="ml-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -321,17 +321,17 @@ export function DatabaseManager() {
|
|||
<div className="cyber-card p-4">
|
||||
<p className="stat-label">任务</p>
|
||||
<p className="stat-value text-emerald-400">{stats.total_tasks}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">完成: {stats.completed_tasks} | 进行中: {stats.running_tasks}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">完成: {stats.completed_tasks} | 进行中: {stats.running_tasks}</p>
|
||||
</div>
|
||||
<div className="cyber-card p-4">
|
||||
<p className="stat-label">问题</p>
|
||||
<p className="stat-value text-amber-400">{stats.total_issues}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">未解决: {stats.open_issues} | 已解决: {stats.resolved_issues}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">未解决: {stats.open_issues} | 已解决: {stats.resolved_issues}</p>
|
||||
</div>
|
||||
<div className="cyber-card p-4">
|
||||
<p className="stat-label">分析记录</p>
|
||||
<p className="stat-value text-violet-400">{stats.total_analyses}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">即时分析</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">即时分析</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -347,7 +347,7 @@ export function DatabaseManager() {
|
|||
<div className="cyber-card p-0">
|
||||
<div className="cyber-card-header">
|
||||
<Database className="w-5 h-5 text-primary" />
|
||||
<h3 className="text-lg font-bold uppercase tracking-wider text-white">数据操作</h3>
|
||||
<h3 className="text-lg font-bold uppercase tracking-wider text-foreground">数据操作</h3>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
{message && (
|
||||
|
|
@ -369,11 +369,11 @@ export function DatabaseManager() {
|
|||
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-bold uppercase text-gray-300 flex items-center gap-2">
|
||||
<h4 className="text-sm font-bold uppercase text-foreground flex items-center gap-2">
|
||||
<Download className="h-4 w-4 text-sky-400" />
|
||||
导出数据
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500">将数据导出为 JSON 文件,用于备份或迁移</p>
|
||||
<p className="text-xs text-muted-foreground">将数据导出为 JSON 文件,用于备份或迁移</p>
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
disabled={loading}
|
||||
|
|
@ -385,11 +385,11 @@ export function DatabaseManager() {
|
|||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-bold uppercase text-gray-300 flex items-center gap-2">
|
||||
<h4 className="text-sm font-bold uppercase text-foreground flex items-center gap-2">
|
||||
<Upload className="h-4 w-4 text-emerald-400" />
|
||||
导入数据
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500">从 JSON 文件恢复数据(最大 50MB)</p>
|
||||
<p className="text-xs text-muted-foreground">从 JSON 文件恢复数据(最大 50MB)</p>
|
||||
<Button
|
||||
onClick={() => document.getElementById('import-file')?.click()}
|
||||
disabled={loading}
|
||||
|
|
@ -412,7 +412,7 @@ export function DatabaseManager() {
|
|||
<Trash2 className="h-4 w-4" />
|
||||
清空数据
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500">删除所有数据(不可恢复)</p>
|
||||
<p className="text-xs text-muted-foreground">删除所有数据(不可恢复)</p>
|
||||
<Button
|
||||
onClick={handleClear}
|
||||
disabled={loading}
|
||||
|
|
@ -424,7 +424,7 @@ export function DatabaseManager() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-6 border-t border-gray-800 border-dashed">
|
||||
<div className="pt-6 border-t border-border border-dashed">
|
||||
<div className="bg-sky-500/10 border border-sky-500/30 p-4 flex items-start gap-3 rounded-lg">
|
||||
<Info className="h-5 w-5 text-sky-400 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-sm text-sky-300/80">
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ export default function DatabaseTest() {
|
|||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-600">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
测试数据库连接状态和基本功能
|
||||
</p>
|
||||
<Button
|
||||
|
|
@ -188,7 +188,7 @@ export default function DatabaseTest() {
|
|||
{getStatusIcon(result.status)}
|
||||
<div>
|
||||
<p className="font-medium text-sm">{result.name}</p>
|
||||
<p className="text-xs text-gray-500">{result.message}</p>
|
||||
<p className="text-xs text-muted-foreground">{result.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
{getStatusBadge(result.status)}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
/**
|
||||
* Sidebar Component
|
||||
* Cyberpunk Terminal Aesthetic
|
||||
* Premium Terminal Aesthetic with Enhanced Visual Design
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ThemeToggle } from "@/components/ui/theme-toggle";
|
||||
import {
|
||||
Menu,
|
||||
X,
|
||||
|
|
@ -22,12 +23,12 @@ import {
|
|||
Shield,
|
||||
MessageSquare,
|
||||
Bot,
|
||||
Terminal
|
||||
ExternalLink,
|
||||
} from "lucide-react";
|
||||
import routes from "@/app/routes";
|
||||
import { version } from "../../../package.json";
|
||||
|
||||
// Icon mapping for routes
|
||||
// Icon mapping for routes with consistent sizing
|
||||
const routeIcons: Record<string, React.ReactNode> = {
|
||||
"/": <Bot className="w-5 h-5" />,
|
||||
"/dashboard": <LayoutDashboard className="w-5 h-5" />,
|
||||
|
|
@ -57,7 +58,12 @@ export default function Sidebar({ collapsed, setCollapsed }: SidebarProps) {
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="fixed top-4 left-4 z-50 md:hidden bg-[#0c0c12] border border-gray-800 text-gray-300 hover:bg-gray-800 hover:text-white"
|
||||
className="fixed top-4 left-4 z-50 md:hidden"
|
||||
style={{
|
||||
background: 'var(--cyber-bg)',
|
||||
border: '1px solid var(--cyber-border)',
|
||||
color: 'var(--cyber-text-muted)'
|
||||
}}
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
>
|
||||
{mobileOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
|
||||
|
|
@ -74,91 +80,106 @@ export default function Sidebar({ collapsed, setCollapsed }: SidebarProps) {
|
|||
{/* Sidebar */}
|
||||
<aside
|
||||
className={`
|
||||
fixed top-0 left-0 h-screen
|
||||
bg-[#0a0a0f] border-r border-gray-800/60
|
||||
z-40 transition-all duration-300 ease-in-out
|
||||
fixed top-0 left-0 h-screen z-40 transition-all duration-300 ease-in-out
|
||||
${collapsed ? "w-20" : "w-64"}
|
||||
${mobileOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"}
|
||||
`}
|
||||
style={{
|
||||
background: 'var(--cyber-bg)',
|
||||
borderRight: '1px solid var(--cyber-border)'
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col h-full relative">
|
||||
{/* Subtle gradient background */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-primary/5 via-transparent to-transparent pointer-events-none" />
|
||||
|
||||
{/* Subtle grid background */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-30 pointer-events-none"
|
||||
className="absolute inset-0 opacity-20 pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(255,107,44,0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,107,44,0.03) 1px, transparent 1px)
|
||||
linear-gradient(var(--cyber-border-accent) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--cyber-border-accent) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: '24px 24px',
|
||||
backgroundSize: '32px 32px',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Logo Section */}
|
||||
<div className={`
|
||||
relative flex items-center h-[72px]
|
||||
border-b border-gray-800/60 bg-[#0c0c12]
|
||||
${collapsed ? 'px-3 justify-center' : 'px-4 pr-6'}
|
||||
`}>
|
||||
{/* Right edge glow */}
|
||||
<div className="absolute top-0 right-0 bottom-0 w-px bg-gradient-to-b from-primary/30 via-primary/10 to-primary/30 pointer-events-none" />
|
||||
|
||||
{/* Logo Section with enhanced styling */}
|
||||
<div
|
||||
className={`relative flex items-center h-[72px] ${collapsed ? 'px-3 justify-center' : 'px-5 pr-6'}`}
|
||||
style={{
|
||||
background: 'var(--cyber-bg-elevated)',
|
||||
borderBottom: '1px solid var(--cyber-border)'
|
||||
}}
|
||||
>
|
||||
{/* Bottom accent line */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-primary/40 via-primary/20 to-transparent" />
|
||||
|
||||
<Link
|
||||
to="/"
|
||||
className={`
|
||||
flex items-center gap-3 group transition-all duration-300
|
||||
${collapsed ? 'justify-center' : 'flex-1 min-w-0'}
|
||||
`}
|
||||
className={`flex items-center gap-3 group transition-all duration-300 ${collapsed ? 'justify-center' : 'flex-1 min-w-0'}`}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
>
|
||||
{/* Logo Icon */}
|
||||
{/* Logo Icon with enhanced styling */}
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className="w-10 h-10 bg-[#0a0a0f] border border-primary/30 rounded-lg flex items-center justify-center overflow-hidden group-hover:border-primary/60 transition-colors">
|
||||
<div
|
||||
className="w-11 h-11 rounded-xl flex items-center justify-center overflow-hidden transition-all duration-300 group-hover:shadow-[0_0_20px_rgba(255,107,44,0.3)]"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, hsl(var(--primary) / 0.15), hsl(var(--primary) / 0.05))',
|
||||
border: '1px solid hsl(var(--primary) / 0.4)'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/logo_deepaudit.png"
|
||||
alt="DeepAudit"
|
||||
className="w-7 h-7 object-contain"
|
||||
className="w-7 h-7 object-contain transition-transform duration-300 group-hover:scale-110"
|
||||
/>
|
||||
</div>
|
||||
{/* Glow effect */}
|
||||
<div className="absolute inset-0 bg-primary/20 rounded-lg blur-xl opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="absolute inset-0 bg-primary/30 rounded-xl blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
</div>
|
||||
|
||||
{/* Logo Text */}
|
||||
<div className={`
|
||||
transition-all duration-300
|
||||
${collapsed ? 'w-0 opacity-0 overflow-hidden' : 'flex-1 min-w-0 opacity-100'}
|
||||
`}>
|
||||
{/* Logo Text with enhanced styling */}
|
||||
<div className={`transition-all duration-300 ${collapsed ? 'w-0 opacity-0 overflow-hidden' : 'flex-1 min-w-0 opacity-100'}`}>
|
||||
<div
|
||||
className="text-xl font-bold tracking-wider font-mono"
|
||||
style={{ textShadow: '0 0 20px rgba(255,107,44,0.3)' }}
|
||||
className="text-xl font-bold tracking-wider font-mono leading-tight"
|
||||
style={{ textShadow: '0 0 25px rgba(255,107,44,0.4)' }}
|
||||
>
|
||||
<span className="text-primary">DEEP</span>
|
||||
<span className="text-white">AUDIT</span>
|
||||
<span style={{ color: 'var(--cyber-text)' }}>AUDIT</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground tracking-[0.15em] uppercase mt-0.5">
|
||||
Security Agent
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Collapse button */}
|
||||
{/* Collapse button with enhanced styling */}
|
||||
<button
|
||||
className={`
|
||||
hidden md:flex absolute -right-3 top-1/2 -translate-y-1/2
|
||||
w-6 h-6 bg-[#0c0c12] border border-gray-700 rounded
|
||||
items-center justify-center text-gray-500
|
||||
hover:bg-primary hover:border-primary hover:text-white
|
||||
transition-all duration-200
|
||||
`}
|
||||
className="hidden md:flex absolute -right-3 top-1/2 -translate-y-1/2 w-6 h-6 rounded-md items-center justify-center hover:bg-primary hover:border-primary hover:text-white transition-all duration-300 shadow-sm"
|
||||
style={{
|
||||
background: 'var(--cyber-bg)',
|
||||
border: '1px solid var(--cyber-border)',
|
||||
color: 'var(--cyber-text-muted)',
|
||||
zIndex: 100
|
||||
}}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
style={{ zIndex: 100 }}
|
||||
>
|
||||
{collapsed ? (
|
||||
<ChevronRight className="w-3 h-3" />
|
||||
<ChevronRight className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<ChevronLeft className="w-3 h-3" />
|
||||
<ChevronLeft className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 overflow-y-auto py-4 px-3 custom-scrollbar">
|
||||
<div className="space-y-1">
|
||||
{/* Navigation with enhanced styling */}
|
||||
<nav className="flex-1 overflow-y-auto py-4 px-3 custom-scrollbar relative">
|
||||
<div className="space-y-1.5">
|
||||
{visibleRoutes.map((route) => {
|
||||
const isActive = location.pathname === route.path;
|
||||
return (
|
||||
|
|
@ -166,43 +187,52 @@ export default function Sidebar({ collapsed, setCollapsed }: SidebarProps) {
|
|||
key={route.path}
|
||||
to={route.path}
|
||||
className={`
|
||||
flex items-center gap-3 px-3 py-2.5
|
||||
transition-all duration-200 group relative rounded-lg
|
||||
flex items-center gap-3 px-3 py-2.5 transition-all duration-300 group relative rounded-lg
|
||||
${isActive
|
||||
? "bg-primary/15 text-primary border border-primary/30"
|
||||
: "text-gray-400 hover:text-gray-200 hover:bg-gray-800/50 border border-transparent"
|
||||
? 'bg-primary/15 border border-primary/40 shadow-[0_0_15px_rgba(255,107,44,0.1)]'
|
||||
: 'border border-transparent hover:bg-card/60 hover:border-border/50'
|
||||
}
|
||||
`}
|
||||
style={{
|
||||
color: isActive ? 'hsl(var(--primary))' : 'var(--cyber-text-muted)'
|
||||
}}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
title={collapsed ? route.name : undefined}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isActive) {
|
||||
e.currentTarget.style.color = 'var(--cyber-text)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isActive) {
|
||||
e.currentTarget.style.color = 'var(--cyber-text-muted)';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Active indicator */}
|
||||
{/* Active indicator with glow */}
|
||||
{isActive && (
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-primary rounded-r" />
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-7 bg-primary rounded-r shadow-[0_0_8px_rgba(255,107,44,0.5)]" />
|
||||
)}
|
||||
|
||||
{/* Icon */}
|
||||
{/* Icon with background on active */}
|
||||
<span className={`
|
||||
flex-shrink-0 transition-colors duration-200
|
||||
${isActive ? "text-primary" : "text-gray-500 group-hover:text-gray-300"}
|
||||
flex-shrink-0 transition-all duration-300 p-1.5 rounded-md
|
||||
${isActive ? 'bg-primary/20' : 'group-hover:bg-muted/50'}
|
||||
`}>
|
||||
{routeIcons[route.path] || <LayoutDashboard className="w-5 h-5" />}
|
||||
</span>
|
||||
|
||||
{/* Label */}
|
||||
{!collapsed && (
|
||||
<span className={`
|
||||
font-mono text-sm tracking-wide
|
||||
${isActive ? 'font-semibold' : 'font-medium'}
|
||||
`}>
|
||||
<span className={`font-mono text-sm tracking-wide transition-all duration-300 ${isActive ? 'font-semibold' : 'font-medium'}`}>
|
||||
{route.name}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Hover arrow */}
|
||||
{/* Hover indicator */}
|
||||
{!isActive && !collapsed && (
|
||||
<span className="absolute right-3 opacity-0 group-hover:opacity-100 text-xs text-primary transition-opacity">
|
||||
→
|
||||
<span className="absolute right-3 opacity-0 group-hover:opacity-100 transition-all duration-300 group-hover:translate-x-1">
|
||||
<ChevronRight className="w-4 h-4 text-primary" />
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
|
|
@ -211,63 +241,97 @@ export default function Sidebar({ collapsed, setCollapsed }: SidebarProps) {
|
|||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-3 border-t border-gray-800/60 bg-[#0c0c12] space-y-1">
|
||||
{/* Account Link */}
|
||||
{/* Footer with enhanced styling */}
|
||||
<div
|
||||
className="p-3 space-y-1.5 relative"
|
||||
style={{
|
||||
background: 'var(--cyber-bg-elevated)',
|
||||
borderTop: '1px solid var(--cyber-border)'
|
||||
}}
|
||||
>
|
||||
{/* Top accent line */}
|
||||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-primary/20 to-transparent" />
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<ThemeToggle collapsed={collapsed} />
|
||||
|
||||
{/* Account Link with enhanced styling */}
|
||||
<Link
|
||||
to="/account"
|
||||
className={`
|
||||
flex items-center gap-3 px-3 py-2.5 rounded-lg
|
||||
transition-all duration-200 group
|
||||
flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-300 group
|
||||
${location.pathname === '/account'
|
||||
? "bg-primary/15 text-primary border border-primary/30"
|
||||
: "text-gray-400 hover:text-gray-200 hover:bg-gray-800/50 border border-transparent"
|
||||
? 'bg-primary/15 border border-primary/40'
|
||||
: 'border border-transparent hover:bg-card/60 hover:border-border/50'
|
||||
}
|
||||
`}
|
||||
style={{
|
||||
color: location.pathname === '/account' ? 'hsl(var(--primary))' : 'var(--cyber-text-muted)'
|
||||
}}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
title={collapsed ? "账号管理" : undefined}
|
||||
>
|
||||
<UserCircle className={`w-5 h-5 flex-shrink-0 ${
|
||||
location.pathname === '/account' ? 'text-primary' : 'text-gray-500 group-hover:text-gray-300'
|
||||
}`} />
|
||||
<span className={`p-1.5 rounded-md transition-all duration-300 ${location.pathname === '/account' ? 'bg-primary/20' : 'group-hover:bg-muted/50'}`}>
|
||||
<UserCircle className="w-5 h-5 flex-shrink-0" />
|
||||
</span>
|
||||
{!collapsed && (
|
||||
<span className="font-mono text-sm">账号管理</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* GitHub Link */}
|
||||
{/* GitHub Link with enhanced styling */}
|
||||
<a
|
||||
href="https://github.com/lintsinghua/DeepAudit"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`
|
||||
flex items-center gap-3 px-3 py-2.5 rounded-lg
|
||||
text-gray-400 hover:text-gray-200 hover:bg-gray-800/50
|
||||
transition-all duration-200 group border border-transparent
|
||||
`}
|
||||
className="flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-300 group border border-transparent hover:bg-card/60 hover:border-border/50"
|
||||
style={{ color: 'var(--cyber-text-muted)' }}
|
||||
title={collapsed ? "GitHub" : undefined}
|
||||
>
|
||||
<Github className="w-5 h-5 flex-shrink-0 text-gray-500 group-hover:text-gray-300" />
|
||||
<span className="p-1.5 rounded-md transition-all duration-300 group-hover:bg-muted/50">
|
||||
<Github className="w-5 h-5 flex-shrink-0" />
|
||||
</span>
|
||||
{!collapsed && (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-mono text-sm">GitHub</span>
|
||||
<span className="text-[10px] text-gray-600 font-mono">v{version}</span>
|
||||
<div className="flex-1 flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-mono text-sm">GitHub</span>
|
||||
<span className="text-xs font-mono text-muted-foreground/70">v{version}</span>
|
||||
</div>
|
||||
<ExternalLink className="w-3.5 h-3.5 opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
|
||||
{/* System Status */}
|
||||
{/* System Status with enhanced styling */}
|
||||
{!collapsed && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-800/50">
|
||||
<div className="flex items-center gap-2 px-3 py-2">
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"
|
||||
style={{ boxShadow: '0 0 8px rgba(52, 211, 153, 0.5)' }} />
|
||||
<span className="text-[10px] text-gray-600 font-mono uppercase tracking-wider">
|
||||
<div className="mt-3 pt-3 relative" style={{ borderTop: '1px solid var(--cyber-border)' }}>
|
||||
<div className="flex items-center gap-2.5 px-3 py-2 rounded-lg bg-emerald-500/10 border border-emerald-500/20">
|
||||
<div className="relative">
|
||||
<div
|
||||
className="w-2.5 h-2.5 rounded-full bg-emerald-400"
|
||||
style={{ boxShadow: '0 0 10px rgba(52, 211, 153, 0.6)' }}
|
||||
/>
|
||||
<div className="absolute inset-0 w-2.5 h-2.5 rounded-full bg-emerald-400 animate-ping opacity-50" />
|
||||
</div>
|
||||
<span className="text-xs font-mono uppercase tracking-wider text-emerald-500">
|
||||
System Online
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Collapsed system status indicator */}
|
||||
{collapsed && (
|
||||
<div className="flex justify-center py-2">
|
||||
<div className="relative">
|
||||
<div
|
||||
className="w-2.5 h-2.5 rounded-full bg-emerald-400"
|
||||
style={{ boxShadow: '0 0 10px rgba(52, 211, 153, 0.6)' }}
|
||||
/>
|
||||
<div className="absolute inset-0 w-2.5 h-2.5 rounded-full bg-emerald-400 animate-ping opacity-50" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
|
|
|||
|
|
@ -62,13 +62,13 @@ export default function ExportReportDialog({
|
|||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px] bg-[#0c0c12] border-gray-700">
|
||||
<DialogContent className="sm:max-w-[600px] cyber-dialog border-border">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-3 text-lg font-bold uppercase tracking-wider text-white">
|
||||
<DialogTitle className="flex items-center gap-3 text-lg font-bold uppercase tracking-wider text-foreground">
|
||||
<Download className="w-5 h-5 text-primary" />
|
||||
导出审计报告
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-400 font-mono text-xs">
|
||||
<DialogDescription className="text-muted-foreground font-mono text-xs">
|
||||
选择报告格式并导出完整的代码审计结果
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
|
@ -79,57 +79,57 @@ export default function ExportReportDialog({
|
|||
onValueChange={(value) => setSelectedFormat(value as ExportFormat)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="flex items-center space-x-3 p-4 border border-gray-700 rounded bg-gray-900/30 cursor-pointer hover:bg-gray-800/50">
|
||||
<div className="flex items-center space-x-3 p-4 border border-border rounded bg-muted/50 cursor-pointer hover:bg-muted">
|
||||
<RadioGroupItem value="json" id="json" />
|
||||
<Label htmlFor="json" className="flex items-center gap-3 cursor-pointer flex-1">
|
||||
<FileJson className="w-5 h-5 text-amber-400" />
|
||||
<div>
|
||||
<div className="font-bold text-gray-200">JSON 格式</div>
|
||||
<div className="text-xs text-gray-500">结构化数据,适合程序处理和集成</div>
|
||||
<div className="font-bold text-foreground">JSON 格式</div>
|
||||
<div className="text-xs text-muted-foreground">结构化数据,适合程序处理和集成</div>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 p-4 border border-gray-700 rounded bg-gray-900/30 cursor-pointer hover:bg-gray-800/50">
|
||||
<div className="flex items-center space-x-3 p-4 border border-border rounded bg-muted/50 cursor-pointer hover:bg-muted">
|
||||
<RadioGroupItem value="pdf" id="pdf" />
|
||||
<Label htmlFor="pdf" className="flex items-center gap-3 cursor-pointer flex-1">
|
||||
<FileText className="w-5 h-5 text-rose-400" />
|
||||
<div>
|
||||
<div className="font-bold text-gray-200">PDF 格式</div>
|
||||
<div className="text-xs text-gray-500">专业报告,适合打印和分享</div>
|
||||
<div className="font-bold text-foreground">PDF 格式</div>
|
||||
<div className="text-xs text-muted-foreground">专业报告,适合打印和分享</div>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
{/* 报告预览信息 */}
|
||||
<div className="mt-6 border border-gray-700 rounded bg-gray-900/30">
|
||||
<div className="px-4 py-2 border-b border-gray-800 bg-gray-900/50 flex items-center gap-2">
|
||||
<div className="mt-6 border border-border rounded bg-muted/50">
|
||||
<div className="px-4 py-2 border-b border-border bg-muted flex items-center gap-2">
|
||||
<Terminal className="w-3 h-3 text-primary" />
|
||||
<h4 className="font-bold text-gray-300 uppercase text-xs">报告内容预览</h4>
|
||||
<h4 className="font-bold text-foreground uppercase text-xs">报告内容预览</h4>
|
||||
</div>
|
||||
<div className="p-4 grid grid-cols-2 gap-3 text-xs font-mono">
|
||||
<div className="flex items-center justify-between border-b border-gray-800 pb-2">
|
||||
<span className="text-gray-600">项目名称:</span>
|
||||
<span className="font-bold text-white">{task.project?.name || "未知"}</span>
|
||||
<div className="flex items-center justify-between border-b border-border pb-2">
|
||||
<span className="text-muted-foreground">项目名称:</span>
|
||||
<span className="font-bold text-foreground">{task.project?.name || "未知"}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-gray-800 pb-2">
|
||||
<span className="text-gray-600">质量评分:</span>
|
||||
<div className="flex items-center justify-between border-b border-border pb-2">
|
||||
<span className="text-muted-foreground">质量评分:</span>
|
||||
<span className="font-bold text-emerald-400">{task.quality_score.toFixed(1)}/100</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-gray-800 pb-2">
|
||||
<span className="text-gray-600">扫描文件:</span>
|
||||
<span className="font-bold text-white">{task.scanned_files}/{task.total_files}</span>
|
||||
<div className="flex items-center justify-between border-b border-border pb-2">
|
||||
<span className="text-muted-foreground">扫描文件:</span>
|
||||
<span className="font-bold text-foreground">{task.scanned_files}/{task.total_files}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-gray-800 pb-2">
|
||||
<span className="text-gray-600">发现问题:</span>
|
||||
<div className="flex items-center justify-between border-b border-border pb-2">
|
||||
<span className="text-muted-foreground">发现问题:</span>
|
||||
<span className="font-bold text-amber-400">{issues.length}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-gray-800 pb-2">
|
||||
<span className="text-gray-600">代码行数:</span>
|
||||
<span className="font-bold text-white">{task.total_lines.toLocaleString()}</span>
|
||||
<div className="flex items-center justify-between border-b border-border pb-2">
|
||||
<span className="text-muted-foreground">代码行数:</span>
|
||||
<span className="font-bold text-foreground">{task.total_lines.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-gray-800 pb-2">
|
||||
<span className="text-gray-600">严重问题:</span>
|
||||
<div className="flex items-center justify-between border-b border-border pb-2">
|
||||
<span className="text-muted-foreground">严重问题:</span>
|
||||
<span className="font-bold text-rose-400">
|
||||
{issues.filter(i => i.severity === "critical").length}
|
||||
</span>
|
||||
|
|
@ -138,7 +138,7 @@ export default function ExportReportDialog({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="border-t border-gray-800 pt-4">
|
||||
<DialogFooter className="border-t border-border pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
|
|
|
|||
|
|
@ -72,13 +72,13 @@ export default function InstantExportDialog({
|
|||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px] bg-[#0c0c12] border-gray-700">
|
||||
<DialogContent className="sm:max-w-[600px] cyber-dialog border-border">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-3 text-lg font-bold uppercase tracking-wider text-white">
|
||||
<DialogTitle className="flex items-center gap-3 text-lg font-bold uppercase tracking-wider text-foreground">
|
||||
<Download className="w-5 h-5 text-primary" />
|
||||
导出分析报告
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-400 font-mono text-xs">
|
||||
<DialogDescription className="text-muted-foreground font-mono text-xs">
|
||||
选择报告格式并导出代码分析结果
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
|
@ -89,23 +89,23 @@ export default function InstantExportDialog({
|
|||
onValueChange={(value) => setSelectedFormat(value as ExportFormat)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="flex items-center space-x-3 p-4 border border-gray-700 rounded bg-gray-900/30 cursor-pointer hover:bg-gray-800/50">
|
||||
<div className="flex items-center space-x-3 p-4 border border-border rounded bg-muted/50 cursor-pointer hover:bg-muted">
|
||||
<RadioGroupItem value="json" id="json" />
|
||||
<Label htmlFor="json" className="flex items-center gap-3 cursor-pointer flex-1">
|
||||
<FileJson className="w-5 h-5 text-amber-400" />
|
||||
<div>
|
||||
<div className="font-bold text-gray-200">JSON 格式</div>
|
||||
<div className="text-xs text-gray-500">结构化数据,适合程序处理和集成</div>
|
||||
<div className="font-bold text-foreground">JSON 格式</div>
|
||||
<div className="text-xs text-muted-foreground">结构化数据,适合程序处理和集成</div>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
<div className={`flex items-center space-x-3 p-4 border border-gray-700 rounded bg-gray-900/30 ${isPdfDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-gray-800/50'}`}>
|
||||
<div className={`flex items-center space-x-3 p-4 border border-border rounded bg-muted/50 ${isPdfDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-muted'}`}>
|
||||
<RadioGroupItem value="pdf" id="pdf" disabled={isPdfDisabled} />
|
||||
<Label htmlFor="pdf" className={`flex items-center gap-3 flex-1 ${isPdfDisabled ? 'cursor-not-allowed' : 'cursor-pointer'}`}>
|
||||
<FileText className="w-5 h-5 text-rose-400" />
|
||||
<div>
|
||||
<div className="font-bold text-gray-200">PDF 格式</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
<div className="font-bold text-foreground">PDF 格式</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{isPdfDisabled && <AlertTriangle className="w-3 h-3 inline mr-1 text-amber-500" />}
|
||||
{isPdfDisabled ? "需要先保存到历史记录" : "专业报告,适合打印和分享"}
|
||||
</div>
|
||||
|
|
@ -115,33 +115,33 @@ export default function InstantExportDialog({
|
|||
</RadioGroup>
|
||||
|
||||
{/* 报告预览信息 */}
|
||||
<div className="mt-6 border border-gray-700 rounded bg-gray-900/30">
|
||||
<div className="px-4 py-2 border-b border-gray-800 bg-gray-900/50 flex items-center gap-2">
|
||||
<div className="mt-6 border border-border rounded bg-muted/50">
|
||||
<div className="px-4 py-2 border-b border-border bg-muted flex items-center gap-2">
|
||||
<Terminal className="w-3 h-3 text-primary" />
|
||||
<h4 className="font-bold text-gray-300 uppercase text-xs">报告内容预览</h4>
|
||||
<h4 className="font-bold text-foreground uppercase text-xs">报告内容预览</h4>
|
||||
</div>
|
||||
<div className="p-4 grid grid-cols-2 gap-3 text-xs font-mono">
|
||||
<div className="flex items-center justify-between border-b border-gray-800 pb-2">
|
||||
<span className="text-gray-600">编程语言:</span>
|
||||
<div className="flex items-center justify-between border-b border-border pb-2">
|
||||
<span className="text-muted-foreground">编程语言:</span>
|
||||
<span className="font-bold text-sky-400">{language.toUpperCase()}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-gray-800 pb-2">
|
||||
<span className="text-gray-600">质量评分:</span>
|
||||
<div className="flex items-center justify-between border-b border-border pb-2">
|
||||
<span className="text-muted-foreground">质量评分:</span>
|
||||
<span className="font-bold text-emerald-400">{(analysisResult.quality_score ?? 0).toFixed(1)}/100</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-gray-800 pb-2">
|
||||
<span className="text-gray-600">发现问题:</span>
|
||||
<div className="flex items-center justify-between border-b border-border pb-2">
|
||||
<span className="text-muted-foreground">发现问题:</span>
|
||||
<span className="font-bold text-amber-400">{analysisResult.issues?.length ?? 0}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-gray-800 pb-2">
|
||||
<span className="text-gray-600">分析耗时:</span>
|
||||
<span className="font-bold text-white">{(analysisTime ?? 0).toFixed(2)}s</span>
|
||||
<div className="flex items-center justify-between border-b border-border pb-2">
|
||||
<span className="text-muted-foreground">分析耗时:</span>
|
||||
<span className="font-bold text-foreground">{(analysisTime ?? 0).toFixed(2)}s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="border-t border-gray-800 pt-4">
|
||||
<DialogFooter className="border-t border-border pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ export function SystemConfig() {
|
|||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [testingLLM, setTestingLLM] = useState(false);
|
||||
const [llmTestResult, setLlmTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||
const [llmTestResult, setLlmTestResult] = useState<{ success: boolean; message: string; debug?: Record<string, unknown> } | null>(null);
|
||||
const [showDebugInfo, setShowDebugInfo] = useState(true);
|
||||
|
||||
useEffect(() => { loadConfig(); }, []);
|
||||
|
||||
|
|
@ -210,7 +211,7 @@ export function SystemConfig() {
|
|||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="loading-spinner mx-auto" />
|
||||
<p className="text-gray-500 font-mono text-sm uppercase tracking-wider">加载配置中...</p>
|
||||
<p className="text-muted-foreground font-mono text-sm uppercase tracking-wider">加载配置中...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -252,17 +253,17 @@ export function SystemConfig() {
|
|||
</div>
|
||||
|
||||
<Tabs defaultValue="llm" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4 bg-gray-900/50 border border-gray-800 p-1 h-auto gap-1 rounded-lg mb-6">
|
||||
<TabsTrigger value="llm" className="data-[state=active]:bg-primary data-[state=active]:text-white font-mono font-bold uppercase py-2.5 text-gray-400 transition-all rounded text-xs flex items-center gap-2">
|
||||
<TabsList className="grid w-full grid-cols-4 bg-muted border border-border p-1 h-auto gap-1 rounded-lg mb-6">
|
||||
<TabsTrigger value="llm" className="data-[state=active]:bg-primary data-[state=active]:text-foreground font-mono font-bold uppercase py-2.5 text-muted-foreground transition-all rounded text-xs flex items-center gap-2">
|
||||
<Zap className="w-3 h-3" /> LLM 配置
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="embedding" className="data-[state=active]:bg-primary data-[state=active]:text-white font-mono font-bold uppercase py-2.5 text-gray-400 transition-all rounded text-xs flex items-center gap-2">
|
||||
<TabsTrigger value="embedding" className="data-[state=active]:bg-primary data-[state=active]:text-foreground font-mono font-bold uppercase py-2.5 text-muted-foreground transition-all rounded text-xs flex items-center gap-2">
|
||||
<Brain className="w-3 h-3" /> 嵌入模型
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="analysis" className="data-[state=active]:bg-primary data-[state=active]:text-white font-mono font-bold uppercase py-2.5 text-gray-400 transition-all rounded text-xs flex items-center gap-2">
|
||||
<TabsTrigger value="analysis" className="data-[state=active]:bg-primary data-[state=active]:text-foreground font-mono font-bold uppercase py-2.5 text-muted-foreground transition-all rounded text-xs flex items-center gap-2">
|
||||
<Settings className="w-3 h-3" /> 分析参数
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="git" className="data-[state=active]:bg-primary data-[state=active]:text-white font-mono font-bold uppercase py-2.5 text-gray-400 transition-all rounded text-xs flex items-center gap-2">
|
||||
<TabsTrigger value="git" className="data-[state=active]:bg-primary data-[state=active]:text-foreground font-mono font-bold uppercase py-2.5 text-muted-foreground transition-all rounded text-xs flex items-center gap-2">
|
||||
<Globe className="w-3 h-3" /> Git 集成
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
|
@ -272,29 +273,29 @@ export function SystemConfig() {
|
|||
<div className="cyber-card p-6 space-y-6">
|
||||
{/* Provider Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold text-gray-500 uppercase">选择 LLM 提供商</Label>
|
||||
<Label className="text-xs font-bold text-muted-foreground uppercase">选择 LLM 提供商</Label>
|
||||
<Select value={config.llmProvider} onValueChange={(v) => updateConfig('llmProvider', v)}>
|
||||
<SelectTrigger className="h-12 cyber-input">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#0c0c12] border-gray-700">
|
||||
<div className="px-2 py-1.5 text-xs font-bold text-gray-500 uppercase">LiteLLM 统一适配 (推荐)</div>
|
||||
<SelectContent className="cyber-dialog border-border">
|
||||
<div className="px-2 py-1.5 text-xs font-bold text-muted-foreground uppercase">LiteLLM 统一适配 (推荐)</div>
|
||||
{LLM_PROVIDERS.filter(p => p.category === 'litellm').map(p => (
|
||||
<SelectItem key={p.value} value={p.value} className="font-mono">
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{p.icon}</span>
|
||||
<span>{p.label}</span>
|
||||
<span className="text-xs text-gray-500">- {p.hint}</span>
|
||||
<span className="text-xs text-muted-foreground">- {p.hint}</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
<div className="px-2 py-1.5 text-xs font-bold text-gray-500 uppercase mt-2">原生适配器</div>
|
||||
<div className="px-2 py-1.5 text-xs font-bold text-muted-foreground uppercase mt-2">原生适配器</div>
|
||||
{LLM_PROVIDERS.filter(p => p.category === 'native').map(p => (
|
||||
<SelectItem key={p.value} value={p.value} className="font-mono">
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{p.icon}</span>
|
||||
<span>{p.label}</span>
|
||||
<span className="text-xs text-gray-500">- {p.hint}</span>
|
||||
<span className="text-xs text-muted-foreground">- {p.hint}</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
|
|
@ -305,7 +306,7 @@ export function SystemConfig() {
|
|||
{/* API Key */}
|
||||
{config.llmProvider !== 'ollama' && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold text-gray-500 uppercase">API Key</Label>
|
||||
<Label className="text-xs font-bold text-muted-foreground uppercase">API Key</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type={showApiKey ? 'text' : 'password'}
|
||||
|
|
@ -329,7 +330,7 @@ export function SystemConfig() {
|
|||
{/* Model and Base URL */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold text-gray-500 uppercase">模型名称 (可选)</Label>
|
||||
<Label className="text-xs font-bold text-muted-foreground uppercase">模型名称 (可选)</Label>
|
||||
<Input
|
||||
value={config.llmModel}
|
||||
onChange={(e) => updateConfig('llmModel', e.target.value)}
|
||||
|
|
@ -338,7 +339,7 @@ export function SystemConfig() {
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold text-gray-500 uppercase">API Base URL (可选)</Label>
|
||||
<Label className="text-xs font-bold text-muted-foreground uppercase">API Base URL (可选)</Label>
|
||||
<Input
|
||||
value={config.llmBaseUrl}
|
||||
onChange={(e) => updateConfig('llmBaseUrl', e.target.value)}
|
||||
|
|
@ -349,10 +350,10 @@ export function SystemConfig() {
|
|||
</div>
|
||||
|
||||
{/* Test Connection */}
|
||||
<div className="pt-4 border-t border-gray-800 border-dashed flex items-center justify-between flex-wrap gap-4">
|
||||
<div className="pt-4 border-t border-border border-dashed flex items-center justify-between flex-wrap gap-4">
|
||||
<div className="text-sm">
|
||||
<span className="font-bold text-gray-300">测试连接</span>
|
||||
<span className="text-gray-500 ml-2">验证配置是否正确</span>
|
||||
<span className="font-bold text-foreground">测试连接</span>
|
||||
<span className="text-muted-foreground ml-2">验证配置是否正确</span>
|
||||
</div>
|
||||
<Button
|
||||
onClick={testLLMConnection}
|
||||
|
|
@ -374,25 +375,110 @@ export function SystemConfig() {
|
|||
</div>
|
||||
{llmTestResult && (
|
||||
<div className={`p-3 rounded-lg ${llmTestResult.success ? 'bg-emerald-500/10 border border-emerald-500/30' : 'bg-rose-500/10 border border-rose-500/30'}`}>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
{llmTestResult.success ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-400" />
|
||||
) : (
|
||||
<AlertCircle className="h-4 w-4 text-rose-400" />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
{llmTestResult.success ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-400" />
|
||||
) : (
|
||||
<AlertCircle className="h-4 w-4 text-rose-400" />
|
||||
)}
|
||||
<span className={llmTestResult.success ? 'text-emerald-300/80' : 'text-rose-300/80'}>
|
||||
{llmTestResult.message}
|
||||
</span>
|
||||
</div>
|
||||
{llmTestResult.debug && (
|
||||
<button
|
||||
onClick={() => setShowDebugInfo(!showDebugInfo)}
|
||||
className="text-xs text-muted-foreground hover:text-foreground underline"
|
||||
>
|
||||
{showDebugInfo ? '隐藏调试信息' : '显示调试信息'}
|
||||
</button>
|
||||
)}
|
||||
<span className={llmTestResult.success ? 'text-emerald-300/80' : 'text-rose-300/80'}>
|
||||
{llmTestResult.message}
|
||||
</span>
|
||||
</div>
|
||||
{showDebugInfo && llmTestResult.debug && (
|
||||
<div className="mt-3 pt-3 border-t border-border/50">
|
||||
<div className="text-xs font-mono space-y-1 text-muted-foreground">
|
||||
<div className="font-bold text-foreground mb-2">连接信息:</div>
|
||||
<div>Provider: <span className="text-foreground">{String(llmTestResult.debug.provider)}</span></div>
|
||||
<div>Model: <span className="text-foreground">{String(llmTestResult.debug.model_used || llmTestResult.debug.model_requested || 'N/A')}</span></div>
|
||||
<div>Base URL: <span className="text-foreground">{String(llmTestResult.debug.base_url_used || llmTestResult.debug.base_url_requested || '(default)')}</span></div>
|
||||
<div>Adapter: <span className="text-foreground">{String(llmTestResult.debug.adapter_type || 'N/A')}</span></div>
|
||||
<div>API Key: <span className="text-foreground">{String(llmTestResult.debug.api_key_prefix)} (长度: {String(llmTestResult.debug.api_key_length)})</span></div>
|
||||
<div>耗时: <span className="text-foreground">{String(llmTestResult.debug.elapsed_time_ms || 'N/A')} ms</span></div>
|
||||
|
||||
{/* 用户保存的配置参数 */}
|
||||
{llmTestResult.debug.saved_config && (
|
||||
<div className="mt-3 pt-2 border-t border-border/30">
|
||||
<div className="font-bold text-cyan-400 mb-2">已保存的配置参数:</div>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1">
|
||||
<div>温度: <span className="text-foreground">{String((llmTestResult.debug.saved_config as Record<string, unknown>).temperature ?? 'N/A')}</span></div>
|
||||
<div>最大Tokens: <span className="text-foreground">{String((llmTestResult.debug.saved_config as Record<string, unknown>).max_tokens ?? 'N/A')}</span></div>
|
||||
<div>超时: <span className="text-foreground">{String((llmTestResult.debug.saved_config as Record<string, unknown>).timeout_ms ?? 'N/A')} ms</span></div>
|
||||
<div>请求间隔: <span className="text-foreground">{String((llmTestResult.debug.saved_config as Record<string, unknown>).gap_ms ?? 'N/A')} ms</span></div>
|
||||
<div>并发数: <span className="text-foreground">{String((llmTestResult.debug.saved_config as Record<string, unknown>).concurrency ?? 'N/A')}</span></div>
|
||||
<div>最大文件数: <span className="text-foreground">{String((llmTestResult.debug.saved_config as Record<string, unknown>).max_analyze_files ?? 'N/A')}</span></div>
|
||||
<div>输出语言: <span className="text-foreground">{String((llmTestResult.debug.saved_config as Record<string, unknown>).output_language ?? 'N/A')}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 测试时实际使用的参数 */}
|
||||
{llmTestResult.debug.test_params && (
|
||||
<div className="mt-2 pt-2 border-t border-border/30">
|
||||
<div className="font-bold text-emerald-400 mb-2">测试时使用的参数:</div>
|
||||
<div className="grid grid-cols-3 gap-x-4">
|
||||
<div>温度: <span className="text-foreground">{String((llmTestResult.debug.test_params as Record<string, unknown>).temperature ?? 'N/A')}</span></div>
|
||||
<div>超时: <span className="text-foreground">{String((llmTestResult.debug.test_params as Record<string, unknown>).timeout ?? 'N/A')}s</span></div>
|
||||
<div>MaxTokens: <span className="text-foreground">{String((llmTestResult.debug.test_params as Record<string, unknown>).max_tokens ?? 'N/A')}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{llmTestResult.debug.error_category && (
|
||||
<div className="mt-2">错误类型: <span className="text-rose-400">{String(llmTestResult.debug.error_category)}</span></div>
|
||||
)}
|
||||
{llmTestResult.debug.error_type && (
|
||||
<div>异常类型: <span className="text-rose-400">{String(llmTestResult.debug.error_type)}</span></div>
|
||||
)}
|
||||
{llmTestResult.debug.status_code && (
|
||||
<div>HTTP 状态码: <span className="text-rose-400">{String(llmTestResult.debug.status_code)}</span></div>
|
||||
)}
|
||||
{llmTestResult.debug.api_response && (
|
||||
<div className="mt-2">
|
||||
<div className="font-bold text-amber-400">API 服务器返回:</div>
|
||||
<pre className="mt-1 p-2 bg-amber-500/10 border border-amber-500/30 rounded text-xs overflow-x-auto">
|
||||
{String(llmTestResult.debug.api_response)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{llmTestResult.debug.error_message && (
|
||||
<div className="mt-2">
|
||||
<div className="font-bold text-foreground">完整错误信息:</div>
|
||||
<pre className="mt-1 p-2 bg-background/50 rounded text-xs overflow-x-auto max-h-32 overflow-y-auto">
|
||||
{String(llmTestResult.debug.error_message)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{llmTestResult.debug.traceback && (
|
||||
<details className="mt-2">
|
||||
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">完整堆栈跟踪</summary>
|
||||
<pre className="mt-1 p-2 bg-background/50 rounded text-xs overflow-x-auto max-h-48 overflow-y-auto whitespace-pre-wrap">
|
||||
{String(llmTestResult.debug.traceback)}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Advanced Parameters */}
|
||||
<details className="pt-4 border-t border-gray-800 border-dashed">
|
||||
<summary className="font-bold uppercase cursor-pointer hover:text-primary text-gray-400 text-sm">高级参数</summary>
|
||||
<details className="pt-4 border-t border-border border-dashed">
|
||||
<summary className="font-bold uppercase cursor-pointer hover:text-primary text-muted-foreground text-sm">高级参数</summary>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-gray-500 uppercase">超时 (毫秒)</Label>
|
||||
<Label className="text-xs text-muted-foreground uppercase">超时 (毫秒)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.llmTimeout}
|
||||
|
|
@ -401,7 +487,7 @@ export function SystemConfig() {
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-gray-500 uppercase">温度 (0-2)</Label>
|
||||
<Label className="text-xs text-muted-foreground uppercase">温度 (0-2)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.1"
|
||||
|
|
@ -413,7 +499,7 @@ export function SystemConfig() {
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-gray-500 uppercase">最大 Tokens</Label>
|
||||
<Label className="text-xs text-muted-foreground uppercase">最大 Tokens</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.llmMaxTokens}
|
||||
|
|
@ -426,14 +512,14 @@ export function SystemConfig() {
|
|||
</div>
|
||||
|
||||
{/* Usage Notes */}
|
||||
<div className="bg-gray-900/50 border border-gray-800 p-4 rounded-lg text-xs space-y-2">
|
||||
<p className="font-bold uppercase text-gray-400 flex items-center gap-2">
|
||||
<div className="bg-muted border border-border p-4 rounded-lg text-xs space-y-2">
|
||||
<p className="font-bold uppercase text-muted-foreground flex items-center gap-2">
|
||||
<Info className="w-4 h-4 text-sky-400" />
|
||||
配置说明
|
||||
</p>
|
||||
<p className="text-gray-500">• <strong className="text-gray-400">LiteLLM 统一适配</strong>: 大多数提供商通过 LiteLLM 统一处理,支持自动重试和负载均衡</p>
|
||||
<p className="text-gray-500">• <strong className="text-gray-400">原生适配器</strong>: 百度、MiniMax、豆包因 API 格式特殊,使用专用适配器</p>
|
||||
<p className="text-gray-500">• <strong className="text-gray-400">API 中转站</strong>: 在 Base URL 填入中转站地址即可,API Key 填中转站提供的 Key</p>
|
||||
<p className="text-muted-foreground">• <strong className="text-muted-foreground">LiteLLM 统一适配</strong>: 大多数提供商通过 LiteLLM 统一处理,支持自动重试和负载均衡</p>
|
||||
<p className="text-muted-foreground">• <strong className="text-muted-foreground">原生适配器</strong>: 百度、MiniMax、豆包因 API 格式特殊,使用专用适配器</p>
|
||||
<p className="text-muted-foreground">• <strong className="text-muted-foreground">API 中转站</strong>: 在 Base URL 填入中转站地址即可,API Key 填中转站提供的 Key</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
|
|
@ -447,47 +533,47 @@ export function SystemConfig() {
|
|||
<div className="cyber-card p-6 space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold text-gray-500 uppercase">最大分析文件数</Label>
|
||||
<Label className="text-xs font-bold text-muted-foreground uppercase">最大分析文件数</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.maxAnalyzeFiles}
|
||||
onChange={(e) => updateConfig('maxAnalyzeFiles', Number(e.target.value))}
|
||||
className="h-10 cyber-input"
|
||||
/>
|
||||
<p className="text-xs text-gray-600">单次任务最多处理的文件数量</p>
|
||||
<p className="text-xs text-muted-foreground">单次任务最多处理的文件数量</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold text-gray-500 uppercase">LLM 并发数</Label>
|
||||
<Label className="text-xs font-bold text-muted-foreground uppercase">LLM 并发数</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.llmConcurrency}
|
||||
onChange={(e) => updateConfig('llmConcurrency', Number(e.target.value))}
|
||||
className="h-10 cyber-input"
|
||||
/>
|
||||
<p className="text-xs text-gray-600">同时发送的 LLM 请求数量</p>
|
||||
<p className="text-xs text-muted-foreground">同时发送的 LLM 请求数量</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold text-gray-500 uppercase">请求间隔 (毫秒)</Label>
|
||||
<Label className="text-xs font-bold text-muted-foreground uppercase">请求间隔 (毫秒)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.llmGapMs}
|
||||
onChange={(e) => updateConfig('llmGapMs', Number(e.target.value))}
|
||||
className="h-10 cyber-input"
|
||||
/>
|
||||
<p className="text-xs text-gray-600">每个请求之间的延迟时间</p>
|
||||
<p className="text-xs text-muted-foreground">每个请求之间的延迟时间</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold text-gray-500 uppercase">输出语言</Label>
|
||||
<Label className="text-xs font-bold text-muted-foreground uppercase">输出语言</Label>
|
||||
<Select value={config.outputLanguage} onValueChange={(v) => updateConfig('outputLanguage', v)}>
|
||||
<SelectTrigger className="h-10 cyber-input">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#0c0c12] border-gray-700">
|
||||
<SelectContent className="cyber-dialog border-border">
|
||||
<SelectItem value="zh-CN" className="font-mono">🇨🇳 中文</SelectItem>
|
||||
<SelectItem value="en-US" className="font-mono">🇺🇸 English</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-gray-600">代码审查结果的输出语言</p>
|
||||
<p className="text-xs text-muted-foreground">代码审查结果的输出语言</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -497,7 +583,7 @@ export function SystemConfig() {
|
|||
<TabsContent value="git" className="space-y-6">
|
||||
<div className="cyber-card p-6 space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold text-gray-500 uppercase">GitHub Token (可选)</Label>
|
||||
<Label className="text-xs font-bold text-muted-foreground uppercase">GitHub Token (可选)</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={config.githubToken}
|
||||
|
|
@ -505,7 +591,7 @@ export function SystemConfig() {
|
|||
placeholder="ghp_xxxxxxxxxxxx"
|
||||
className="h-10 cyber-input"
|
||||
/>
|
||||
<p className="text-xs text-gray-600">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
用于访问私有仓库。获取:{' '}
|
||||
<a href="https://github.com/settings/tokens" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
||||
github.com/settings/tokens
|
||||
|
|
@ -513,7 +599,7 @@ export function SystemConfig() {
|
|||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold text-gray-500 uppercase">GitLab Token (可选)</Label>
|
||||
<Label className="text-xs font-bold text-muted-foreground uppercase">GitLab Token (可选)</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={config.gitlabToken}
|
||||
|
|
@ -521,7 +607,7 @@ export function SystemConfig() {
|
|||
placeholder="glpat-xxxxxxxxxxxx"
|
||||
className="h-10 cyber-input"
|
||||
/>
|
||||
<p className="text-xs text-gray-600">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
用于访问私有仓库。获取:{' '}
|
||||
<a href="https://gitlab.com/-/profile/personal_access_tokens" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
||||
gitlab.com/-/profile/personal_access_tokens
|
||||
|
|
@ -529,7 +615,7 @@ export function SystemConfig() {
|
|||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold text-gray-500 uppercase">Gitea Token (可选)</Label>
|
||||
<Label className="text-xs font-bold text-muted-foreground uppercase">Gitea Token (可选)</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={config.giteaToken}
|
||||
|
|
@ -537,20 +623,20 @@ export function SystemConfig() {
|
|||
placeholder="sha1_xxxxxxxxxxxx"
|
||||
className="h-10 cyber-input"
|
||||
/>
|
||||
<p className="text-xs text-gray-600">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
用于访问 Gitea 私有仓库。获取:{' '}
|
||||
<span className="text-primary">
|
||||
[your-gitea-instance]/user/settings/applications
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 border border-gray-800 p-4 rounded-lg text-xs">
|
||||
<p className="font-bold text-gray-400 flex items-center gap-2 mb-2">
|
||||
<div className="bg-muted border border-border p-4 rounded-lg text-xs">
|
||||
<p className="font-bold text-muted-foreground flex items-center gap-2 mb-2">
|
||||
<Info className="w-4 h-4 text-sky-400" />
|
||||
提示
|
||||
</p>
|
||||
<p className="text-gray-500">• 公开仓库无需配置 Token</p>
|
||||
<p className="text-gray-500">• 私有仓库需要配置对应平台的 Token</p>
|
||||
<p className="text-muted-foreground">• 公开仓库无需配置 Token</p>
|
||||
<p className="text-muted-foreground">• 私有仓库需要配置对应平台的 Token</p>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
|
|
|||
|
|
@ -33,13 +33,13 @@ function AccordionTrigger({
|
|||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-sm py-4 text-left text-base font-semibold transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-5 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
);
|
||||
|
|
@ -53,7 +53,7 @@ function AccordionContent({
|
|||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-base"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ function AlertDialogPortal({
|
|||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" container={document.body} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -49,14 +49,16 @@ function AlertDialogContent({
|
|||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 grid w-full max-w-lg gap-6 rounded-sm border border-border p-0 shadow-lg duration-200 overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
</AlertDialogPortal>
|
||||
);
|
||||
}
|
||||
|
|
@ -68,7 +70,7 @@ function AlertDialogHeader({
|
|||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
className={cn("flex flex-col gap-2 px-6 pt-6 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -82,7 +84,7 @@ function AlertDialogFooter({
|
|||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
"flex flex-col-reverse gap-3 sm:flex-row sm:justify-end px-6 pb-6 pt-4 border-t border-border bg-muted/30",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -97,7 +99,7 @@ function AlertDialogTitle({
|
|||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
className={cn("text-xl font-mono font-bold uppercase tracking-wider text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -110,7 +112,7 @@ function AlertDialogDescription({
|
|||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn("text-foreground/70 text-base font-mono", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||
import { cn } from "@/shared/utils/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
"relative w-full rounded-sm border px-5 py-4 text-base grid has-[>svg]:grid-cols-[calc(var(--spacing)*5)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-4 gap-y-1 items-start [&>svg]:size-5 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
@ -39,7 +39,7 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
"col-start-2 line-clamp-1 min-h-5 font-semibold tracking-tight text-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -55,7 +55,7 @@ function AlertDescription({
|
|||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
"text-foreground/70 col-start-2 grid justify-items-start gap-1 text-base [&_p]:leading-relaxed",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
"inline-flex items-center justify-center rounded-sm border px-3 py-1.5 text-sm font-mono font-semibold uppercase tracking-wider w-fit whitespace-nowrap shrink-0 [&>svg]:size-4 gap-1.5 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
@ -14,7 +14,7 @@ const badgeVariants = cva(
|
|||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
"border-transparent bg-destructive text-foreground [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm text-sm font-mono font-semibold transition-all duration-150 focus-visible:outline-none focus-visible:shadow-focus disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm text-base font-mono font-semibold transition-all duration-150 focus-visible:outline-none focus-visible:shadow-focus disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-5 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
@ -21,10 +21,10 @@ const buttonVariants = cva(
|
|||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-sm px-3 text-xs",
|
||||
lg: "h-10 rounded-sm px-8",
|
||||
icon: "h-9 w-9",
|
||||
default: "h-11 px-5 py-2.5",
|
||||
sm: "h-9 rounded-sm px-4 text-sm",
|
||||
lg: "h-13 rounded-sm px-8 text-lg",
|
||||
icon: "h-11 w-11",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6",
|
||||
"bg-card text-card-foreground flex flex-col gap-4 rounded-sm border border-border p-5 shadow-sm transition-all duration-200 hover:shadow-md relative overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -20,7 +20,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
"flex flex-col gap-2 pb-4 border-b border-border",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -32,7 +32,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
className={cn("text-lg font-mono font-bold uppercase tracking-wider text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -42,7 +42,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn("text-base text-foreground/70 font-mono", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -65,7 +65,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
className={cn("py-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -75,7 +75,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
className={cn("flex items-center gap-3 pt-4 border-t border-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ function Checkbox({
|
|||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"peer border-border bg-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-primary focus-visible:shadow-focus aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-6 shrink-0 rounded-sm border shadow-none transition-all outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -21,7 +21,7 @@ function Checkbox({
|
|||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
<CheckIcon className="size-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ function Command({
|
|||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -64,7 +64,7 @@ function CommandInput({
|
|||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-sm bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
|
|||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-6 border border-border bg-background p-0 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -59,7 +59,7 @@ const DialogHeader = ({
|
|||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
"flex flex-col space-y-2 px-6 pt-6 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -73,7 +73,7 @@ const DialogFooter = ({
|
|||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end gap-3 px-6 pb-6 pt-4 border-t border-border bg-muted/30",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -88,7 +88,7 @@ const DialogTitle = React.forwardRef<
|
|||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
"text-xl font-mono font-bold uppercase tracking-wider text-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -102,7 +102,7 @@ const DialogDescription = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
className={cn("text-base text-foreground/70 font-mono", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
|||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
"flex cursor-default select-none items-center gap-2 rounded-sm px-3 py-2 text-base outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-5 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
|
|
@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
|
|||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-sm border border-border bg-popover p-1 text-popover-foreground shadow-lg font-mono",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
|
|
@ -84,7 +84,7 @@ const DropdownMenuItem = React.forwardRef<
|
|||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-4 py-2.5 text-base outline-none transition-colors focus:bg-muted focus:text-foreground hover:bg-muted data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-5 [&>svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
|
|
@ -148,7 +148,7 @@ const DropdownMenuLabel = React.forwardRef<
|
|||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
"px-3 py-2 text-base font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground font-mono flex h-9 w-full min-w-0 rounded-sm border border-input bg-background px-3 py-2 text-sm shadow-none transition-[border-color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground font-mono font-medium flex h-11 w-full min-w-0 rounded-sm border border-input bg-background px-4 py-2.5 text-base shadow-none transition-[border-color,box-shadow] outline-none file:inline-flex file:h-9 file:border-0 file:bg-transparent file:text-base file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"focus:border-primary focus:shadow-focus",
|
||||
"aria-invalid:border-secondary",
|
||||
className
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ function Label({
|
|||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
"flex items-center gap-2 text-base leading-none font-mono font-semibold text-foreground/80 uppercase tracking-wider select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ function Menubar({
|
|||
<MenubarPrimitive.Root
|
||||
data-slot="menubar"
|
||||
className={cn(
|
||||
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
|
||||
"bg-background flex h-9 items-center gap-1 rounded-sm border p-1 shadow-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -77,7 +77,7 @@ function MenubarContent({
|
|||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-sm border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -246,7 +246,7 @@ function MenubarSubContent({
|
|||
<MenubarPrimitive.SubContent
|
||||
data-slot="menubar-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-sm border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -89,13 +89,13 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||
<div className="relative z-20 inline-block w-full" ref={containerRef}>
|
||||
<div className="relative flex flex-col items-center">
|
||||
<div onClick={toggleDropdown} className="w-full">
|
||||
<div className="mb-2 flex h-11 rounded-lg border border-gray-300 py-1.5 pl-3 pr-3 shadow-theme-xs outline-hidden transition focus:border-brand-300 focus:shadow-focus-ring dark:border-gray-700 dark:bg-gray-900 dark:focus:border-brand-300">
|
||||
<div className="mb-2 flex h-11 rounded-sm border border-border py-1.5 pl-3 pr-3 shadow-theme-xs outline-hidden transition focus:border-brand-300 focus:shadow-focus-ring dark:border-border dark:bg-card dark:focus:border-brand-300">
|
||||
<div className="flex flex-wrap flex-auto gap-2">
|
||||
{selectedValuesText.length > 0 ? (
|
||||
selectedValuesText.map((text, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="group flex items-center justify-center rounded-full border-[0.7px] border-transparent bg-gray-100 py-1 pl-2.5 pr-2 text-sm text-gray-800 hover:border-gray-200 dark:bg-gray-800 dark:text-white/90 dark:hover:border-gray-800"
|
||||
className="group flex items-center justify-center rounded-full border-[0.7px] border-transparent bg-muted py-1 pl-2.5 pr-2 text-sm text-foreground hover:border-border dark:bg-muted dark:text-foreground/90 dark:hover:border-border"
|
||||
>
|
||||
<span className="flex-initial max-w-full">{text}</span>
|
||||
<div className="flex flex-row-reverse flex-auto">
|
||||
|
|
@ -104,7 +104,7 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||
e.stopPropagation();
|
||||
removeOption(selectedOptions[index]);
|
||||
}}
|
||||
className="pl-2 text-gray-500 cursor-pointer group-hover:text-gray-400 dark:text-gray-400"
|
||||
className="pl-2 text-muted-foreground cursor-pointer group-hover:text-muted-foreground dark:text-muted-foreground"
|
||||
>
|
||||
<svg
|
||||
className="fill-current"
|
||||
|
|
@ -127,7 +127,7 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||
) : (
|
||||
<input
|
||||
placeholder="请选择选项..."
|
||||
className="w-full h-full p-1 pr-2 text-sm bg-transparent border-0 outline-hidden appearance-none placeholder:text-gray-800 focus:border-0 focus:outline-hidden focus:ring-0 dark:placeholder:text-white/90"
|
||||
className="w-full h-full p-1 pr-2 text-sm bg-transparent border-0 outline-hidden appearance-none placeholder:text-foreground focus:border-0 focus:outline-hidden focus:ring-0 dark:placeholder:text-foreground/90"
|
||||
readOnly
|
||||
value="请选择选项..."
|
||||
/>
|
||||
|
|
@ -137,7 +137,7 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||
<button
|
||||
type="button"
|
||||
onClick={toggleDropdown}
|
||||
className="w-5 h-5 text-gray-700 outline-hidden cursor-pointer focus:outline-hidden dark:text-gray-400"
|
||||
className="w-5 h-5 text-muted-foreground outline-hidden cursor-pointer focus:outline-hidden dark:text-muted-foreground"
|
||||
>
|
||||
<svg
|
||||
className={`stroke-current ${isOpen ? "rotate-180" : ""}`}
|
||||
|
|
@ -162,14 +162,14 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||
|
||||
{isOpen && (
|
||||
<div
|
||||
className="absolute left-0 z-40 w-full overflow-y-auto bg-white rounded-lg shadow-sm top-full max-h-select dark:bg-gray-900"
|
||||
className="absolute left-0 z-40 w-full overflow-y-auto bg-background rounded-sm shadow-sm top-full max-h-select dark:bg-card"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
{options.map((option, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`hover:bg-primary/5 w-full cursor-pointer rounded-t border-b border-gray-200 dark:border-gray-800`}
|
||||
className={`hover:bg-primary/5 w-full cursor-pointer rounded-t border-b border-border dark:border-border`}
|
||||
onClick={() => handleSelect(option.value)}
|
||||
>
|
||||
<div
|
||||
|
|
@ -179,7 +179,7 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className="mx-2 leading-6 text-gray-800 dark:text-white/90">
|
||||
<div className="mx-2 leading-6 text-foreground dark:text-foreground/90">
|
||||
{option.label}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ function NavigationMenuItem({
|
|||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-sm bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
);
|
||||
|
||||
function NavigationMenuTrigger({
|
||||
|
|
@ -91,7 +91,7 @@ function NavigationMenuContent({
|
|||
data-slot="navigation-menu-content"
|
||||
className={cn(
|
||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-sm group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -112,7 +112,7 @@ function NavigationMenuViewport({
|
|||
<NavigationMenuPrimitive.Viewport
|
||||
data-slot="navigation-menu-viewport"
|
||||
className={cn(
|
||||
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-sm border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ function PopoverContent({
|
|||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-sm border border-border p-5 shadow-lg outline-hidden font-mono text-base",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ function Progress({
|
|||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
"bg-muted relative h-3 w-full overflow-hidden rounded-full border border-border",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
|
|||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
"flex h-11 w-full items-center justify-between whitespace-nowrap rounded-sm border border-input bg-background px-4 py-2.5 text-base font-mono font-medium shadow-none ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:border-primary focus:shadow-focus disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -75,7 +75,7 @@ const SelectContent = React.forwardRef<
|
|||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
|
||||
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-sm border border-border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
|
|
@ -118,7 +118,7 @@ const SelectItem = React.forwardRef<
|
|||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-2.5 pl-4 pr-9 text-base font-mono outline-none focus:bg-muted focus:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-muted transition-colors",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ const SheetOverlay = React.forwardRef<
|
|||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"fixed z-50 gap-4 bg-background p-0 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out border-border",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
|
|
@ -108,7 +108,7 @@ const SheetTitle = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
className={cn("text-xl font-mono font-bold uppercase tracking-wider text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
|
@ -120,7 +120,7 @@ const SheetDescription = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
className={cn("text-base text-foreground/70 font-mono", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||
className={cn("bg-muted animate-pulse rounded-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ function Switch({
|
|||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted focus-visible:border-primary focus-visible:shadow-focus inline-flex h-7 w-12 shrink-0 items-center rounded-full border border-border shadow-none transition-all outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -19,7 +19,7 @@ function Switch({
|
|||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||
"bg-background pointer-events-none block size-5.5 rounded-full ring-0 shadow-sm transition-transform data-[state=checked]:translate-x-5.5 data-[state=unchecked]:translate-x-0.5"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
className="relative w-full overflow-x-auto rounded-sm border border-border"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
className={cn("w-full caption-bottom text-base font-mono", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -21,7 +21,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
|||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
className={cn("bg-muted/50 [&_tr]:border-b border-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -55,7 +55,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
"hover:bg-muted/30 data-[state=selected]:bg-muted border-b border-border transition-colors",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -68,7 +68,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
"text-foreground/70 h-12 px-4 text-left align-middle font-mono font-bold text-sm uppercase tracking-wider whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -81,7 +81,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
"px-4 py-3.5 align-middle text-foreground text-base [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ function TabsList({
|
|||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
"bg-muted text-muted-foreground inline-flex h-12 w-fit items-center justify-center rounded-sm p-1.5 border border-border",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -40,7 +40,7 @@ function TabsTrigger({
|
|||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:border-primary/30 focus-visible:border-primary focus-visible:shadow-focus text-muted-foreground inline-flex h-10 flex-1 items-center justify-center gap-2 rounded-sm border border-transparent px-4 py-2 text-base font-mono font-semibold uppercase tracking-wider whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export function Textarea({ className, ...props }: React.ComponentProps<"textarea
|
|||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-primary focus-visible:shadow-focus aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex field-sizing-content min-h-28 w-full rounded-sm border bg-background px-4 py-3 text-base font-mono font-medium leading-relaxed shadow-none transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,194 @@
|
|||
/**
|
||||
* Theme Toggle Component
|
||||
* Cyberpunk-styled theme switcher with smooth animations
|
||||
*/
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { Sun, Moon, Monitor } from "lucide-react";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
interface ThemeToggleProps {
|
||||
collapsed?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Enable smooth theme transition
|
||||
const enableTransition = () => {
|
||||
const html = document.documentElement;
|
||||
html.classList.add('theme-transition');
|
||||
|
||||
// Remove transition class after animation completes
|
||||
setTimeout(() => {
|
||||
html.classList.remove('theme-transition');
|
||||
}, 280);
|
||||
};
|
||||
|
||||
export function ThemeToggle({ collapsed = false, className }: ThemeToggleProps) {
|
||||
const { theme, setTheme, resolvedTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// Prevent hydration mismatch
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// Smooth theme change handler
|
||||
const handleThemeChange = useCallback((newTheme: string) => {
|
||||
enableTransition();
|
||||
setTheme(newTheme);
|
||||
}, [setTheme]);
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className={cn("h-10 w-full animate-pulse rounded-lg bg-muted", className)} />
|
||||
);
|
||||
}
|
||||
|
||||
const themes = [
|
||||
{ value: "light", icon: Sun, label: "浅色" },
|
||||
{ value: "dark", icon: Moon, label: "深色" },
|
||||
{ value: "system", icon: Monitor, label: "系统" },
|
||||
];
|
||||
|
||||
const currentTheme = themes.find((t) => t.value === theme) || themes[2];
|
||||
const CurrentIcon = currentTheme.icon;
|
||||
|
||||
// Cycle through themes
|
||||
const cycleTheme = () => {
|
||||
const currentIndex = themes.findIndex((t) => t.value === theme);
|
||||
const nextIndex = (currentIndex + 1) % themes.length;
|
||||
handleThemeChange(themes[nextIndex].value);
|
||||
};
|
||||
|
||||
// Collapsed mode - single button
|
||||
if (collapsed) {
|
||||
return (
|
||||
<button
|
||||
onClick={cycleTheme}
|
||||
className={cn(
|
||||
"flex items-center justify-center w-full h-10 rounded-lg",
|
||||
"border border-transparent transition-colors duration-200",
|
||||
"dark:text-muted-foreground dark:hover:text-primary dark:hover:bg-primary/10 dark:hover:border-primary/30",
|
||||
"text-muted-foreground hover:text-primary hover:bg-primary/5 hover:border-primary/20",
|
||||
className
|
||||
)}
|
||||
title={`当前: ${currentTheme.label}模式`}
|
||||
>
|
||||
<CurrentIcon
|
||||
className={cn(
|
||||
"w-6 h-6 transition-transform duration-200",
|
||||
resolvedTheme === "dark" && "text-amber-400",
|
||||
resolvedTheme === "light" && "text-orange-500"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Expanded mode - segmented control
|
||||
return (
|
||||
<div className={cn("w-full", className)}>
|
||||
<div className="flex items-center gap-2 px-3 py-2">
|
||||
<span className="text-xs text-muted-foreground dark:text-muted-foreground font-mono uppercase tracking-wider">
|
||||
主题
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1 p-1 mx-3 mb-2 rounded-lg",
|
||||
"dark:cyber-bg-elevated dark:border dark:border-[#1a2535]",
|
||||
"bg-muted border border-border"
|
||||
)}
|
||||
>
|
||||
{themes.map(({ value, icon: Icon, label }) => {
|
||||
const isActive = theme === value;
|
||||
return (
|
||||
<button
|
||||
key={value}
|
||||
onClick={() => handleThemeChange(value)}
|
||||
className={cn(
|
||||
"flex-1 flex items-center justify-center gap-1.5 py-1.5 px-2 rounded-md transition-all duration-200",
|
||||
"text-xs font-mono uppercase tracking-wider",
|
||||
isActive
|
||||
? cn(
|
||||
"dark:bg-primary/20 dark:text-primary dark:border dark:border-primary/40",
|
||||
"bg-white text-primary border border-primary/30 shadow-sm"
|
||||
)
|
||||
: cn(
|
||||
"dark:text-muted-foreground dark:hover:text-foreground dark:hover:bg-[#151a22]",
|
||||
"text-muted-foreground hover:text-muted-foreground hover:bg-background"
|
||||
)
|
||||
)}
|
||||
title={`${label}模式`}
|
||||
>
|
||||
<Icon
|
||||
className={cn(
|
||||
"w-3.5 h-3.5 transition-all duration-200",
|
||||
isActive && value === "dark" && "text-amber-400",
|
||||
isActive && value === "light" && "text-orange-500",
|
||||
isActive && value === "system" && "text-cyan-400"
|
||||
)}
|
||||
/>
|
||||
<span className="hidden sm:inline">{label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Compact version for dropdown menus
|
||||
export function ThemeToggleCompact({ className }: { className?: string }) {
|
||||
const { setTheme, resolvedTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
enableTransition();
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark");
|
||||
}, [setTheme, resolvedTheme]);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const isDark = resolvedTheme === "dark";
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
className={cn(
|
||||
"relative flex items-center justify-center w-10 h-10 rounded-lg",
|
||||
"dark:cyber-bg-elevated dark:border dark:border-[#1a2535] dark:hover:border-primary/50",
|
||||
"bg-muted border border-border hover:border-primary/50",
|
||||
"group transition-colors duration-200",
|
||||
className
|
||||
)}
|
||||
title={isDark ? "切换到浅色模式" : "切换到深色模式"}
|
||||
>
|
||||
{/* Sun icon */}
|
||||
<Sun
|
||||
className={cn(
|
||||
"absolute w-5 h-5 transition-all duration-250",
|
||||
isDark
|
||||
? "opacity-0 rotate-90 scale-0"
|
||||
: "opacity-100 rotate-0 scale-100 text-orange-500"
|
||||
)}
|
||||
/>
|
||||
{/* Moon icon */}
|
||||
<Moon
|
||||
className={cn(
|
||||
"absolute w-5 h-5 transition-all duration-250",
|
||||
isDark
|
||||
? "opacity-100 rotate-0 scale-100 text-amber-400"
|
||||
: "opacity-0 -rotate-90 scale-0"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default ThemeToggle;
|
||||
|
|
@ -26,7 +26,7 @@ function ToggleGroup({
|
|||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
|
||||
"group/toggle-group flex w-fit items-center rounded-sm data-[variant=outline]:shadow-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
"inline-flex items-center justify-center gap-2 rounded-sm text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
|
|||
|
|
@ -46,13 +46,13 @@ function TooltipContent({
|
|||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-sm px-4 py-2 text-sm font-mono text-balance shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -138,17 +138,17 @@ export default function Account() {
|
|||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-[#0a0a0f]">
|
||||
<div className="flex items-center justify-center min-h-screen cyber-bg-elevated">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="loading-spinner mx-auto" />
|
||||
<p className="text-gray-500 font-mono text-sm uppercase tracking-wider">加载中...</p>
|
||||
<p className="text-muted-foreground font-mono text-sm uppercase tracking-wider">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6 bg-[#0a0a0f] min-h-screen font-mono relative">
|
||||
<div className="space-y-6 p-6 cyber-bg-elevated min-h-screen font-mono relative">
|
||||
{/* Grid background */}
|
||||
<div className="absolute inset-0 cyber-grid-subtle pointer-events-none" />
|
||||
|
||||
|
|
@ -157,7 +157,7 @@ export default function Account() {
|
|||
<div className="cyber-card p-0">
|
||||
<div className="cyber-card-header">
|
||||
<User className="w-5 h-5 text-primary" />
|
||||
<h3 className="text-lg font-bold uppercase tracking-wider text-white">用户信息</h3>
|
||||
<h3 className="text-lg font-bold uppercase tracking-wider text-foreground">用户信息</h3>
|
||||
</div>
|
||||
<div className="p-6 text-center">
|
||||
<div className="relative inline-block mb-4">
|
||||
|
|
@ -167,31 +167,31 @@ export default function Account() {
|
|||
{getInitials(profile?.full_name, profile?.email)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="absolute -bottom-1 -right-1 w-6 h-6 bg-emerald-500 rounded-full border-2 border-[#0a0a0f] flex items-center justify-center">
|
||||
<div className="w-2 h-2 bg-white rounded-full animate-pulse" />
|
||||
<div className="absolute -bottom-1 -right-1 w-6 h-6 bg-emerald-500 rounded-full border-2 border-background flex items-center justify-center">
|
||||
<div className="w-2 h-2 bg-foreground rounded-full animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
<h4 className="text-lg font-bold text-white uppercase mb-1">
|
||||
<h4 className="text-lg font-bold text-foreground uppercase mb-1">
|
||||
{profile?.full_name || "未设置姓名"}
|
||||
</h4>
|
||||
<p className="text-gray-500 text-sm">{profile?.email}</p>
|
||||
<p className="text-muted-foreground text-sm">{profile?.email}</p>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-gray-800 space-y-3 text-left">
|
||||
<div className="mt-6 pt-6 border-t border-border space-y-3 text-left">
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<Shield className="w-4 h-4 text-violet-400" />
|
||||
<span className="text-gray-500">角色:</span>
|
||||
<span className="text-muted-foreground">角色:</span>
|
||||
<span className="text-violet-400 font-bold uppercase">
|
||||
{profile?.role === 'admin' ? '管理员' : '成员'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<Calendar className="w-4 h-4 text-sky-400" />
|
||||
<span className="text-gray-500">注册时间:</span>
|
||||
<span className="text-white font-mono">{formatDate(profile?.created_at)}</span>
|
||||
<span className="text-muted-foreground">注册时间:</span>
|
||||
<span className="text-foreground font-mono">{formatDate(profile?.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-gray-800 space-y-2">
|
||||
<div className="mt-6 pt-6 border-t border-border space-y-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleSwitchAccount}
|
||||
|
|
@ -216,24 +216,24 @@ export default function Account() {
|
|||
<div className="lg:col-span-2 cyber-card p-0">
|
||||
<div className="cyber-card-header">
|
||||
<Terminal className="w-5 h-5 text-primary" />
|
||||
<h3 className="text-lg font-bold uppercase tracking-wider text-white">基本信息</h3>
|
||||
<h3 className="text-lg font-bold uppercase tracking-wider text-foreground">基本信息</h3>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-xs font-bold text-gray-500 uppercase flex items-center gap-2">
|
||||
<Label htmlFor="email" className="text-xs font-bold text-muted-foreground uppercase flex items-center gap-2">
|
||||
<Mail className="w-3 h-3" /> 邮箱
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
value={profile?.email || ""}
|
||||
disabled
|
||||
className="cyber-input bg-gray-900/50 text-gray-500 cursor-not-allowed"
|
||||
className="cyber-input bg-muted text-muted-foreground cursor-not-allowed"
|
||||
/>
|
||||
<p className="text-xs text-gray-600">邮箱不可修改</p>
|
||||
<p className="text-xs text-muted-foreground">邮箱不可修改</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="full_name" className="text-xs font-bold text-gray-500 uppercase flex items-center gap-2">
|
||||
<Label htmlFor="full_name" className="text-xs font-bold text-muted-foreground uppercase flex items-center gap-2">
|
||||
<User className="w-3 h-3" /> 姓名
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -245,7 +245,7 @@ export default function Account() {
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone" className="text-xs font-bold text-gray-500 uppercase flex items-center gap-2">
|
||||
<Label htmlFor="phone" className="text-xs font-bold text-muted-foreground uppercase flex items-center gap-2">
|
||||
<Phone className="w-3 h-3" /> 手机号
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -258,14 +258,14 @@ export default function Account() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-6 border-t border-gray-800">
|
||||
<div className="pt-6 border-t border-border">
|
||||
<h3 className="section-title text-sm mb-4 flex items-center gap-2">
|
||||
<GitBranch className="w-4 h-4" />
|
||||
代码托管账号
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="github" className="text-xs font-bold text-gray-500 uppercase flex items-center gap-2">
|
||||
<Label htmlFor="github" className="text-xs font-bold text-muted-foreground uppercase flex items-center gap-2">
|
||||
<GitBranch className="w-3 h-3" /> GitHub 用户名
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -277,7 +277,7 @@ export default function Account() {
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gitlab" className="text-xs font-bold text-gray-500 uppercase flex items-center gap-2">
|
||||
<Label htmlFor="gitlab" className="text-xs font-bold text-muted-foreground uppercase flex items-center gap-2">
|
||||
<GitBranch className="w-3 h-3" /> GitLab 用户名
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -313,12 +313,12 @@ export default function Account() {
|
|||
<div className="lg:col-span-3 cyber-card p-0">
|
||||
<div className="cyber-card-header">
|
||||
<KeyRound className="w-5 h-5 text-amber-400" />
|
||||
<h3 className="text-lg font-bold uppercase tracking-wider text-white">修改密码</h3>
|
||||
<h3 className="text-lg font-bold uppercase tracking-wider text-foreground">修改密码</h3>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="new_password" className="text-xs font-bold text-gray-500 uppercase">新密码</Label>
|
||||
<Label htmlFor="new_password" className="text-xs font-bold text-muted-foreground uppercase">新密码</Label>
|
||||
<Input
|
||||
id="new_password"
|
||||
type="password"
|
||||
|
|
@ -329,7 +329,7 @@ export default function Account() {
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirm_password" className="text-xs font-bold text-gray-500 uppercase">确认密码</Label>
|
||||
<Label htmlFor="confirm_password" className="text-xs font-bold text-muted-foreground uppercase">确认密码</Label>
|
||||
<Input
|
||||
id="confirm_password"
|
||||
type="password"
|
||||
|
|
@ -365,13 +365,13 @@ export default function Account() {
|
|||
|
||||
{/* Logout Confirmation Dialog */}
|
||||
<AlertDialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
|
||||
<AlertDialogContent className="cyber-card border-rose-500/30 bg-[#0c0c12]">
|
||||
<AlertDialogContent className="cyber-card border-rose-500/30 cyber-dialog">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-lg font-bold uppercase text-white flex items-center gap-2">
|
||||
<AlertDialogTitle className="text-lg font-bold uppercase text-foreground flex items-center gap-2">
|
||||
<LogOut className="w-5 h-5 text-rose-400" />
|
||||
确认退出登录?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-gray-400">
|
||||
<AlertDialogDescription className="text-muted-foreground">
|
||||
退出后需要重新登录才能访问系统。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { Settings, Database, Terminal } from "lucide-react";
|
|||
|
||||
export default function AdminDashboard() {
|
||||
return (
|
||||
<div className="space-y-6 p-6 bg-[#0a0a0f] min-h-screen font-mono relative">
|
||||
<div className="space-y-6 p-6 cyber-bg-elevated min-h-screen font-mono relative">
|
||||
{/* Grid background */}
|
||||
<div className="absolute inset-0 cyber-grid-subtle pointer-events-none" />
|
||||
|
||||
|
|
@ -19,24 +19,24 @@ export default function AdminDashboard() {
|
|||
<div className="cyber-card p-0">
|
||||
<div className="cyber-card-header">
|
||||
<Terminal className="w-5 h-5 text-primary" />
|
||||
<h1 className="text-lg font-bold uppercase tracking-wider text-white">系统管理</h1>
|
||||
<h1 className="text-lg font-bold uppercase tracking-wider text-foreground">系统管理</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Tabs */}
|
||||
<Tabs defaultValue="config" className="w-full relative z-10">
|
||||
<TabsList className="grid w-full grid-cols-2 bg-gray-900/50 border border-gray-800 p-1 h-auto gap-1 rounded-lg mb-6">
|
||||
<TabsList className="grid w-full grid-cols-2 bg-muted border border-border p-1 h-auto gap-1 rounded-lg mb-6">
|
||||
<TabsTrigger
|
||||
value="config"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-white font-mono font-bold uppercase py-3 text-gray-400 transition-all rounded text-sm flex items-center gap-2"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-foreground font-mono font-bold uppercase py-3 text-muted-foreground transition-all rounded text-sm flex items-center gap-2"
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
系统配置
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="data"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-white font-mono font-bold uppercase py-3 text-gray-400 transition-all rounded text-sm flex items-center gap-2"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-foreground font-mono font-bold uppercase py-3 text-muted-foreground transition-all rounded text-sm flex items-center gap-2"
|
||||
>
|
||||
<Database className="w-4 h-4" />
|
||||
数据管理
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export const AgentDetailPanel = memo(function AgentDetailPanel({ agentId, treeNo
|
|||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-primary/50 to-transparent" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-3 border-b border-gray-800/50">
|
||||
<div className="flex items-center justify-between p-3 border-b border-border">
|
||||
<div className="flex items-center gap-2.5">
|
||||
{/* Agent type icon with color */}
|
||||
<div className={`text-${typeConfig.color}-400`}>
|
||||
|
|
@ -62,22 +62,22 @@ export const AgentDetailPanel = memo(function AgentDetailPanel({ agentId, treeNo
|
|||
|
||||
{/* Agent name */}
|
||||
<div>
|
||||
<span className="text-sm font-medium text-white block">{agent.agent_name}</span>
|
||||
<span className="text-[10px] text-gray-500 uppercase tracking-wider">{typeConfig.label}</span>
|
||||
<span className="text-sm font-medium text-foreground block">{agent.agent_name}</span>
|
||||
<span className="text-xs text-muted-foreground uppercase tracking-wider">{typeConfig.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-6 h-6 flex items-center justify-center rounded hover:bg-white/10 transition-colors text-gray-500 hover:text-white"
|
||||
className="w-6 h-6 flex items-center justify-center rounded hover:bg-white/10 transition-colors text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Status indicator */}
|
||||
<div className="px-3 py-2 border-b border-gray-800/50 bg-gray-900/20">
|
||||
<div className="px-3 py-2 border-b border-border bg-muted/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<div className={`
|
||||
|
|
@ -86,7 +86,7 @@ export const AgentDetailPanel = memo(function AgentDetailPanel({ agentId, treeNo
|
|||
${agent.status === 'completed' ? 'bg-green-500' : ''}
|
||||
${agent.status === 'failed' ? 'bg-red-400' : ''}
|
||||
${agent.status === 'waiting' ? 'bg-yellow-400' : ''}
|
||||
${agent.status === 'created' ? 'bg-gray-500' : ''}
|
||||
${agent.status === 'created' ? 'bg-background0' : ''}
|
||||
`} />
|
||||
{isRunning && (
|
||||
<div className="absolute inset-0 w-2.5 h-2.5 rounded-full bg-green-400 animate-ping opacity-30" />
|
||||
|
|
@ -101,30 +101,30 @@ export const AgentDetailPanel = memo(function AgentDetailPanel({ agentId, treeNo
|
|||
{/* Metrics grid */}
|
||||
<div className="p-3 grid grid-cols-2 gap-2">
|
||||
{/* Iterations */}
|
||||
<div className="flex items-center gap-2 p-2 rounded bg-gray-900/30 border border-gray-800/30">
|
||||
<div className="flex items-center gap-2 p-2 rounded bg-muted/50 border border-border">
|
||||
<Repeat className="w-3.5 h-3.5 text-cyan-400/70" />
|
||||
<div>
|
||||
<div className="text-[9px] text-gray-600 uppercase">Iterations</div>
|
||||
<div className="text-sm text-white font-mono">{agent.iterations || 0}</div>
|
||||
<div className="text-xs text-muted-foreground uppercase">Iterations</div>
|
||||
<div className="text-sm text-foreground font-mono">{agent.iterations || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tool Calls */}
|
||||
<div className="flex items-center gap-2 p-2 rounded bg-gray-900/30 border border-gray-800/30">
|
||||
<div className="flex items-center gap-2 p-2 rounded bg-muted/50 border border-border">
|
||||
<Zap className="w-3.5 h-3.5 text-amber-400/70" />
|
||||
<div>
|
||||
<div className="text-[9px] text-gray-600 uppercase">Tool Calls</div>
|
||||
<div className="text-sm text-white font-mono">{agent.tool_calls || 0}</div>
|
||||
<div className="text-xs text-muted-foreground uppercase">Tool Calls</div>
|
||||
<div className="text-sm text-foreground font-mono">{agent.tool_calls || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Findings - Only show for Orchestrator (root agent with no parent) */}
|
||||
{!agent.parent_agent_id && (
|
||||
<div className="flex items-center gap-2 p-2 rounded bg-gray-900/30 border border-gray-800/30">
|
||||
<Bug className={`w-3.5 h-3.5 ${agent.findings_count > 0 ? 'text-red-400/70' : 'text-gray-500/70'}`} />
|
||||
<div className="flex items-center gap-2 p-2 rounded bg-muted/50 border border-border">
|
||||
<Bug className={`w-3.5 h-3.5 ${agent.findings_count > 0 ? 'text-red-400/70' : 'text-muted-foreground/70'}`} />
|
||||
<div>
|
||||
<div className="text-[9px] text-gray-600 uppercase">Findings</div>
|
||||
<div className={`text-sm font-mono ${agent.findings_count > 0 ? 'text-red-400' : 'text-white'}`}>
|
||||
<div className="text-xs text-muted-foreground uppercase">Findings</div>
|
||||
<div className={`text-sm font-mono ${agent.findings_count > 0 ? 'text-red-400' : 'text-foreground'}`}>
|
||||
{agent.findings_count}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -133,13 +133,13 @@ export const AgentDetailPanel = memo(function AgentDetailPanel({ agentId, treeNo
|
|||
|
||||
{/* Duration/Status - Show for sub-agents instead of Findings */}
|
||||
{agent.parent_agent_id && (
|
||||
<div className="flex items-center gap-2 p-2 rounded bg-gray-900/30 border border-gray-800/30">
|
||||
<Clock className="w-3.5 h-3.5 text-slate-400/70" />
|
||||
<div className="flex items-center gap-2 p-2 rounded bg-muted/50 border border-border">
|
||||
<Clock className="w-3.5 h-3.5 text-muted-foreground/70" />
|
||||
<div>
|
||||
<div className="text-[9px] text-gray-600 uppercase">
|
||||
<div className="text-xs text-muted-foreground uppercase">
|
||||
{agent.duration_ms ? "Duration" : "Status"}
|
||||
</div>
|
||||
<div className="text-sm text-white font-mono">
|
||||
<div className="text-sm text-foreground font-mono">
|
||||
{agent.duration_ms
|
||||
? `${(agent.duration_ms / 1000).toFixed(1)}s`
|
||||
: (AGENT_STATUS_CONFIG[agent.status]?.text || agent.status)
|
||||
|
|
@ -150,11 +150,11 @@ export const AgentDetailPanel = memo(function AgentDetailPanel({ agentId, treeNo
|
|||
)}
|
||||
|
||||
{/* Tokens */}
|
||||
<div className="flex items-center gap-2 p-2 rounded bg-gray-900/30 border border-gray-800/30">
|
||||
<div className="flex items-center gap-2 p-2 rounded bg-muted/50 border border-border">
|
||||
<FileCode className="w-3.5 h-3.5 text-purple-400/70" />
|
||||
<div>
|
||||
<div className="text-[9px] text-gray-600 uppercase">Tokens</div>
|
||||
<div className="text-sm text-white font-mono">
|
||||
<div className="text-xs text-muted-foreground uppercase">Tokens</div>
|
||||
<div className="text-sm text-foreground font-mono">
|
||||
{((agent.tokens_used || 0) / 1000).toFixed(1)}k
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -164,12 +164,12 @@ export const AgentDetailPanel = memo(function AgentDetailPanel({ agentId, treeNo
|
|||
{/* Task description */}
|
||||
{agent.task_description && (
|
||||
<div className="px-3 pb-3">
|
||||
<div className="p-2.5 rounded bg-gray-900/30 border border-gray-800/30">
|
||||
<div className="p-2.5 rounded bg-muted/50 border border-border">
|
||||
<div className="flex items-center gap-1.5 mb-1.5">
|
||||
<Clock className="w-3 h-3 text-gray-500" />
|
||||
<span className="text-[9px] text-gray-500 uppercase tracking-wider">Current Task</span>
|
||||
<Clock className="w-3 h-3 text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground uppercase tracking-wider">Current Task</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 leading-relaxed line-clamp-3">
|
||||
<p className="text-xs text-muted-foreground leading-relaxed line-clamp-3">
|
||||
{agent.task_description}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -179,7 +179,7 @@ export const AgentDetailPanel = memo(function AgentDetailPanel({ agentId, treeNo
|
|||
{/* Sub-agents indicator */}
|
||||
{agent.children && agent.children.length > 0 && (
|
||||
<div className="px-3 pb-3">
|
||||
<div className="flex items-center gap-2 text-[10px] text-gray-500">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Network className="w-3 h-3" />
|
||||
<span className="uppercase tracking-wider">
|
||||
{agent.children.length} Sub-agent{agent.children.length > 1 ? 's' : ''}
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ export class AgentErrorBoundary extends Component<Props, State> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-[#0a0a0f] flex items-center justify-center p-4">
|
||||
<div className="h-screen cyber-bg-elevated flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-lg space-y-6">
|
||||
{/* Error Header */}
|
||||
<div className="flex items-center gap-4">
|
||||
|
|
@ -179,16 +179,16 @@ export class AgentErrorBoundary extends Component<Props, State> {
|
|||
)} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white">Agent Error</h2>
|
||||
<p className="text-sm text-gray-400">{this.getRecoveryHint()}</p>
|
||||
<h2 className="text-xl font-bold text-foreground">Agent Error</h2>
|
||||
<p className="text-sm text-muted-foreground">{this.getRecoveryHint()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Details */}
|
||||
<div className="bg-[#0d0d12] border border-gray-800 rounded-lg overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-gray-800 flex items-center gap-2">
|
||||
<Terminal className="w-4 h-4 text-gray-500" />
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wider font-bold">
|
||||
<div className="cyber-dialog border border-border rounded-lg overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-border flex items-center gap-2">
|
||||
<Terminal className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground uppercase tracking-wider font-bold">
|
||||
Error Details
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -199,20 +199,20 @@ export class AgentErrorBoundary extends Component<Props, State> {
|
|||
<Bug className="w-4 h-4 text-red-400 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-mono text-red-400">{error.name}</p>
|
||||
<p className="text-sm text-gray-300">{error.message}</p>
|
||||
<p className="text-sm text-foreground">{error.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{this.props.taskId && (
|
||||
<div className="text-xs text-gray-500">
|
||||
Task ID: <span className="font-mono text-gray-400">{this.props.taskId}</span>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Task ID: <span className="font-mono text-muted-foreground">{this.props.taskId}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{retryCount > 0 && (
|
||||
<div className="text-xs text-gray-500">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Retry attempts: <span className="text-yellow-400">{retryCount}/{maxRetries}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -220,10 +220,10 @@ export class AgentErrorBoundary extends Component<Props, State> {
|
|||
{/* Stack trace (dev only) */}
|
||||
{import.meta.env.DEV && error?.stack && (
|
||||
<details className="text-xs">
|
||||
<summary className="cursor-pointer text-gray-500 hover:text-gray-300 transition-colors">
|
||||
<summary className="cursor-pointer text-muted-foreground hover:text-foreground transition-colors">
|
||||
Stack Trace
|
||||
</summary>
|
||||
<pre className="mt-2 p-3 bg-black/50 rounded text-[10px] text-gray-400 overflow-auto max-h-40">
|
||||
<pre className="mt-2 p-3 bg-background/50 rounded text-xs text-muted-foreground overflow-auto max-h-40">
|
||||
{error.stack}
|
||||
</pre>
|
||||
</details>
|
||||
|
|
@ -231,10 +231,10 @@ export class AgentErrorBoundary extends Component<Props, State> {
|
|||
|
||||
{import.meta.env.DEV && errorInfo?.componentStack && (
|
||||
<details className="text-xs">
|
||||
<summary className="cursor-pointer text-gray-500 hover:text-gray-300 transition-colors">
|
||||
<summary className="cursor-pointer text-muted-foreground hover:text-foreground transition-colors">
|
||||
Component Stack
|
||||
</summary>
|
||||
<pre className="mt-2 p-3 bg-black/50 rounded text-[10px] text-gray-400 overflow-auto max-h-40">
|
||||
<pre className="mt-2 p-3 bg-background/50 rounded text-xs text-muted-foreground overflow-auto max-h-40">
|
||||
{errorInfo.componentStack}
|
||||
</pre>
|
||||
</details>
|
||||
|
|
@ -257,7 +257,7 @@ export class AgentErrorBoundary extends Component<Props, State> {
|
|||
<Button
|
||||
onClick={this.handleGoBack}
|
||||
variant="outline"
|
||||
className="flex-1 border-gray-700 hover:bg-gray-800"
|
||||
className="flex-1 border-border hover:bg-muted"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Go Back
|
||||
|
|
@ -265,7 +265,7 @@ export class AgentErrorBoundary extends Component<Props, State> {
|
|||
<Button
|
||||
onClick={this.handleReload}
|
||||
variant="ghost"
|
||||
className="flex-1 text-gray-400 hover:text-white"
|
||||
className="flex-1 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Refresh Page
|
||||
</Button>
|
||||
|
|
@ -273,7 +273,7 @@ export class AgentErrorBoundary extends Component<Props, State> {
|
|||
|
||||
{/* Recovery suggestion */}
|
||||
{!canRetry && (
|
||||
<p className="text-center text-xs text-gray-500">
|
||||
<p className="text-center text-xs text-muted-foreground">
|
||||
Maximum retry attempts reached. Please refresh the page or contact support.
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,165 +1,159 @@
|
|||
/**
|
||||
* Agent Tree Node Component
|
||||
* Elegant tree visualization with cassette futurism aesthetic
|
||||
* Features: Animated connection lines, status indicators, smooth transitions
|
||||
* Enhanced color palette for better visibility
|
||||
* Clean tree visualization with simple connection lines
|
||||
*/
|
||||
|
||||
import { useState, memo } from "react";
|
||||
import { ChevronDown, ChevronRight, Bot, Cpu, Scan, FileSearch, ShieldCheck } from "lucide-react";
|
||||
import { ChevronDown, ChevronRight, Bot, Cpu, Scan, FileSearch, ShieldCheck, Zap } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { AGENT_STATUS_CONFIG } from "../constants";
|
||||
import type { AgentTreeNodeItemProps } from "../types";
|
||||
|
||||
// Agent type icons with enhanced colors
|
||||
// Agent type icons
|
||||
const AGENT_TYPE_ICONS: Record<string, React.ReactNode> = {
|
||||
orchestrator: <Cpu className="w-3.5 h-3.5 text-violet-400" />,
|
||||
recon: <Scan className="w-3.5 h-3.5 text-teal-400" />,
|
||||
analysis: <FileSearch className="w-3.5 h-3.5 text-amber-400" />,
|
||||
verification: <ShieldCheck className="w-3.5 h-3.5 text-emerald-400" />,
|
||||
orchestrator: <Cpu className="w-4 h-4 text-violet-600 dark:text-violet-500" />,
|
||||
recon: <Scan className="w-4 h-4 text-teal-600 dark:text-teal-500" />,
|
||||
analysis: <FileSearch className="w-4 h-4 text-amber-600 dark:text-amber-500" />,
|
||||
verification: <ShieldCheck className="w-4 h-4 text-emerald-600 dark:text-emerald-500" />,
|
||||
};
|
||||
|
||||
// Status colors for the glow effect
|
||||
const STATUS_GLOW_COLORS: Record<string, string> = {
|
||||
running: 'shadow-[0_0_10px_rgba(52,211,153,0.4)]',
|
||||
completed: '',
|
||||
failed: 'shadow-[0_0_8px_rgba(251,113,133,0.3)]',
|
||||
waiting: '',
|
||||
created: '',
|
||||
// Agent type background colors
|
||||
const AGENT_TYPE_BG: Record<string, string> = {
|
||||
orchestrator: 'bg-violet-100 dark:bg-violet-500/15 border-violet-300 dark:border-violet-500/30',
|
||||
recon: 'bg-teal-100 dark:bg-teal-500/15 border-teal-300 dark:border-teal-500/30',
|
||||
analysis: 'bg-amber-100 dark:bg-amber-500/15 border-amber-300 dark:border-amber-500/30',
|
||||
verification: 'bg-emerald-100 dark:bg-emerald-500/15 border-emerald-300 dark:border-emerald-500/30',
|
||||
};
|
||||
|
||||
export const AgentTreeNodeItem = memo(function AgentTreeNodeItem({
|
||||
node,
|
||||
depth = 0,
|
||||
selectedId,
|
||||
onSelect
|
||||
}: AgentTreeNodeItemProps) {
|
||||
onSelect,
|
||||
isLast = false
|
||||
}: AgentTreeNodeItemProps & { isLast?: boolean }) {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
const isSelected = selectedId === node.agent_id;
|
||||
const isRunning = node.status === 'running';
|
||||
const isCompleted = node.status === 'completed';
|
||||
const isFailed = node.status === 'failed';
|
||||
|
||||
const statusConfig = AGENT_STATUS_CONFIG[node.status] || AGENT_STATUS_CONFIG.created;
|
||||
const typeIcon = AGENT_TYPE_ICONS[node.agent_type] || <Bot className="w-3.5 h-3.5 text-slate-400" />;
|
||||
const typeIcon = AGENT_TYPE_ICONS[node.agent_type] || <Bot className="w-3.5 h-3.5 text-muted-foreground" />;
|
||||
const typeBg = AGENT_TYPE_BG[node.agent_type] || 'bg-muted border-border';
|
||||
|
||||
const indent = depth * 24;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Connection line to parent - vertical line */}
|
||||
{/* 树形连接线 */}
|
||||
{depth > 0 && (
|
||||
<div
|
||||
className="absolute top-0 w-px bg-gradient-to-b from-slate-600 to-slate-700"
|
||||
style={{
|
||||
left: `${depth * 16 - 8}px`,
|
||||
height: '20px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Horizontal connector line */}
|
||||
{depth > 0 && (
|
||||
<div
|
||||
className="absolute top-[20px] h-px bg-slate-600"
|
||||
style={{
|
||||
left: `${depth * 16 - 8}px`,
|
||||
width: '8px',
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
{/* 垂直线 - 从父节点延伸下来 */}
|
||||
<div
|
||||
className="absolute border-l-2 border-slate-300 dark:border-slate-600"
|
||||
style={{
|
||||
left: `${indent - 12}px`,
|
||||
top: 0,
|
||||
height: isLast ? '20px' : '100%',
|
||||
}}
|
||||
/>
|
||||
{/* 水平线 - 连接到当前节点 */}
|
||||
<div
|
||||
className="absolute border-t-2 border-slate-300 dark:border-slate-600"
|
||||
style={{
|
||||
left: `${indent - 12}px`,
|
||||
top: '20px',
|
||||
width: '12px',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Node item */}
|
||||
<div
|
||||
className={`
|
||||
group relative flex items-center gap-2 py-2 px-2.5 cursor-pointer rounded-sm
|
||||
transition-all duration-200 ease-out
|
||||
relative flex items-center gap-2 py-2 px-2 cursor-pointer rounded-md
|
||||
${isSelected
|
||||
? 'bg-primary/15 border border-primary/40'
|
||||
: 'border border-transparent hover:bg-white/5 hover:border-slate-700/50'
|
||||
? 'bg-primary/15 border-2 border-primary shadow-[0_0_12px_rgba(255,95,31,0.4)]'
|
||||
: isRunning
|
||||
? 'bg-emerald-50 dark:bg-emerald-950/30 border-2 border-emerald-400 dark:border-emerald-500 shadow-[0_0_10px_rgba(52,211,153,0.3)]'
|
||||
: isCompleted
|
||||
? 'bg-slate-50 dark:bg-card border border-emerald-300 dark:border-emerald-600'
|
||||
: isFailed
|
||||
? 'bg-rose-50 dark:bg-rose-950/20 border border-rose-300 dark:border-rose-500'
|
||||
: node.status === 'waiting'
|
||||
? 'bg-amber-50 dark:bg-amber-950/20 border border-amber-300 dark:border-amber-500'
|
||||
: 'bg-slate-50 dark:bg-card border border-slate-300 dark:border-slate-600 hover:border-slate-400 dark:hover:border-slate-500'
|
||||
}
|
||||
${STATUS_GLOW_COLORS[node.status] || ''}
|
||||
`}
|
||||
style={{ marginLeft: `${depth * 16}px` }}
|
||||
style={{ marginLeft: `${indent}px` }}
|
||||
onClick={() => onSelect(node.agent_id)}
|
||||
>
|
||||
{/* Expand/collapse button */}
|
||||
{hasChildren ? (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setExpanded(!expanded); }}
|
||||
className="flex-shrink-0 w-5 h-5 flex items-center justify-center rounded hover:bg-white/10 transition-colors"
|
||||
className="flex-shrink-0 w-5 h-5 flex items-center justify-center rounded hover:bg-muted"
|
||||
>
|
||||
{expanded ? (
|
||||
<ChevronDown className="w-3.5 h-3.5 text-slate-400" />
|
||||
<ChevronDown className="w-4 h-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="w-3.5 h-3.5 text-slate-400" />
|
||||
<ChevronRight className="w-4 h-4 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<span className="w-5" />
|
||||
)}
|
||||
|
||||
{/* Status indicator with glow */}
|
||||
{/* Status indicator */}
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className={`
|
||||
w-2.5 h-2.5 rounded-full transition-all duration-300
|
||||
${isRunning ? 'bg-emerald-400 animate-pulse' : ''}
|
||||
${node.status === 'completed' ? 'bg-emerald-500' : ''}
|
||||
${node.status === 'failed' ? 'bg-rose-400' : ''}
|
||||
${node.status === 'waiting' ? 'bg-amber-400' : ''}
|
||||
${node.status === 'created' ? 'bg-slate-500' : ''}
|
||||
w-2.5 h-2.5 rounded-full
|
||||
${isRunning ? 'bg-emerald-500' : ''}
|
||||
${isCompleted ? 'bg-emerald-500' : ''}
|
||||
${isFailed ? 'bg-rose-500' : ''}
|
||||
${node.status === 'waiting' ? 'bg-amber-500' : ''}
|
||||
${node.status === 'created' ? 'bg-slate-400' : ''}
|
||||
`} />
|
||||
{isRunning && (
|
||||
<div className="absolute inset-0 w-2.5 h-2.5 rounded-full bg-emerald-400 animate-ping opacity-30" />
|
||||
<div className="absolute inset-0 w-2.5 h-2.5 rounded-full bg-emerald-500 animate-ping opacity-50" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Agent type icon */}
|
||||
<div className="flex-shrink-0">
|
||||
<div className={`flex-shrink-0 p-1 rounded border ${typeBg}`}>
|
||||
{typeIcon}
|
||||
</div>
|
||||
|
||||
{/* Agent name */}
|
||||
<span className={`
|
||||
text-xs font-mono truncate flex-1 transition-colors duration-200
|
||||
${isSelected ? 'text-white font-medium' : 'text-slate-300 group-hover:text-white'}
|
||||
text-sm font-mono truncate flex-1
|
||||
${isSelected ? 'text-foreground font-semibold' : 'text-foreground'}
|
||||
`}>
|
||||
{node.agent_name}
|
||||
</span>
|
||||
|
||||
{/* Metrics badges */}
|
||||
{/* Metrics */}
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||
{/* Iterations */}
|
||||
{(node.iterations ?? 0) > 0 && (
|
||||
<span className="text-[9px] text-slate-400 font-mono bg-slate-800/60 px-1.5 py-0.5 rounded">
|
||||
{node.iterations}x
|
||||
</span>
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground font-mono bg-muted px-1.5 py-0.5 rounded border border-border">
|
||||
<Zap className="w-3 h-3" />
|
||||
<span>{node.iterations}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Findings count - Only show for Orchestrator (root agent) */}
|
||||
{!node.parent_agent_id && node.findings_count > 0 && (
|
||||
<Badge className="h-4 px-1.5 text-[9px] bg-rose-500/25 text-rose-300 border border-rose-500/40 font-mono font-semibold">
|
||||
<Badge className="h-5 px-2 text-xs bg-rose-100 dark:bg-rose-500/20 text-rose-600 dark:text-rose-300 border border-rose-300 dark:border-rose-500/40 font-mono font-bold">
|
||||
{node.findings_count}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Children with animated reveal */}
|
||||
{/* Children */}
|
||||
{expanded && hasChildren && (
|
||||
<div
|
||||
className="relative"
|
||||
style={{
|
||||
animation: 'slideDown 0.2s ease-out',
|
||||
}}
|
||||
>
|
||||
{/* Vertical connection line for children */}
|
||||
<div
|
||||
className="absolute w-px bg-gradient-to-b from-slate-600 via-slate-700 to-transparent"
|
||||
style={{
|
||||
left: `${(depth + 1) * 16 - 8}px`,
|
||||
top: '0',
|
||||
height: `calc(100% - 20px)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative">
|
||||
{node.children.map((child, index) => (
|
||||
<AgentTreeNodeItem
|
||||
key={child.agent_id}
|
||||
|
|
@ -167,24 +161,11 @@ export const AgentTreeNodeItem = memo(function AgentTreeNodeItem({
|
|||
depth={depth + 1}
|
||||
selectedId={selectedId}
|
||||
onSelect={onSelect}
|
||||
isLast={index === node.children.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Inline animation */}
|
||||
<style>{`
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ const STATUS_CONFIG: Record<ConnectionState, {
|
|||
disconnected: {
|
||||
icon: WifiOff,
|
||||
label: 'Disconnected',
|
||||
color: 'text-gray-400',
|
||||
bgColor: 'bg-gray-400/10',
|
||||
color: 'text-muted-foreground',
|
||||
bgColor: 'bg-muted/30',
|
||||
},
|
||||
connecting: {
|
||||
icon: RefreshCw,
|
||||
|
|
@ -67,7 +67,7 @@ export function ConnectionStatus({
|
|||
return (
|
||||
<div className={cn('flex items-center gap-1.5', className)}>
|
||||
<div className={cn(
|
||||
'flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium',
|
||||
'flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium',
|
||||
config.bgColor,
|
||||
config.color
|
||||
)}>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
/**
|
||||
* Header Component
|
||||
* Minimalist mechanical terminal header
|
||||
* Features: Subtle glow effects, refined controls
|
||||
* Features: Enhanced glow effects, refined controls, premium feel
|
||||
*/
|
||||
|
||||
import { Bot, Square, Download, Play, Loader2, Radio, Cpu } from "lucide-react";
|
||||
import { Square, Download, Play, Loader2, Radio, Cpu, Sparkles } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { StatusBadge } from "./StatusBadge";
|
||||
import type { HeaderProps } from "../types";
|
||||
|
|
@ -18,74 +18,88 @@ export function Header({
|
|||
onNewAudit
|
||||
}: HeaderProps) {
|
||||
return (
|
||||
<header className="flex-shrink-0 h-14 border-b border-gray-800/80 flex items-center justify-between px-5 bg-gradient-to-r from-[#0d0d12] via-[#0e0e14] to-[#0d0d12] relative overflow-hidden">
|
||||
{/* Subtle animated line at top */}
|
||||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-primary/30 to-transparent" />
|
||||
<header className="flex-shrink-0 h-16 border-b border-border/50 flex items-center justify-between px-6 bg-card/80 backdrop-blur-md relative overflow-hidden">
|
||||
{/* Animated gradient line at top */}
|
||||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-primary/50 to-transparent" />
|
||||
{/* Subtle glow effect */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-primary/5 via-transparent to-primary/5 pointer-events-none" />
|
||||
|
||||
{/* Left side - Brand and task info */}
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Logo section */}
|
||||
<div className="flex items-center gap-2.5 pr-4 border-r border-gray-800/50">
|
||||
<div className="relative">
|
||||
<Cpu className="w-5 h-5 text-primary" />
|
||||
{isRunning && (
|
||||
<span className="absolute -top-0.5 -right-0.5 w-2 h-2 bg-green-400 rounded-full animate-pulse" />
|
||||
)}
|
||||
<div className="flex items-center gap-5 relative z-10">
|
||||
{/* Logo section with enhanced styling */}
|
||||
<div className="flex items-center gap-3 pr-5 border-r border-border/50">
|
||||
<div className="relative group">
|
||||
{/* Logo background glow */}
|
||||
<div className="absolute inset-0 bg-primary/20 rounded-lg blur-lg opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
<div className="relative p-2 rounded-lg bg-gradient-to-br from-primary/20 to-primary/5 border border-primary/30">
|
||||
<Cpu className="w-5 h-5 text-primary" />
|
||||
{isRunning && (
|
||||
<>
|
||||
<span className="absolute -top-1 -right-1 w-2.5 h-2.5 bg-emerald-400 rounded-full animate-pulse shadow-[0_0_10px_rgba(52,211,153,0.6)]" />
|
||||
<span className="absolute -top-1 -right-1 w-2.5 h-2.5 bg-emerald-400 rounded-full animate-ping opacity-75" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-foreground tracking-wider text-base leading-tight">
|
||||
DEEP<span className="text-primary">AUDIT</span>
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground tracking-[0.2em] uppercase">Security Agent</span>
|
||||
</div>
|
||||
<span className="font-bold text-white tracking-wide text-sm">
|
||||
DEEP<span className="text-primary">AUDIT</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Task info */}
|
||||
{/* Task info with enhanced styling */}
|
||||
{task && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2 text-gray-400">
|
||||
<Radio className="w-3 h-3" />
|
||||
<span className="text-xs font-mono uppercase tracking-wider">Task</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2 px-2.5 py-1 rounded-md bg-muted/50 border border-border/50">
|
||||
<Radio className="w-3 h-3 text-muted-foreground" />
|
||||
<span className="text-xs font-mono uppercase tracking-wider text-muted-foreground">Task</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-foreground text-sm font-mono truncate max-w-[200px] font-medium">
|
||||
{task.name || task.id.slice(0, 8)}
|
||||
</span>
|
||||
<StatusBadge status={task.status} />
|
||||
</div>
|
||||
<span className="text-gray-300 text-sm font-mono truncate max-w-[180px]">
|
||||
{task.name || task.id.slice(0, 8)}
|
||||
</span>
|
||||
<StatusBadge status={task.status} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right side - Controls */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-3 relative z-10">
|
||||
{isRunning && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onCancel}
|
||||
disabled={isCancelling}
|
||||
className="h-8 px-3 text-xs font-mono uppercase tracking-wider text-red-400 hover:text-red-300 hover:bg-red-950/30 border border-transparent hover:border-red-900/50 transition-all duration-200 disabled:opacity-50"
|
||||
className="h-9 px-4 text-xs font-mono uppercase tracking-wider text-rose-400 hover:text-rose-300 bg-rose-500/10 hover:bg-rose-500/20 border border-rose-500/30 hover:border-rose-500/50 transition-all duration-300 disabled:opacity-50 rounded-md shadow-[0_0_15px_rgba(244,63,94,0.1)] hover:shadow-[0_0_20px_rgba(244,63,94,0.2)]"
|
||||
>
|
||||
{isCancelling ? (
|
||||
<>
|
||||
<Loader2 className="w-3.5 h-3.5 mr-1.5 animate-spin" />
|
||||
<Loader2 className="w-3.5 h-3.5 mr-2 animate-spin" />
|
||||
<span>Stopping</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Square className="w-3.5 h-3.5 mr-1.5" />
|
||||
<Square className="w-3.5 h-3.5 mr-2" />
|
||||
<span>Abort</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="h-6 w-px bg-gray-800/50 mx-1" />
|
||||
<div className="h-8 w-px bg-border/50 mx-1" />
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onExport}
|
||||
disabled={!task}
|
||||
className="h-8 px-3 text-xs font-mono uppercase tracking-wider text-cyan-400 hover:text-cyan-300 hover:bg-cyan-950/30 border border-transparent hover:border-cyan-900/50 transition-all duration-200 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:border-transparent"
|
||||
className="h-9 px-4 text-xs font-mono uppercase tracking-wider text-cyan-400 hover:text-cyan-300 bg-cyan-500/10 hover:bg-cyan-500/20 border border-cyan-500/30 hover:border-cyan-500/50 transition-all duration-300 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:border-transparent rounded-md shadow-[0_0_15px_rgba(6,182,212,0.1)] hover:shadow-[0_0_20px_rgba(6,182,212,0.2)]"
|
||||
>
|
||||
<Download className="w-3.5 h-3.5 mr-1.5" />
|
||||
<Download className="w-3.5 h-3.5 mr-2" />
|
||||
<span>Export</span>
|
||||
</Button>
|
||||
|
||||
|
|
@ -93,16 +107,22 @@ export function Header({
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onNewAudit}
|
||||
className="h-8 px-3 text-xs font-mono uppercase tracking-wider text-primary hover:text-primary/80 hover:bg-primary/10 border border-transparent hover:border-primary/30 transition-all duration-200"
|
||||
className="h-9 px-4 text-xs font-mono uppercase tracking-wider text-primary hover:text-primary/90 bg-primary/10 hover:bg-primary/20 border border-primary/30 hover:border-primary/50 transition-all duration-300 rounded-md shadow-[0_0_15px_rgba(255,107,44,0.15)] hover:shadow-[0_0_25px_rgba(255,107,44,0.25)]"
|
||||
>
|
||||
<Play className="w-3.5 h-3.5 mr-1.5" />
|
||||
<Sparkles className="w-3.5 h-3.5 mr-2" />
|
||||
<span>New Audit</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Subtle bottom glow when running */}
|
||||
{/* Bottom accent line */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-border/50 to-transparent" />
|
||||
|
||||
{/* Enhanced bottom glow when running */}
|
||||
{isRunning && (
|
||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-1/3 h-px bg-gradient-to-r from-transparent via-green-500/50 to-transparent" />
|
||||
<>
|
||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-1/2 h-px bg-gradient-to-r from-transparent via-emerald-500/60 to-transparent" />
|
||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-1/3 h-4 bg-gradient-to-t from-emerald-500/10 to-transparent pointer-events-none" />
|
||||
</>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue