merge: 同步上游 v3.0.0 并更新 uv 依赖锁文件

This commit is contained in:
vinland100 2025-12-25 11:45:52 +08:00
commit e4f1391a28
123 changed files with 6992 additions and 3038 deletions

View File

@ -129,7 +129,7 @@ pnpm dev
### 后端 (Python)
- 使用 Python 3.13+ 类型注解
- 使用 Python 3.11+ 类型注解
- 遵循 PEP 8 代码风格
- 使用 Ruff 进行代码格式化和检查
- 使用 mypy 进行类型检查

674
LICENSE
View File

@ -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
View File

@ -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">
[![Version](https://img.shields.io/badge/version-3.0.1-blue.svg)](https://github.com/lintsinghua/DeepAudit/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Version](https://img.shields.io/badge/version-3.0.2-blue.svg)](https://github.com/lintsinghua/DeepAudit/releases)
[![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
[![React](https://img.shields.io/badge/React-18-61dafb.svg)](https://reactjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178c6.svg)](https://www.typescriptlang.org/)
[![FastAPI](https://img.shields.io/badge/FastAPI-0.100+-009688.svg)](https://fastapi.tiangolo.com/)
[![Python](https://img.shields.io/badge/Python-3.13+-3776ab.svg)](https://www.python.org/)
[![Python](https://img.shields.io/badge/Python-3.11+-3776ab.svg)](https://www.python.org/)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/lintsinghua/DeepAudit)
[![Stars](https://img.shields.io/github/stars/lintsinghua/DeepAudit?style=social)](https://github.com/lintsinghua/DeepAudit/stargazers)
[![Forks](https://img.shields.io/github/forks/lintsinghua/DeepAudit?style=social)](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/)
---
## ⚠️ 重要安全声明
### 法律合规声明

462
README_EN.md Normal file
View File

@ -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">
[![Version](https://img.shields.io/badge/version-3.0.2-blue.svg)](https://github.com/lintsinghua/DeepAudit/releases)
[![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
[![React](https://img.shields.io/badge/React-18-61dafb.svg)](https://reactjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178c6.svg)](https://www.typescriptlang.org/)
[![FastAPI](https://img.shields.io/badge/FastAPI-0.100+-009688.svg)](https://fastapi.tiangolo.com/)
[![Python](https://img.shields.io/badge/Python-3.11+-3776ab.svg)](https://www.python.org/)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/lintsinghua/DeepAudit)
[![Stars](https://img.shields.io/github/stars/lintsinghua/DeepAudit?style=social)](https://github.com/lintsinghua/DeepAudit/stargazers)
[![Forks](https://img.shields.io/github/forks/lintsinghua/DeepAudit?style=social)](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

View File

@ -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("**漏洞代码:**")

View File

@ -292,17 +292,72 @@ 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
@ -322,13 +377,32 @@ async def test_llm_connection(
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(
@ -336,8 +410,9 @@ async def test_llm_connection(
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,
)
# 直接创建新的适配器实例(不使用缓存),确保使用最新的配置
@ -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
)

View File

@ -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)

View File

@ -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

View File

@ -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留空使用提供商默认地址

View File

@ -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():

View File

@ -845,10 +845,9 @@ class BaseAgent(ABC):
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,
)
@ -929,8 +928,8 @@ class BaseAgent(ABC):
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]:
"""
@ -940,8 +939,8 @@ class BaseAgent(ABC):
Args:
messages: 消息列表
temperature: 温度
max_tokens: 最大 token
temperature: 温度None 时使用用户配置
max_tokens: 最大 token None 时使用用户配置
auto_compress: 是否自动压缩过长的消息历史
Returns:
@ -964,7 +963,7 @@ class BaseAgent(ABC):
logger.info(f"[{self.name}] ✅ thinking_start emitted, starting LLM stream...")
try:
# 获取流式迭代器
# 获取流式迭代器(传入 None 时使用用户配置)
stream = self.llm_service.chat_completion_stream(
messages=messages,
temperature=temperature,

View File

@ -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,18 +535,25 @@ 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
@ -1001,11 +1008,46 @@ Action Input: {{"参数": "值"}}
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:

View File

@ -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():

View File

@ -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 HarnessPoC 脚本
- 你可以完全控制测试逻辑
- 参数: 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 Harnessmock 危险函数来检测调用
3. 使用 `run_code` 执行 Harness
4. 分析输出确认漏洞是否触发
### 对于数据泄露型漏洞SQL注入、路径遍历等
1. 获取目标代码
2. 编写 Harnessmock 数据库/文件系统
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)
# 🔥 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 = {}

View File

@ -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

View File

@ -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", "")

View File

@ -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(),
}

View File

@ -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,7 +169,7 @@ 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(
@ -191,13 +195,41 @@ class GetVulnerabilityKnowledgeTool(AgentTool):
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(knowledge.get("content", ""))
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(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:
@ -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):
"""列出知识模块输入"""

View File

@ -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",

View File

@ -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",

View File

@ -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",
]

View File

@ -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]

View File

@ -5,6 +5,7 @@
"""
import logging
import os
import uuid
from datetime import datetime, timezone
from typing import Optional, List, Dict, Any
@ -50,14 +51,17 @@ 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:
@ -126,6 +130,22 @@ 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()

View File

@ -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}'"}

View File

@ -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'

View File

@ -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:
"""
@ -109,6 +114,18 @@ 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):

View File

@ -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:

View File

@ -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]

View File

@ -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
# 解析响应

View File

@ -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]

View File

@ -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" \
@ -90,7 +103,8 @@ class BaseLLMAdapter(ABC):
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:

View File

@ -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)
@ -402,23 +404,29 @@ Please analyze the following code:
# 重新抛出异常,让调用者处理
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 的字典
包含 contentusage 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"])
@ -427,8 +435,60 @@ Please analyze the following code:
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)
@ -446,20 +506,24 @@ 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
@ -467,8 +531,8 @@ Please analyze the following code:
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:
@ -870,12 +934,14 @@ 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)

View File

@ -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年最新推荐)

View File

@ -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)

View File

@ -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

View File

@ -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,6 +398,13 @@ 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 = []
@ -418,7 +428,24 @@ class ReportGenerator:
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

View File

@ -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

View File

@ -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 ============

View File

@ -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]]

View File

@ -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:

View File

@ -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:

View File

@ -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/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

View File

@ -289,8 +289,8 @@ server {
| 依赖 | 版本要求 | 说明 |
|------|---------|------|
| Node.js | 18+ | 前端运行环境 |
| Python | 3.13+ | 后端运行环境 |
| Node.js | 20+ | 前端运行环境 |
| Python | 3.11+ | 后端运行环境 |
| PostgreSQL | 15+ | 数据库 |
| pnpm | 8+ | 推荐的前端包管理器 |
| uv | 最新版 | 推荐的 Python 包管理器 |

View File

@ -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/) | 价格实惠 |
---
## 选择建议

View File

@ -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

View File

@ -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>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem
disableTransitionOnChange={false}
>
<AppWrapper>
<App />
</AppWrapper>
</ThemeProvider>
</ErrorBoundary>
</StrictMode>
);

File diff suppressed because it is too large Load Diff

View File

@ -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>

View File

@ -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>
)}

View File

@ -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>
);

View File

@ -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>

View File

@ -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>

View File

@ -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" />

View File

@ -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">

View File

@ -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} ×

View File

@ -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 ? "未找到匹配的项目" : "暂无可用项目"}

View File

@ -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">

View File

@ -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)}

View File

@ -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-1 flex items-center justify-between">
<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>
<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>

View File

@ -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)}

View File

@ -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)}

View File

@ -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,6 +375,7 @@ 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 justify-between">
<div className="flex items-center gap-2 text-sm">
{llmTestResult.success ? (
<CheckCircle2 className="h-4 w-4 text-emerald-400" />
@ -384,15 +386,99 @@ export function SystemConfig() {
{llmTestResult.message}
</span>
</div>
{llmTestResult.debug && (
<button
onClick={() => setShowDebugInfo(!showDebugInfo)}
className="text-xs text-muted-foreground hover:text-foreground underline"
>
{showDebugInfo ? '隐藏调试信息' : '显示调试信息'}
</button>
)}
</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>

View File

@ -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>

View File

@ -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 />
<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 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",
"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}
/>
);

View File

@ -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}

View File

@ -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",
},

View File

@ -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: {

View File

@ -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}
/>
);

View File

@ -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>
);

View File

@ -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}

View File

@ -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}
/>
))

View File

@ -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
)}

View File

@ -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

View File

@ -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}

View File

@ -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}

View File

@ -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>

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}
/>
));

View File

@ -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}
/>
);

View File

@ -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>

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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;

View File

@ -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}

View File

@ -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: {

View File

@ -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>
);

View File

@ -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>

View File

@ -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" />

View File

@ -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' : ''}

View File

@ -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>
)}

View File

@ -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"
className="absolute border-l-2 border-slate-300 dark:border-slate-600"
style={{
left: `${depth * 16 - 8}px`,
height: '20px',
left: `${indent - 12}px`,
top: 0,
height: isLast ? '20px' : '100%',
}}
/>
)}
{/* Horizontal connector line */}
{depth > 0 && (
{/* 水平线 - 连接到当前节点 */}
<div
className="absolute top-[20px] h-px bg-slate-600"
className="absolute border-t-2 border-slate-300 dark:border-slate-600"
style={{
left: `${depth * 16 - 8}px`,
width: '8px',
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>
);
});

View File

@ -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
)}>

View File

@ -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">
<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-0.5 -right-0.5 w-2 h-2 bg-green-400 rounded-full animate-pulse" />
<>
<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>
<span className="font-bold text-white tracking-wide text-sm">
</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>
</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>
<span className="text-gray-300 text-sm font-mono truncate max-w-[180px]">
<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>
</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