myagent-ai 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +30 -0
- package/README.md +333 -0
- package/agents/__init__.py +6 -0
- package/agents/__pycache__/main_agent.cpython-312.pyc +0 -0
- package/agents/base.py +115 -0
- package/agents/main_agent.py +695 -0
- package/agents/memory_agent.py +313 -0
- package/agents/tool_agent.py +248 -0
- package/chatbot/__init__.py +5 -0
- package/chatbot/base.py +124 -0
- package/chatbot/discord_bot.py +146 -0
- package/chatbot/feishu_bot.py +548 -0
- package/chatbot/manager.py +164 -0
- package/chatbot/qq_bot.py +189 -0
- package/chatbot/telegram_bot.py +167 -0
- package/chatbot/wechat_bot.py +558 -0
- package/communication/__init__.py +66 -0
- package/communication/channel.py +576 -0
- package/communication/crypto.py +347 -0
- package/communication/manager.py +397 -0
- package/communication/peer.py +156 -0
- package/config.py +464 -0
- package/core/__init__.py +10 -0
- package/core/config_broadcast.py +276 -0
- package/core/llm.py +878 -0
- package/core/logger.py +241 -0
- package/core/task_queue.py +362 -0
- package/core/utils.py +184 -0
- package/executor/__init__.py +4 -0
- package/executor/__pycache__/engine.cpython-312.pyc +0 -0
- package/executor/engine.py +1215 -0
- package/groups/__init__.py +15 -0
- package/groups/manager.py +724 -0
- package/knowledge/__init__.py +4 -0
- package/knowledge/rag.py +444 -0
- package/main.py +801 -0
- package/memory/__init__.py +4 -0
- package/memory/manager.py +840 -0
- package/organization/__init__.py +4 -0
- package/organization/manager.py +350 -0
- package/package.json +58 -0
- package/requirements.txt +59 -0
- package/setup.py +40 -0
- package/skills/ASR/LICENSE.txt +21 -0
- package/skills/ASR/SKILL.md +580 -0
- package/skills/ASR/scripts/asr.ts +27 -0
- package/skills/LLM/LICENSE.txt +21 -0
- package/skills/LLM/SKILL.md +856 -0
- package/skills/LLM/scripts/chat.ts +32 -0
- package/skills/TTS/LICENSE.txt +21 -0
- package/skills/TTS/SKILL.md +735 -0
- package/skills/TTS/tts.ts +25 -0
- package/skills/VLM/LICENSE.txt +21 -0
- package/skills/VLM/SKILL.md +588 -0
- package/skills/VLM/scripts/vlm.ts +57 -0
- package/skills/__init__.py +5 -0
- package/skills/agent-browser/SKILL.md +328 -0
- package/skills/ai-news-collectors/SKILL.md +157 -0
- package/skills/ai-news-collectors/_meta.json +6 -0
- package/skills/ai-news-collectors/references/sources.md +128 -0
- package/skills/aminer-open-academic/SKILL.md +312 -0
- package/skills/aminer-open-academic/_meta.json +6 -0
- package/skills/aminer-open-academic/evals/evals.json +46 -0
- package/skills/aminer-open-academic/references/api-catalog.md +1032 -0
- package/skills/aminer-open-academic/scripts/__pycache__/aminer_client.cpython-312.pyc +0 -0
- package/skills/aminer-open-academic/scripts/aminer_client.py +875 -0
- package/skills/auto-target-tracker/SKILL.md +317 -0
- package/skills/base.py +147 -0
- package/skills/blog-writer/2024-02-17-radical-transparency-sales.md +35 -0
- package/skills/blog-writer/2024-02-17-raycast-spotlight-superpowers.md +33 -0
- package/skills/blog-writer/2024-02-17-short-form-content-marketing.md +47 -0
- package/skills/blog-writer/2024-02-17-typing-speed-benefits.md +33 -0
- package/skills/blog-writer/2024-03-14-effective-ai-prompts.md +55 -0
- package/skills/blog-writer/2024-11-08-ai-revolutionizing-entry-level-sales.md +43 -0
- package/skills/blog-writer/2025-11-12-why-ai-art-is-useless.md +49 -0
- package/skills/blog-writer/README.md +2 -0
- package/skills/blog-writer/SKILL.md +158 -0
- package/skills/blog-writer/__pycache__/manage_examples.cpython-312.pyc +0 -0
- package/skills/blog-writer/_meta.json +6 -0
- package/skills/blog-writer/manage_examples.py +90 -0
- package/skills/blog-writer/style-guide.md +160 -0
- package/skills/browser_skill.py +146 -0
- package/skills/coding-agent/SKILL.md +120 -0
- package/skills/coding-agent/_meta.json +6 -0
- package/skills/coding-agent/criteria.md +48 -0
- package/skills/coding-agent/execution.md +42 -0
- package/skills/coding-agent/memory-template.md +38 -0
- package/skills/coding-agent/planning.md +31 -0
- package/skills/coding-agent/state.md +60 -0
- package/skills/coding-agent/verification.md +39 -0
- package/skills/content-strategy/SKILL.md +181 -0
- package/skills/content-strategy/_meta.json +6 -0
- package/skills/contentanalysis/ExtractWisdom/SKILL.md +229 -0
- package/skills/contentanalysis/ExtractWisdom/Workflows/Extract.md +60 -0
- package/skills/contentanalysis/SKILL.md +14 -0
- package/skills/docx/CHANGELOG.md +85 -0
- package/skills/docx/LICENSE.txt +30 -0
- package/skills/docx/SKILL.md +455 -0
- package/skills/docx/docx-js.md +681 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/docx/ooxml/schemas/mce/mc.xsd +75 -0
- package/skills/docx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/docx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/docx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/docx/ooxml/scripts/__pycache__/pack.cpython-312.pyc +0 -0
- package/skills/docx/ooxml/scripts/__pycache__/unpack.cpython-312.pyc +0 -0
- package/skills/docx/ooxml/scripts/__pycache__/validate.cpython-312.pyc +0 -0
- package/skills/docx/ooxml/scripts/pack.py +159 -0
- package/skills/docx/ooxml/scripts/unpack.py +29 -0
- package/skills/docx/ooxml/scripts/validate.py +69 -0
- package/skills/docx/ooxml/scripts/validation/__init__.py +15 -0
- package/skills/docx/ooxml/scripts/validation/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/docx/ooxml/scripts/validation/__pycache__/base.cpython-312.pyc +0 -0
- package/skills/docx/ooxml/scripts/validation/__pycache__/docx.cpython-312.pyc +0 -0
- package/skills/docx/ooxml/scripts/validation/__pycache__/pptx.cpython-312.pyc +0 -0
- package/skills/docx/ooxml/scripts/validation/__pycache__/redlining.cpython-312.pyc +0 -0
- package/skills/docx/ooxml/scripts/validation/base.py +951 -0
- package/skills/docx/ooxml/scripts/validation/docx.py +274 -0
- package/skills/docx/ooxml/scripts/validation/pptx.py +315 -0
- package/skills/docx/ooxml/scripts/validation/redlining.py +279 -0
- package/skills/docx/ooxml.md +615 -0
- package/skills/docx/scripts/__init__.py +1 -0
- package/skills/docx/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/docx/scripts/__pycache__/add_toc_placeholders.cpython-312.pyc +0 -0
- package/skills/docx/scripts/__pycache__/document.cpython-312.pyc +0 -0
- package/skills/docx/scripts/__pycache__/utilities.cpython-312.pyc +0 -0
- package/skills/docx/scripts/add_toc_placeholders.py +220 -0
- package/skills/docx/scripts/document.py +1302 -0
- package/skills/docx/scripts/templates/comments.xml +3 -0
- package/skills/docx/scripts/templates/commentsExtended.xml +3 -0
- package/skills/docx/scripts/templates/commentsExtensible.xml +3 -0
- package/skills/docx/scripts/templates/commentsIds.xml +3 -0
- package/skills/docx/scripts/templates/people.xml +3 -0
- package/skills/docx/scripts/utilities.py +374 -0
- package/skills/dream-interpreter/SKILL.md +88 -0
- package/skills/dream-interpreter/assets/example_asset.txt +24 -0
- package/skills/dream-interpreter/references/api_reference.md +34 -0
- package/skills/dream-interpreter/references/interpretation-guide.md +83 -0
- package/skills/dream-interpreter/references/output-schema.md +65 -0
- package/skills/dream-interpreter/references/questioning-strategy.md +62 -0
- package/skills/dream-interpreter/references/visual-mapping.md +81 -0
- package/skills/dream-interpreter/scripts/__pycache__/example.cpython-312.pyc +0 -0
- package/skills/dream-interpreter/scripts/example.py +19 -0
- package/skills/dream-interpreter/skill.json +7 -0
- package/skills/file_skill.py +246 -0
- package/skills/finance/Finance_API_Doc.md +445 -0
- package/skills/finance/SKILL.md +53 -0
- package/skills/fullstack-dev/SKILL.md +205 -0
- package/skills/get-fortune-analysis/SKILL.md +370 -0
- package/skills/get-fortune-analysis/lunar_python.py +91 -0
- package/skills/gift-evaluator/SKILL.md +83 -0
- package/skills/gift-evaluator/__pycache__/html_tools.cpython-312.pyc +0 -0
- package/skills/gift-evaluator/html_tools.py +268 -0
- package/skills/image-edit/LICENSE.txt +21 -0
- package/skills/image-edit/SKILL.md +896 -0
- package/skills/image-edit/scripts/image-edit.ts +36 -0
- package/skills/image-generation/LICENSE.txt +21 -0
- package/skills/image-generation/SKILL.md +583 -0
- package/skills/image-generation/scripts/image-generation.ts +28 -0
- package/skills/image-understand/LICENSE.txt +21 -0
- package/skills/image-understand/SKILL.md +855 -0
- package/skills/image-understand/scripts/image-understand.ts +41 -0
- package/skills/interview-designer/README.md +70 -0
- package/skills/interview-designer/SKILL.md +53 -0
- package/skills/interview-designer/_meta.json +6 -0
- package/skills/interview-designer/references/design_rationale.md +43 -0
- package/skills/interview-designer/templates/interview_guide_template.md +62 -0
- package/skills/market-research-reports/SKILL.md +901 -0
- package/skills/market-research-reports/assets/FORMATTING_GUIDE.md +428 -0
- package/skills/market-research-reports/assets/market_report_template.tex +1380 -0
- package/skills/market-research-reports/assets/market_research.sty +564 -0
- package/skills/market-research-reports/references/data_analysis_patterns.md +548 -0
- package/skills/market-research-reports/references/report_structure_guide.md +999 -0
- package/skills/market-research-reports/references/visual_generation_guide.md +1077 -0
- package/skills/market-research-reports/scripts/__pycache__/generate_market_visuals.cpython-312.pyc +0 -0
- package/skills/market-research-reports/scripts/generate_market_visuals.py +529 -0
- package/skills/marketing-mode/README.md +49 -0
- package/skills/marketing-mode/SKILL.md +693 -0
- package/skills/marketing-mode/_meta.json +6 -0
- package/skills/marketing-mode/mode-prompt.md +39 -0
- package/skills/marketing-mode/skill.json +51 -0
- package/skills/mindfulness-meditation/SKILL.md +65 -0
- package/skills/mindfulness-meditation/_meta.json +6 -0
- package/skills/multi-search-engine/CHANGELOG.md +15 -0
- package/skills/multi-search-engine/CHANNELLOG.md +48 -0
- package/skills/multi-search-engine/SKILL.md +78 -0
- package/skills/multi-search-engine/_meta.json +6 -0
- package/skills/multi-search-engine/config.json +14 -0
- package/skills/multi-search-engine/metadata.json +7 -0
- package/skills/multi-search-engine/references/international-search.md +651 -0
- package/skills/pdf/LICENSE.txt +30 -0
- package/skills/pdf/SKILL.md +1534 -0
- package/skills/pdf/forms.md +205 -0
- package/skills/pdf/reference.md +765 -0
- package/skills/pdf/scripts/__pycache__/add_zai_metadata.cpython-312.pyc +0 -0
- package/skills/pdf/scripts/__pycache__/check_bounding_boxes.cpython-312.pyc +0 -0
- package/skills/pdf/scripts/__pycache__/check_bounding_boxes_test.cpython-312.pyc +0 -0
- package/skills/pdf/scripts/__pycache__/check_fillable_fields.cpython-312.pyc +0 -0
- package/skills/pdf/scripts/__pycache__/convert_pdf_to_images.cpython-312.pyc +0 -0
- package/skills/pdf/scripts/__pycache__/create_validation_image.cpython-312.pyc +0 -0
- package/skills/pdf/scripts/__pycache__/extract_form_field_info.cpython-312.pyc +0 -0
- package/skills/pdf/scripts/__pycache__/fill_fillable_fields.cpython-312.pyc +0 -0
- package/skills/pdf/scripts/__pycache__/fill_pdf_form_with_annotations.cpython-312.pyc +0 -0
- package/skills/pdf/scripts/__pycache__/sanitize_code.cpython-312.pyc +0 -0
- package/skills/pdf/scripts/add_zai_metadata.py +172 -0
- package/skills/pdf/scripts/check_bounding_boxes.py +70 -0
- package/skills/pdf/scripts/check_bounding_boxes_test.py +226 -0
- package/skills/pdf/scripts/check_fillable_fields.py +12 -0
- package/skills/pdf/scripts/convert_pdf_to_images.py +35 -0
- package/skills/pdf/scripts/create_validation_image.py +41 -0
- package/skills/pdf/scripts/extract_form_field_info.py +152 -0
- package/skills/pdf/scripts/fill_fillable_fields.py +114 -0
- package/skills/pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
- package/skills/pdf/scripts/sanitize_code.py +110 -0
- package/skills/podcast-generate/LICENSE.txt +21 -0
- package/skills/podcast-generate/SKILL.md +198 -0
- package/skills/podcast-generate/generate.ts +661 -0
- package/skills/podcast-generate/package.json +30 -0
- package/skills/podcast-generate/readme.md +177 -0
- package/skills/podcast-generate/test_data/segments.jsonl +3 -0
- package/skills/podcast-generate/tsconfig.json +26 -0
- package/skills/pptx/LICENSE.txt +30 -0
- package/skills/pptx/SKILL.md +507 -0
- package/skills/pptx/html2pptx.md +625 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/pptx/ooxml/schemas/mce/mc.xsd +75 -0
- package/skills/pptx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/pptx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/pptx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/pptx/ooxml/scripts/__pycache__/pack.cpython-312.pyc +0 -0
- package/skills/pptx/ooxml/scripts/__pycache__/unpack.cpython-312.pyc +0 -0
- package/skills/pptx/ooxml/scripts/__pycache__/validate.cpython-312.pyc +0 -0
- package/skills/pptx/ooxml/scripts/pack.py +159 -0
- package/skills/pptx/ooxml/scripts/unpack.py +29 -0
- package/skills/pptx/ooxml/scripts/validate.py +69 -0
- package/skills/pptx/ooxml/scripts/validation/__init__.py +15 -0
- package/skills/pptx/ooxml/scripts/validation/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/pptx/ooxml/scripts/validation/__pycache__/base.cpython-312.pyc +0 -0
- package/skills/pptx/ooxml/scripts/validation/__pycache__/docx.cpython-312.pyc +0 -0
- package/skills/pptx/ooxml/scripts/validation/__pycache__/pptx.cpython-312.pyc +0 -0
- package/skills/pptx/ooxml/scripts/validation/__pycache__/redlining.cpython-312.pyc +0 -0
- package/skills/pptx/ooxml/scripts/validation/base.py +951 -0
- package/skills/pptx/ooxml/scripts/validation/docx.py +274 -0
- package/skills/pptx/ooxml/scripts/validation/pptx.py +315 -0
- package/skills/pptx/ooxml/scripts/validation/redlining.py +279 -0
- package/skills/pptx/ooxml.md +427 -0
- package/skills/pptx/scripts/__pycache__/inventory.cpython-312.pyc +0 -0
- package/skills/pptx/scripts/__pycache__/inventory.cpython-313.pyc +0 -0
- package/skills/pptx/scripts/__pycache__/rearrange.cpython-312.pyc +0 -0
- package/skills/pptx/scripts/__pycache__/replace.cpython-312.pyc +0 -0
- package/skills/pptx/scripts/__pycache__/thumbnail.cpython-312.pyc +0 -0
- package/skills/pptx/scripts/html2pptx.js +1044 -0
- package/skills/pptx/scripts/inventory.py +1020 -0
- package/skills/pptx/scripts/rearrange.py +231 -0
- package/skills/pptx/scripts/replace.py +385 -0
- package/skills/pptx/scripts/thumbnail.py +450 -0
- package/skills/qingyan-research/SKILL.md +294 -0
- package/skills/qingyan-research/__pycache__/generate_html.cpython-312.pyc +0 -0
- package/skills/qingyan-research/generate_html.py +33 -0
- package/skills/registry.py +344 -0
- package/skills/search_skill.py +228 -0
- package/skills/seo-content-writer/SKILL.md +661 -0
- package/skills/seo-content-writer/_meta.json +6 -0
- package/skills/seo-content-writer/references/content-structure-templates.md +875 -0
- package/skills/seo-content-writer/references/title-formulas.md +339 -0
- package/skills/skill-creator/LICENSE.txt +202 -0
- package/skills/skill-creator/SKILL.md +485 -0
- package/skills/skill-creator/agents/analyzer.md +274 -0
- package/skills/skill-creator/agents/comparator.md +202 -0
- package/skills/skill-creator/agents/grader.md +223 -0
- package/skills/skill-creator/assets/eval_review.html +146 -0
- package/skills/skill-creator/eval-viewer/__pycache__/generate_review.cpython-312.pyc +0 -0
- package/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/skills/skill-creator/references/schemas.md +430 -0
- package/skills/skill-creator/scripts/__init__.py +0 -0
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/aggregate_benchmark.cpython-312.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-312.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-312.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/package_skill.cpython-312.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/quick_validate.cpython-312.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-312.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-312.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-312.pyc +0 -0
- package/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/skills/skill-creator/scripts/generate_report.py +326 -0
- package/skills/skill-creator/scripts/improve_description.py +236 -0
- package/skills/skill-creator/scripts/package_skill.py +136 -0
- package/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/skills/skill-creator/scripts/run_eval.py +310 -0
- package/skills/skill-creator/scripts/run_loop.py +328 -0
- package/skills/skill-creator/scripts/utils.py +47 -0
- package/skills/skill-finder-cn/SKILL.md +66 -0
- package/skills/skill-finder-cn/_meta.json +6 -0
- package/skills/skill-finder-cn/package.json +5 -0
- package/skills/skill-finder-cn/scripts/search.sh +15 -0
- package/skills/skill-vetter/SKILL.md +137 -0
- package/skills/stock-analysis-skill/SKILL.md +156 -0
- package/skills/stock-analysis-skill/package.json +21 -0
- package/skills/stock-analysis-skill/src/analyzer.ts +264 -0
- package/skills/stock-analysis-skill/src/dataFetcher.ts +130 -0
- package/skills/stock-analysis-skill/src/dividend.ts +226 -0
- package/skills/stock-analysis-skill/src/index.ts +327 -0
- package/skills/stock-analysis-skill/src/rumorScanner.ts +200 -0
- package/skills/stock-analysis-skill/src/types.ts +167 -0
- package/skills/stock-analysis-skill/src/watchlist.ts +292 -0
- package/skills/stock-analysis-skill/tsconfig.json +15 -0
- package/skills/storyboard-manager/SKILL.md +532 -0
- package/skills/storyboard-manager/index.js +9 -0
- package/skills/storyboard-manager/package.json +11 -0
- package/skills/storyboard-manager/references/character_development.md +232 -0
- package/skills/storyboard-manager/references/story_structures.md +148 -0
- package/skills/storyboard-manager/scripts/__pycache__/consistency_checker.cpython-312.pyc +0 -0
- package/skills/storyboard-manager/scripts/__pycache__/timeline_tracker.cpython-312.pyc +0 -0
- package/skills/storyboard-manager/scripts/consistency_checker.py +391 -0
- package/skills/storyboard-manager/scripts/timeline_tracker.py +352 -0
- package/skills/system_skill.py +249 -0
- package/skills/ui-ux-pro-max/SKILL.md +43 -0
- package/skills/ui-ux-pro-max/_meta.json +6 -0
- package/skills/ui-ux-pro-max/assets/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/assets/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/assets/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/assets/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/assets/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/assets/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/assets/data/styles.csv +68 -0
- package/skills/ui-ux-pro-max/assets/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/assets/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/assets/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/assets/data/web-interface.csv +31 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills/ui-ux-pro-max/references/upstream-README.md +488 -0
- package/skills/ui-ux-pro-max/references/upstream-skill-content.md +288 -0
- package/skills/ui-ux-pro-max/scripts/__init__.py +0 -0
- package/skills/ui-ux-pro-max/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-312.pyc +0 -0
- package/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-312.pyc +0 -0
- package/skills/ui-ux-pro-max/scripts/__pycache__/search.cpython-312.pyc +0 -0
- package/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/skills/ui-ux-pro-max/scripts/design_system.py +1071 -0
- package/skills/ui-ux-pro-max/scripts/search.py +111 -0
- package/skills/video-generation/LICENSE.txt +21 -0
- package/skills/video-generation/SKILL.md +1082 -0
- package/skills/video-generation/scripts/video.ts +168 -0
- package/skills/video-understand/LICENSE.txt +21 -0
- package/skills/video-understand/SKILL.md +916 -0
- package/skills/video-understand/scripts/video-understand.ts +41 -0
- package/skills/visual-design-foundations/SKILL.md +318 -0
- package/skills/visual-design-foundations/references/color-systems.md +417 -0
- package/skills/visual-design-foundations/references/spacing-iconography.md +425 -0
- package/skills/visual-design-foundations/references/typography-systems.md +432 -0
- package/skills/web-reader/LICENSE.txt +21 -0
- package/skills/web-reader/SKILL.md +1140 -0
- package/skills/web-reader/scripts/web-reader.ts +37 -0
- package/skills/web-search/LICENSE.txt +21 -0
- package/skills/web-search/SKILL.md +912 -0
- package/skills/web-search/scripts/web_search.ts +44 -0
- package/skills/web-shader-extractor/SKILL.md +145 -0
- package/skills/web-shader-extractor/references/config-extraction.md +50 -0
- package/skills/web-shader-extractor/references/encoded-definitions.md +53 -0
- package/skills/web-shader-extractor/references/extraction-workflow.md +61 -0
- package/skills/web-shader-extractor/references/porting-strategy.md +164 -0
- package/skills/web-shader-extractor/references/shader-injection.md +126 -0
- package/skills/web-shader-extractor/references/shaders-com.md +190 -0
- package/skills/web-shader-extractor/references/tech-signatures.md +54 -0
- package/skills/web-shader-extractor/references/tsl-extraction.md +41 -0
- package/skills/web-shader-extractor/references/unicorn-studio.md +353 -0
- package/skills/web-shader-extractor/scripts/fetch-rendered-dom.mjs +153 -0
- package/skills/web-shader-extractor/scripts/scan-bundle.sh +76 -0
- package/skills/writing-plans/SKILL.md +116 -0
- package/skills/writing-plans/_meta.json +6 -0
- package/skills/xlsx/LICENSE.txt +30 -0
- package/skills/xlsx/SKILL.md +496 -0
- package/skills/xlsx/__pycache__/recalc.cpython-312.pyc +0 -0
- package/skills/xlsx/recalc.py +178 -0
- package/start.sh +36 -0
- package/web/__init__.py +1 -0
- package/web/__pycache__/api_server.cpython-312.pyc +0 -0
- package/web/api_server.py +2043 -0
- package/web/ui/chat.html +3235 -0
- package/web/ui/index.html +458 -0
|
@@ -0,0 +1,2043 @@
|
|
|
1
|
+
"""
|
|
2
|
+
web/api_server.py - 管理后台 HTTP API
|
|
3
|
+
======================================
|
|
4
|
+
为管理 UI 提供 RESTful API。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import asyncio, json, os, time, shutil
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from aiohttp import web
|
|
11
|
+
from core.logger import get_logger
|
|
12
|
+
from core.llm import Message
|
|
13
|
+
from config import ModelEntry
|
|
14
|
+
import datetime
|
|
15
|
+
|
|
16
|
+
logger = get_logger("myagent.api")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _agent_color(name: str) -> str:
|
|
20
|
+
"""Generate a consistent color for an agent based on its name."""
|
|
21
|
+
colors = ['#4f46e5','#7c3aed','#ec4899','#ef4444','#f59e0b','#10b981',
|
|
22
|
+
'#06b6d4','#3b82f6','#8b5cf6','#f97316','#14b8a6','#6366f1']
|
|
23
|
+
h = sum(ord(c) for c in name)
|
|
24
|
+
return colors[h % len(colors)]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ApiServer:
|
|
28
|
+
def __init__(self, app_core):
|
|
29
|
+
self.core = app_core
|
|
30
|
+
self.app = web.Application()
|
|
31
|
+
self._exec_progress: dict = {}
|
|
32
|
+
# Wrap the executor to track progress in real-time (deferred to initialize())
|
|
33
|
+
self._executor_wrapped = False
|
|
34
|
+
self._setup_routes()
|
|
35
|
+
self._runner: Optional[web.AppRunner] = None
|
|
36
|
+
|
|
37
|
+
def _setup_routes(self):
|
|
38
|
+
r = self.app.router
|
|
39
|
+
r.add_get("/api/status", self.handle_status)
|
|
40
|
+
r.add_post("/api/shutdown", self.handle_shutdown)
|
|
41
|
+
r.add_get("/api/agents", self.handle_list_agents)
|
|
42
|
+
r.add_get("/api/agents/tree", self.handle_agents_tree)
|
|
43
|
+
r.add_post("/api/agents", self.handle_create_agent)
|
|
44
|
+
r.add_get("/api/agents/{name:[a-zA-Z0-9_/-]+}", self.handle_get_agent)
|
|
45
|
+
r.add_put("/api/agents/{name:[a-zA-Z0-9_/-]+}", self.handle_update_agent)
|
|
46
|
+
r.add_delete("/api/agents/{name:[a-zA-Z0-9_/-]+}", self.handle_delete_agent)
|
|
47
|
+
r.add_get("/api/agents/{name:[a-zA-Z0-9_/-]+}/soul", self.handle_get_soul)
|
|
48
|
+
r.add_put("/api/agents/{name:[a-zA-Z0-9_/-]+}/soul", self.handle_set_soul)
|
|
49
|
+
r.add_get("/api/agents/{name:[a-zA-Z0-9_/-]+}/identity", self.handle_get_identity)
|
|
50
|
+
r.add_put("/api/agents/{name:[a-zA-Z0-9_/-]+}/identity", self.handle_set_identity)
|
|
51
|
+
r.add_get("/api/agents/{name:[a-zA-Z0-9_/-]+}/user", self.handle_get_user)
|
|
52
|
+
r.add_put("/api/agents/{name:[a-zA-Z0-9_/-]+}/user", self.handle_set_user)
|
|
53
|
+
r.add_get("/api/agents/{name:[a-zA-Z0-9_/-]+}/sessions", self.handle_agent_sessions)
|
|
54
|
+
r.add_get("/api/agents/{name:[a-zA-Z0-9_/-]+}/children", self.handle_list_children)
|
|
55
|
+
r.add_post("/api/agents/{name:[a-zA-Z0-9_/-]+}/children", self.handle_create_child)
|
|
56
|
+
r.add_get("/api/platforms", self.handle_list_platforms)
|
|
57
|
+
r.add_put("/api/platforms/{name}", self.handle_update_platform)
|
|
58
|
+
# ── 模型库 CRUD ──
|
|
59
|
+
r.add_get("/api/models", self.handle_list_models)
|
|
60
|
+
r.add_post("/api/models", self.handle_add_model)
|
|
61
|
+
r.add_put("/api/models/{model_id}", self.handle_update_model)
|
|
62
|
+
r.add_delete("/api/models/{model_id}", self.handle_delete_model)
|
|
63
|
+
# ── Agent 绑定查询 ──
|
|
64
|
+
r.add_get("/api/agents/{name:[a-zA-Z0-9_/-]+}/bindings", self.handle_agent_bindings)
|
|
65
|
+
r.add_get("/api/sessions", self.handle_list_sessions)
|
|
66
|
+
r.add_get("/api/sessions/{sid}/messages", self.handle_get_messages)
|
|
67
|
+
r.add_delete("/api/sessions/{sid}", self.handle_clear_session)
|
|
68
|
+
r.add_get("/api/memory/stats", self.handle_memory_stats)
|
|
69
|
+
r.add_get("/api/memory/search", self.handle_memory_search)
|
|
70
|
+
r.add_get("/api/memory/long-term", self.handle_list_long_term)
|
|
71
|
+
r.add_delete("/api/memory/long-term/{mid}", self.handle_delete_long_term)
|
|
72
|
+
r.add_post("/api/memory/cleanup", self.handle_memory_cleanup)
|
|
73
|
+
r.add_get("/api/llm", self.handle_get_llm)
|
|
74
|
+
r.add_put("/api/llm", self.handle_update_llm)
|
|
75
|
+
r.add_post("/api/llm/test", self.handle_test_llm)
|
|
76
|
+
r.add_get("/api/llm/usage", self.handle_llm_usage)
|
|
77
|
+
r.add_get("/api/skills", self.handle_list_skills)
|
|
78
|
+
r.add_get("/api/skills/{name}", self.handle_get_skill)
|
|
79
|
+
r.add_get("/api/executor", self.handle_get_executor)
|
|
80
|
+
r.add_put("/api/executor", self.handle_update_executor)
|
|
81
|
+
r.add_get("/api/workdir", self.handle_get_workdir)
|
|
82
|
+
# ── Task Plan ──
|
|
83
|
+
r.add_get("/api/task-plan", self.handle_get_task_plan)
|
|
84
|
+
r.add_put("/api/task-plan", self.handle_update_task_plan)
|
|
85
|
+
r.add_post("/api/task-plan", self.handle_add_task_item)
|
|
86
|
+
r.add_delete("/api/task-plan/{idx:int}", self.handle_delete_task_item)
|
|
87
|
+
r.add_put("/api/workdir", self.handle_set_workdir)
|
|
88
|
+
r.add_get("/api/workdir/files", self.handle_list_workdir)
|
|
89
|
+
r.add_get("/api/logs", self.handle_get_logs)
|
|
90
|
+
r.add_get("/api/logs/stream", self.handle_log_stream)
|
|
91
|
+
r.add_post("/api/chat", self.handle_chat)
|
|
92
|
+
r.add_get("/chat", self.handle_chat_page)
|
|
93
|
+
r.add_get("/api/execution/progress", self.handle_execution_progress)
|
|
94
|
+
# ── 组织管理 ──
|
|
95
|
+
r.add_get("/api/organization", self.handle_get_organization)
|
|
96
|
+
r.add_put("/api/organization", self.handle_update_organization)
|
|
97
|
+
r.add_get("/api/organization/info", self.handle_get_org_info)
|
|
98
|
+
r.add_put("/api/organization/info", self.handle_update_org_info)
|
|
99
|
+
r.add_get("/api/organization/knowledge", self.handle_list_org_knowledge)
|
|
100
|
+
r.add_post("/api/organization/knowledge/upload", self.handle_upload_org_knowledge)
|
|
101
|
+
r.add_delete("/api/organization/knowledge", self.handle_delete_org_knowledge)
|
|
102
|
+
# ── Agent 知识库 ──
|
|
103
|
+
r.add_get("/api/agents/{name:[a-zA-Z0-9_/-]+}/knowledge", self.handle_list_agent_knowledge)
|
|
104
|
+
r.add_post("/api/agents/{name:[a-zA-Z0-9_/-]+}/knowledge/upload", self.handle_upload_agent_knowledge)
|
|
105
|
+
r.add_delete("/api/agents/{name:[a-zA-Z0-9_/-]+}/knowledge", self.handle_delete_agent_knowledge)
|
|
106
|
+
# ── 知识库 RAG 搜索 ──
|
|
107
|
+
r.add_post("/api/knowledge/search", self.handle_knowledge_search)
|
|
108
|
+
# ── 配置管理 (热重载/导入/导出) ──
|
|
109
|
+
r.add_get("/api/config", self.handle_get_config)
|
|
110
|
+
r.add_post("/api/config/reload", self.handle_reload_config)
|
|
111
|
+
r.add_post("/api/config/export", self.handle_export_config)
|
|
112
|
+
r.add_post("/api/config/import", self.handle_import_config)
|
|
113
|
+
# ── Agent 间通信 ──
|
|
114
|
+
r.add_get("/api/communication", self.handle_get_communication)
|
|
115
|
+
r.add_put("/api/communication", self.handle_update_communication)
|
|
116
|
+
r.add_get("/api/communication/status", self.handle_comm_status)
|
|
117
|
+
r.add_get("/api/communication/peers", self.handle_list_peers)
|
|
118
|
+
r.add_post("/api/communication/peers", self.handle_add_peer)
|
|
119
|
+
r.add_delete("/api/communication/peers/{agent_id}", self.handle_remove_peer)
|
|
120
|
+
r.add_get("/api/communication/messages", self.handle_comm_messages)
|
|
121
|
+
r.add_post("/api/communication/messages", self.handle_send_message)
|
|
122
|
+
r.add_post("/api/communication/messages/{msg_id}/ack", self.handle_ack_message)
|
|
123
|
+
# ── 群聊管理 ──
|
|
124
|
+
r.add_get("/api/groups", self.handle_list_groups)
|
|
125
|
+
r.add_post("/api/groups", self.handle_create_group)
|
|
126
|
+
r.add_get("/api/groups/{gid}", self.handle_get_group)
|
|
127
|
+
r.add_put("/api/groups/{gid}", self.handle_update_group)
|
|
128
|
+
r.add_delete("/api/groups/{gid}", self.handle_delete_group)
|
|
129
|
+
r.add_get("/api/groups/{gid}/stats", self.handle_group_stats)
|
|
130
|
+
# ── 群成员管理 ──
|
|
131
|
+
r.add_post("/api/groups/{gid}/members", self.handle_add_member)
|
|
132
|
+
r.add_delete("/api/groups/{gid}/members/{agent_path:[a-zA-Z0-9_/-]+}", self.handle_remove_member)
|
|
133
|
+
r.add_put("/api/groups/{gid}/members/{agent_path:[a-zA-Z0-9_/-]+}/role", self.handle_set_member_role)
|
|
134
|
+
r.add_put("/api/groups/{gid}/members/{agent_path:[a-zA-Z0-9_/-]+}/mute", self.handle_set_member_muted)
|
|
135
|
+
# ── 群消息 ──
|
|
136
|
+
r.add_get("/api/groups/{gid}/messages", self.handle_get_group_messages)
|
|
137
|
+
r.add_post("/api/groups/{gid}/messages", self.handle_send_group_message)
|
|
138
|
+
r.add_delete("/api/groups/{gid}/messages", self.handle_clear_group_messages)
|
|
139
|
+
ui_dir = Path(__file__).parent / "ui"
|
|
140
|
+
if ui_dir.exists():
|
|
141
|
+
r.add_static("/ui", str(ui_dir))
|
|
142
|
+
# Defer executor tracker installation until initialize() is called # This avoids issues during route setup # The tracker is installed in _install_executor_tracker() self._install_executor_tracker() r.add_get("/", self.handle_index)
|
|
143
|
+
|
|
144
|
+
async def handle_index(self, request):
|
|
145
|
+
raise web.HTTPFound("/ui/chat.html")
|
|
146
|
+
|
|
147
|
+
def _get_group_manager(self):
|
|
148
|
+
"""获取群聊管理器实例(懒加载)"""
|
|
149
|
+
if not hasattr(self, '_group_manager'):
|
|
150
|
+
from groups.manager import GroupManager
|
|
151
|
+
self._group_manager = GroupManager(
|
|
152
|
+
data_dir=self.core.config_mgr.data_dir
|
|
153
|
+
)
|
|
154
|
+
self._group_manager.initialize()
|
|
155
|
+
return self._group_manager
|
|
156
|
+
|
|
157
|
+
# --- Execution Progress ---
|
|
158
|
+
async def handle_execution_progress(self, request):
|
|
159
|
+
"""GET /api/execution/progress - 返回当前执行进度"""
|
|
160
|
+
active = []
|
|
161
|
+
now = time.time()
|
|
162
|
+
for eid, info in list(self._exec_progress.items()):
|
|
163
|
+
if info["status"] == "running":
|
|
164
|
+
info["elapsed"] = now - info["start_time"]
|
|
165
|
+
active.append({"id": eid, **info})
|
|
166
|
+
return web.json_response({
|
|
167
|
+
"active": active,
|
|
168
|
+
"has_running": any(i["status"] == "running" for i in active),
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
# --- Chat ---
|
|
172
|
+
async def handle_chat(self, request):
|
|
173
|
+
"""POST /api/chat - 聊天消息处理"""
|
|
174
|
+
try:
|
|
175
|
+
data = await request.json()
|
|
176
|
+
except Exception:
|
|
177
|
+
return web.json_response({"error": "invalid JSON"}, status=400)
|
|
178
|
+
|
|
179
|
+
message = data.get("message", "").strip()
|
|
180
|
+
if not message:
|
|
181
|
+
return web.json_response({"error": "message is required"}, status=400)
|
|
182
|
+
|
|
183
|
+
agent_name = data.get("agent_name", "default") or "default"
|
|
184
|
+
# 支持 path 格式 (如 "coder/python-expert")
|
|
185
|
+
agent_path = data.get("agent_path", agent_name)
|
|
186
|
+
raw_session_id = data.get("session_id", "") or "web_default"
|
|
187
|
+
session_id = f"{agent_path}_{raw_session_id}"
|
|
188
|
+
chat_mode = data.get("mode", "") # "exec" = 执行模式
|
|
189
|
+
|
|
190
|
+
# ── 执行模式: 注入任务规划上下文 ──
|
|
191
|
+
task_plan_context = ""
|
|
192
|
+
if chat_mode == "exec":
|
|
193
|
+
tp = self._task_md_path(agent_path)
|
|
194
|
+
if tp.exists():
|
|
195
|
+
existing_content = tp.read_text(encoding="utf-8")
|
|
196
|
+
tasks = self._parse_task_md(existing_content)
|
|
197
|
+
if tasks:
|
|
198
|
+
pending = [f" - [ ] {t['text']}" for t in tasks if not t['done']]
|
|
199
|
+
done = [f" - [x] {t['text']}" for t in tasks if t['done']]
|
|
200
|
+
task_plan_context = "\n\n## 当前任务计划 (task.md)\n"
|
|
201
|
+
if done:
|
|
202
|
+
task_plan_context += "已完成:\n" + "\n".join(done) + "\n"
|
|
203
|
+
if pending:
|
|
204
|
+
task_plan_context += "待完成:\n" + "\n".join(pending) + "\n"
|
|
205
|
+
task_plan_context += "\n请在回复中包含更新后的任务计划。用以下格式列出任务:\n```\n## 任务计划\n- [ ] 任务1\n- [ ] 任务2\n```\n"
|
|
206
|
+
|
|
207
|
+
if task_plan_context:
|
|
208
|
+
message = message + task_plan_context
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
# 检查 Agent 是否指定了特定模型(含备用模型列表)
|
|
212
|
+
agent_cfg = self._read_agent_config(agent_path)
|
|
213
|
+
model_chain = self._build_model_chain(agent_cfg, agent_path)
|
|
214
|
+
|
|
215
|
+
if model_chain and self.core.llm:
|
|
216
|
+
# 依次尝试主模型和备用模型
|
|
217
|
+
response = await self._try_model_chain(
|
|
218
|
+
model_chain, message, session_id
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
response = await self.core.process_message(message, session_id)
|
|
222
|
+
|
|
223
|
+
# 保存到记忆
|
|
224
|
+
if self.core.memory:
|
|
225
|
+
self.core.memory.add_short_term(
|
|
226
|
+
session_id=session_id, role="user", content=message,
|
|
227
|
+
)
|
|
228
|
+
self.core.memory.add_short_term(
|
|
229
|
+
session_id=session_id, role="assistant", content=response,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# ── 执行模式: 从回复中提取并更新任务计划 ──
|
|
233
|
+
if chat_mode == "exec" and self.core.llm:
|
|
234
|
+
try:
|
|
235
|
+
await self._extract_and_update_task_plan(agent_path, response)
|
|
236
|
+
except Exception as tp_err:
|
|
237
|
+
logger.warning(f"任务计划更新失败: {tp_err}")
|
|
238
|
+
|
|
239
|
+
return web.json_response({"response": response, "session_id": session_id, "agent_name": agent_path, "agent_path": agent_path})
|
|
240
|
+
except Exception as e:
|
|
241
|
+
logger.error(f"Chat error: {e}", exc_info=True)
|
|
242
|
+
return web.json_response({"error": str(e)}, status=500)
|
|
243
|
+
|
|
244
|
+
async def handle_chat_page(self, request):
|
|
245
|
+
"""GET /chat - 重定向到聊天页面"""
|
|
246
|
+
raise web.HTTPFound("/ui/chat.html")
|
|
247
|
+
|
|
248
|
+
# --- System ---
|
|
249
|
+
async def handle_status(self, request):
|
|
250
|
+
c = self.core
|
|
251
|
+
return web.json_response({
|
|
252
|
+
"running": c._running, "provider": c.config.llm.provider,
|
|
253
|
+
"model": c.config.llm.model, "session": c._session_id,
|
|
254
|
+
"memory": c.memory.get_stats() if c.memory else {},
|
|
255
|
+
"queue": c.task_queue.get_stats() if c.task_queue else {},
|
|
256
|
+
"skills": len(c.skill_registry.list_skills()) if c.skill_registry else 0,
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
# ── Task Plan (task.md) ──
|
|
260
|
+
def _task_md_path(self, agent_path: str = "default") -> Path:
|
|
261
|
+
"""Return the task.md path for a given agent."""
|
|
262
|
+
ad = self._agent_dir(agent_path)
|
|
263
|
+
ad.mkdir(parents=True, exist_ok=True)
|
|
264
|
+
return ad / "task.md"
|
|
265
|
+
|
|
266
|
+
def _parse_task_md(self, content: str) -> list:
|
|
267
|
+
"""Parse task.md content into list of {text, done, raw_line}."""
|
|
268
|
+
tasks = []
|
|
269
|
+
for line in content.splitlines():
|
|
270
|
+
stripped = line.strip()
|
|
271
|
+
if not stripped or stripped.startswith('#'):
|
|
272
|
+
continue
|
|
273
|
+
# Match markdown checkboxes: - [ ] / - [x] / - [X]
|
|
274
|
+
import re
|
|
275
|
+
m = re.match(r'^-\s*\[([ xX])\]\s*(.+)$', stripped)
|
|
276
|
+
if m:
|
|
277
|
+
tasks.append({"done": m.group(1).lower() == 'x', "text": m.group(2).strip()})
|
|
278
|
+
return tasks
|
|
279
|
+
|
|
280
|
+
def _serialize_task_md(self, tasks: list) -> str:
|
|
281
|
+
"""Serialize task list to markdown."""
|
|
282
|
+
lines = ["# Task Plan\n"]
|
|
283
|
+
for t in tasks:
|
|
284
|
+
ck = "x" if t.get("done") else " "
|
|
285
|
+
lines.append(f"- [{ck}] {t.get('text', '')}")
|
|
286
|
+
return "\n".join(lines)
|
|
287
|
+
|
|
288
|
+
async def _extract_and_update_task_plan(self, agent_path: str, response_text: str):
|
|
289
|
+
"""从 Agent 回复中提取任务计划并更新 task.md"""
|
|
290
|
+
import re
|
|
291
|
+
|
|
292
|
+
# 尝试从回复中提取任务计划
|
|
293
|
+
# 匹配格式: ## 任务计划 / ## Task Plan 后跟 - [ ] / - [x] 列表
|
|
294
|
+
tasks_found = []
|
|
295
|
+
in_task_section = False
|
|
296
|
+
for line in response_text.splitlines():
|
|
297
|
+
stripped = line.strip()
|
|
298
|
+
if re.match(r'^##\s*(任务计划|task\s*plan)', stripped, re.IGNORECASE):
|
|
299
|
+
in_task_section = True
|
|
300
|
+
continue
|
|
301
|
+
if in_task_section:
|
|
302
|
+
if stripped.startswith('#'):
|
|
303
|
+
break # 遇到下一个标题,结束
|
|
304
|
+
m = re.match(r'^-\s*\[([ xX])\]\s*(.+)$', stripped)
|
|
305
|
+
if m:
|
|
306
|
+
tasks_found.append({"done": m.group(1).lower() == 'x', "text": m.group(2).strip()})
|
|
307
|
+
|
|
308
|
+
if tasks_found:
|
|
309
|
+
tp = self._task_md_path(agent_path)
|
|
310
|
+
tp.write_text(self._serialize_task_md(tasks_found), encoding="utf-8")
|
|
311
|
+
logger.info(f"[exec模式] 任务计划已更新: {agent_path} ({len(tasks_found)} 项任务)")
|
|
312
|
+
|
|
313
|
+
async def handle_get_task_plan(self, request):
|
|
314
|
+
"""GET /api/task-plan?agent=default - Get task plan for an agent."""
|
|
315
|
+
agent_path = request.query.get("agent", "default")
|
|
316
|
+
tp = self._task_md_path(agent_path)
|
|
317
|
+
if not tp.exists():
|
|
318
|
+
return web.json_response({"agent": agent_path, "tasks": [], "raw": ""})
|
|
319
|
+
content = tp.read_text(encoding="utf-8")
|
|
320
|
+
tasks = self._parse_task_md(content)
|
|
321
|
+
return web.json_response({"agent": agent_path, "tasks": tasks, "raw": content})
|
|
322
|
+
|
|
323
|
+
async def handle_update_task_plan(self, request):
|
|
324
|
+
"""PUT /api/task-plan - Overwrite entire task plan."""
|
|
325
|
+
data = await request.json()
|
|
326
|
+
agent_path = data.get("agent", "default")
|
|
327
|
+
tasks = data.get("tasks", [])
|
|
328
|
+
tp = self._task_md_path(agent_path)
|
|
329
|
+
content = self._serialize_task_md(tasks)
|
|
330
|
+
tp.write_text(content, encoding="utf-8")
|
|
331
|
+
return web.json_response({"ok": True, "agent": agent_path, "tasks": tasks})
|
|
332
|
+
|
|
333
|
+
async def handle_add_task_item(self, request):
|
|
334
|
+
"""POST /api/task-plan - Add a new task item."""
|
|
335
|
+
data = await request.json()
|
|
336
|
+
agent_path = data.get("agent", "default")
|
|
337
|
+
text = data.get("text", "").strip()
|
|
338
|
+
if not text:
|
|
339
|
+
return web.json_response({"error": "text is required"}, status=400)
|
|
340
|
+
tp = self._task_md_path(agent_path)
|
|
341
|
+
content = tp.read_text(encoding="utf-8") if tp.exists() else ""
|
|
342
|
+
tasks = self._parse_task_md(content)
|
|
343
|
+
tasks.append({"done": False, "text": text})
|
|
344
|
+
tp.write_text(self._serialize_task_md(tasks), encoding="utf-8")
|
|
345
|
+
return web.json_response({"ok": True, "tasks": tasks, "added": len(tasks) - 1})
|
|
346
|
+
|
|
347
|
+
async def handle_delete_task_item(self, request):
|
|
348
|
+
"""DELETE /api/task-plan/{idx} - Delete task by index."""
|
|
349
|
+
idx = int(request.match_info["idx"])
|
|
350
|
+
agent_path = request.query.get("agent", "default")
|
|
351
|
+
tp = self._task_md_path(agent_path)
|
|
352
|
+
if not tp.exists():
|
|
353
|
+
return web.json_response({"error": "task plan not found"}, status=404)
|
|
354
|
+
content = tp.read_text(encoding="utf-8")
|
|
355
|
+
tasks = self._parse_task_md(content)
|
|
356
|
+
if 0 <= idx < len(tasks):
|
|
357
|
+
tasks.pop(idx)
|
|
358
|
+
tp.write_text(self._serialize_task_md(tasks), encoding="utf-8")
|
|
359
|
+
return web.json_response({"ok": True, "tasks": tasks})
|
|
360
|
+
|
|
361
|
+
async def handle_shutdown(self, request):
|
|
362
|
+
self.core._running = False
|
|
363
|
+
asyncio.create_task(self.core.shutdown())
|
|
364
|
+
return web.json_response({"ok": True})
|
|
365
|
+
|
|
366
|
+
# --- Agents (层级体系) ---
|
|
367
|
+
# 目录结构: agents/default/{config.json, soul.md, ...}
|
|
368
|
+
# agents/coder/{config.json, soul.md, ...}
|
|
369
|
+
# agents/coder/python-expert/{config.json, soul.md, ...}
|
|
370
|
+
# agent path = 相对于 agents/ 的路径, 如 "default", "coder", "coder/python-expert"
|
|
371
|
+
|
|
372
|
+
def _agents_dir(self):
|
|
373
|
+
d = self.core.config_mgr.data_dir / "agents"
|
|
374
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
375
|
+
return d
|
|
376
|
+
|
|
377
|
+
def _agent_dir(self, path: str) -> Path:
|
|
378
|
+
"""根据 agent path 返回目录 (path 如 'coder/python-expert')"""
|
|
379
|
+
return self._agents_dir() / path
|
|
380
|
+
|
|
381
|
+
def _ensure_default_agent(self):
|
|
382
|
+
"""确保默认 agent 存在"""
|
|
383
|
+
ad = self._agent_dir("default")
|
|
384
|
+
if not (ad / "config.json").exists():
|
|
385
|
+
ad.mkdir(parents=True, exist_ok=True)
|
|
386
|
+
cfg = {
|
|
387
|
+
"name": "default",
|
|
388
|
+
"description": "默认助手 - 本机运行模式",
|
|
389
|
+
"avatar_color": _agent_color("default"),
|
|
390
|
+
"avatar_emoji": "🤖",
|
|
391
|
+
"execution_mode": "local",
|
|
392
|
+
"enabled": True,
|
|
393
|
+
"system_prompt": "你是 MyAgent 默认助手,运行在本机模式。请用友好、专业的方式回答用户的问题。",
|
|
394
|
+
}
|
|
395
|
+
(ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
|
|
396
|
+
for fn, default in [
|
|
397
|
+
("soul.md", "# Default Agent\n\n## 性格\n专业、友好的AI助手\n"),
|
|
398
|
+
("identity.md", "# Default Agent\n\n## 身份\nMyAgent 默认AI助手\n"),
|
|
399
|
+
("user.md", "# 用户信息\n\n## 用户偏好\n<!-- 在此处记录用户偏好 -->\n"),
|
|
400
|
+
]:
|
|
401
|
+
if not (ad / fn).exists():
|
|
402
|
+
(ad / fn).write_text(default)
|
|
403
|
+
logger.info("已创建默认 Agent")
|
|
404
|
+
|
|
405
|
+
def _read_agent_config(self, path: str) -> dict | None:
|
|
406
|
+
"""读取 agent 配置"""
|
|
407
|
+
cfg_file = self._agent_dir(path) / "config.json"
|
|
408
|
+
if not cfg_file.exists():
|
|
409
|
+
return None
|
|
410
|
+
return json.loads(cfg_file.read_text())
|
|
411
|
+
|
|
412
|
+
def _write_agent_config(self, path: str, cfg: dict):
|
|
413
|
+
"""写入 agent 配置"""
|
|
414
|
+
ad = self._agent_dir(path)
|
|
415
|
+
ad.mkdir(parents=True, exist_ok=True)
|
|
416
|
+
(ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
|
|
417
|
+
|
|
418
|
+
def _scan_agents_flat(self, base_dir: Path = None, prefix: str = "") -> list[dict]:
|
|
419
|
+
"""递归扫描所有 agent,返回扁平列表"""
|
|
420
|
+
if base_dir is None:
|
|
421
|
+
base_dir = self._agents_dir()
|
|
422
|
+
agents = []
|
|
423
|
+
if not base_dir.exists():
|
|
424
|
+
return agents
|
|
425
|
+
for d in sorted(base_dir.iterdir()):
|
|
426
|
+
if not d.is_dir():
|
|
427
|
+
continue
|
|
428
|
+
cfg_file = d / "config.json"
|
|
429
|
+
if not cfg_file.exists():
|
|
430
|
+
continue
|
|
431
|
+
agent_path = f"{prefix}{d.name}" if not prefix else f"{prefix}/{d.name}"
|
|
432
|
+
cfg = json.loads(cfg_file.read_text())
|
|
433
|
+
agent = {"path": agent_path, "name": d.name, **cfg}
|
|
434
|
+
agent["avatar_color"] = cfg.get("avatar_color") or _agent_color(d.name)
|
|
435
|
+
agent["depth"] = agent_path.count("/")
|
|
436
|
+
agents.append(agent)
|
|
437
|
+
# 递归子目录
|
|
438
|
+
agents.extend(self._scan_agents_flat(d, agent_path))
|
|
439
|
+
return agents
|
|
440
|
+
|
|
441
|
+
def _build_agent_tree(self, agents_flat: list[dict]) -> list[dict]:
|
|
442
|
+
"""将扁平 agent 列表构建为树结构"""
|
|
443
|
+
# 按 path 索引
|
|
444
|
+
by_path = {a["path"]: {**a, "children": []} for a in agents_flat}
|
|
445
|
+
roots = []
|
|
446
|
+
for a in agents_flat:
|
|
447
|
+
path = a["path"]
|
|
448
|
+
parent_path = "/".join(path.split("/")[:-1]) if "/" in path else None
|
|
449
|
+
node = by_path[path]
|
|
450
|
+
if parent_path and parent_path in by_path:
|
|
451
|
+
by_path[parent_path]["children"].append(node)
|
|
452
|
+
else:
|
|
453
|
+
roots.append(node)
|
|
454
|
+
return roots
|
|
455
|
+
|
|
456
|
+
async def handle_list_agents(self, request):
|
|
457
|
+
"""GET /api/agents - 返回扁平 agent 列表"""
|
|
458
|
+
self._ensure_default_agent()
|
|
459
|
+
agents = self._scan_agents_flat()
|
|
460
|
+
# 统计会话数
|
|
461
|
+
if self.core.memory:
|
|
462
|
+
rows = self.core.memory._get_conn().execute(
|
|
463
|
+
"SELECT session_id, COUNT(*) as cnt FROM memories "
|
|
464
|
+
"WHERE category='short_term' GROUP BY session_id").fetchall()
|
|
465
|
+
session_counts = {}
|
|
466
|
+
for r in rows:
|
|
467
|
+
sid = r["session_id"]
|
|
468
|
+
for ap in [sid.split("_")[0], sid]:
|
|
469
|
+
session_counts[ap] = session_counts.get(ap, 0) + r["cnt"]
|
|
470
|
+
for a in agents:
|
|
471
|
+
a["session_count"] = session_counts.get(a["path"], 0)
|
|
472
|
+
return web.json_response(agents)
|
|
473
|
+
|
|
474
|
+
async def handle_agents_tree(self, request):
|
|
475
|
+
"""GET /api/agents/tree - 返回树形结构"""
|
|
476
|
+
self._ensure_default_agent()
|
|
477
|
+
agents_flat = self._scan_agents_flat()
|
|
478
|
+
# 统计会话数
|
|
479
|
+
if self.core.memory:
|
|
480
|
+
rows = self.core.memory._get_conn().execute(
|
|
481
|
+
"SELECT session_id, COUNT(*) as cnt FROM memories "
|
|
482
|
+
"WHERE category='short_term' GROUP BY session_id").fetchall()
|
|
483
|
+
session_counts = {}
|
|
484
|
+
for r in rows:
|
|
485
|
+
sid = r["session_id"]
|
|
486
|
+
for ap in [sid.split("_")[0], sid]:
|
|
487
|
+
session_counts[ap] = session_counts.get(ap, 0) + r["cnt"]
|
|
488
|
+
for a in agents_flat:
|
|
489
|
+
a["session_count"] = session_counts.get(a["path"], 0)
|
|
490
|
+
tree = self._build_agent_tree(agents_flat)
|
|
491
|
+
return web.json_response(tree)
|
|
492
|
+
|
|
493
|
+
async def handle_create_agent(self, request):
|
|
494
|
+
"""POST /api/agents - 创建顶级 agent"""
|
|
495
|
+
self._ensure_default_agent()
|
|
496
|
+
data = await request.json()
|
|
497
|
+
name = data.get("name", "").strip()
|
|
498
|
+
if not name:
|
|
499
|
+
name = f"agent_{int(time.time())}"
|
|
500
|
+
# 安全校验
|
|
501
|
+
if "/" in name or "\\" in name or name == "default":
|
|
502
|
+
return web.json_response({"error": "invalid name (no slashes, cannot be 'default')"}, status=400)
|
|
503
|
+
|
|
504
|
+
ad = self._agent_dir(name)
|
|
505
|
+
if (ad / "config.json").exists():
|
|
506
|
+
return web.json_response({"error": f"Agent '{name}' already exists"}, status=409)
|
|
507
|
+
|
|
508
|
+
cfg = {
|
|
509
|
+
"name": name,
|
|
510
|
+
"description": data.get("description", ""),
|
|
511
|
+
"avatar_color": data.get("avatar_color") or _agent_color(name),
|
|
512
|
+
"avatar_emoji": data.get("avatar_emoji", ""),
|
|
513
|
+
"execution_mode": data.get("execution_mode", "sandbox"),
|
|
514
|
+
"enabled": True,
|
|
515
|
+
"system_prompt": data.get("system_prompt") or data.get("soul") or f"你是{name},一个专业的AI助手。",
|
|
516
|
+
}
|
|
517
|
+
if "model" in data:
|
|
518
|
+
cfg["model"] = data["model"]
|
|
519
|
+
# 平台绑定和模型库引用
|
|
520
|
+
if data.get("platform"):
|
|
521
|
+
cfg["platform"] = data["platform"]
|
|
522
|
+
if data.get("platform_token"):
|
|
523
|
+
cfg["platform_token"] = data["platform_token"]
|
|
524
|
+
if data.get("platform_app_id"):
|
|
525
|
+
cfg["platform_app_id"] = data["platform_app_id"]
|
|
526
|
+
if data.get("platform_app_secret"):
|
|
527
|
+
cfg["platform_app_secret"] = data["platform_app_secret"]
|
|
528
|
+
if data.get("model_id"):
|
|
529
|
+
cfg["model_id"] = data["model_id"]
|
|
530
|
+
if data.get("backup_model_ids"):
|
|
531
|
+
cfg["backup_model_ids"] = [x for x in data["backup_model_ids"] if isinstance(x, str) and x.strip()]
|
|
532
|
+
|
|
533
|
+
ad.mkdir(parents=True, exist_ok=True)
|
|
534
|
+
(ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
|
|
535
|
+
for fn, default in [
|
|
536
|
+
("soul.md", f"# {name}\n\n## 性格\n专业AI助手\n"),
|
|
537
|
+
("identity.md", f"# {name}\n\n## 身份\nAI助手\n"),
|
|
538
|
+
("user.md", f"# {name} 用户信息\n\n## 用户偏好\n<!-- 在此处记录用户偏好 -->\n"),
|
|
539
|
+
]:
|
|
540
|
+
if not (ad / fn).exists():
|
|
541
|
+
(ad / fn).write_text(default)
|
|
542
|
+
|
|
543
|
+
logger.info(f"创建 Agent: {name} (sandbox模式)")
|
|
544
|
+
return web.json_response({"ok": True, "path": name, "name": name, "avatar_color": cfg["avatar_color"]})
|
|
545
|
+
|
|
546
|
+
async def handle_create_child(self, request):
|
|
547
|
+
"""POST /api/agents/{parent}/children - 创建子 agent"""
|
|
548
|
+
parent_path = request.match_info["name"]
|
|
549
|
+
self._ensure_default_agent()
|
|
550
|
+
|
|
551
|
+
parent_cfg = self._read_agent_config(parent_path)
|
|
552
|
+
if not parent_cfg:
|
|
553
|
+
return web.json_response({"error": f"Parent agent '{parent_path}' not found"}, status=404)
|
|
554
|
+
|
|
555
|
+
data = await request.json()
|
|
556
|
+
name = data.get("name", "").strip()
|
|
557
|
+
if not name:
|
|
558
|
+
name = f"agent_{int(time.time())}"
|
|
559
|
+
if "/" in name or "\\" in name:
|
|
560
|
+
return web.json_response({"error": "invalid name (no slashes)"}, status=400)
|
|
561
|
+
|
|
562
|
+
child_path = f"{parent_path}/{name}"
|
|
563
|
+
ad = self._agent_dir(child_path)
|
|
564
|
+
if (ad / "config.json").exists():
|
|
565
|
+
return web.json_response({"error": f"Agent '{child_path}' already exists"}, status=409)
|
|
566
|
+
|
|
567
|
+
cfg = {
|
|
568
|
+
"name": name,
|
|
569
|
+
"parent": parent_path,
|
|
570
|
+
"description": data.get("description", ""),
|
|
571
|
+
"avatar_color": data.get("avatar_color") or _agent_color(name),
|
|
572
|
+
"avatar_emoji": data.get("avatar_emoji", ""),
|
|
573
|
+
"execution_mode": data.get("execution_mode", "sandbox"),
|
|
574
|
+
"enabled": True,
|
|
575
|
+
"system_prompt": data.get("system_prompt") or data.get("soul") or f"你是{name},{parent_path}的子Agent。请用专业的方式完成你的任务。",
|
|
576
|
+
}
|
|
577
|
+
if "model" in data:
|
|
578
|
+
cfg["model"] = data["model"]
|
|
579
|
+
# 平台绑定和模型库引用
|
|
580
|
+
if data.get("platform"):
|
|
581
|
+
cfg["platform"] = data["platform"]
|
|
582
|
+
if data.get("platform_token"):
|
|
583
|
+
cfg["platform_token"] = data["platform_token"]
|
|
584
|
+
if data.get("platform_app_id"):
|
|
585
|
+
cfg["platform_app_id"] = data["platform_app_id"]
|
|
586
|
+
if data.get("platform_app_secret"):
|
|
587
|
+
cfg["platform_app_secret"] = data["platform_app_secret"]
|
|
588
|
+
if data.get("model_id"):
|
|
589
|
+
cfg["model_id"] = data["model_id"]
|
|
590
|
+
if data.get("backup_model_ids"):
|
|
591
|
+
cfg["backup_model_ids"] = [x for x in data["backup_model_ids"] if isinstance(x, str) and x.strip()]
|
|
592
|
+
|
|
593
|
+
ad.mkdir(parents=True, exist_ok=True)
|
|
594
|
+
(ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
|
|
595
|
+
for fn, default in [
|
|
596
|
+
("soul.md", f"# {name}\n\n## 上级: {parent_path}\n## 性格\n专业AI助手\n"),
|
|
597
|
+
("identity.md", f"# {name}\n\n## 身份\n{parent_path} 的子 Agent\n"),
|
|
598
|
+
("user.md", f"# {name} 用户信息\n\n## 用户偏好\n<!-- 在此处记录用户偏好 -->\n"),
|
|
599
|
+
]:
|
|
600
|
+
if not (ad / fn).exists():
|
|
601
|
+
(ad / fn).write_text(default)
|
|
602
|
+
|
|
603
|
+
logger.info(f"创建子 Agent: {child_path} (sandbox模式)")
|
|
604
|
+
return web.json_response({"ok": True, "path": child_path, "name": name, "parent": parent_path, "avatar_color": cfg["avatar_color"]})
|
|
605
|
+
|
|
606
|
+
async def handle_list_children(self, request):
|
|
607
|
+
"""GET /api/agents/{parent}/children - 列出子 agent"""
|
|
608
|
+
parent_path = request.match_info["name"]
|
|
609
|
+
parent_dir = self._agent_dir(parent_path)
|
|
610
|
+
if not (parent_dir / "config.json").exists():
|
|
611
|
+
return web.json_response({"error": f"Agent '{parent_path}' not found"}, status=404)
|
|
612
|
+
|
|
613
|
+
children = []
|
|
614
|
+
if parent_dir.exists():
|
|
615
|
+
for d in sorted(parent_dir.iterdir()):
|
|
616
|
+
if d.is_dir() and (d / "config.json").exists():
|
|
617
|
+
cfg = json.loads((d / "config.json").read_text())
|
|
618
|
+
child_path = f"{parent_path}/{d.name}"
|
|
619
|
+
child = {"path": child_path, "name": d.name, "parent": parent_path, **cfg}
|
|
620
|
+
child["avatar_color"] = cfg.get("avatar_color") or _agent_color(d.name)
|
|
621
|
+
child["depth"] = child_path.count("/")
|
|
622
|
+
children.append(child)
|
|
623
|
+
return web.json_response(children)
|
|
624
|
+
|
|
625
|
+
async def handle_get_agent(self, request):
|
|
626
|
+
"""GET /api/agents/{path} - 获取 agent 详情"""
|
|
627
|
+
path = request.match_info["name"]
|
|
628
|
+
ad = self._agent_dir(path)
|
|
629
|
+
if not (ad / "config.json").exists():
|
|
630
|
+
return web.json_response({"error": "not found"}, status=404)
|
|
631
|
+
cfg = json.loads((ad / "config.json").read_text())
|
|
632
|
+
soul = (ad / "soul.md").read_text() if (ad / "soul.md").exists() else ""
|
|
633
|
+
identity = (ad / "identity.md").read_text() if (ad / "identity.md").exists() else ""
|
|
634
|
+
user = (ad / "user.md").read_text() if (ad / "user.md").exists() else ""
|
|
635
|
+
# 列出子 agent
|
|
636
|
+
children = []
|
|
637
|
+
for d in sorted(ad.iterdir()):
|
|
638
|
+
if d.is_dir() and (d / "config.json").exists():
|
|
639
|
+
children.append(d.name)
|
|
640
|
+
# 如果有 model_id,解析为完整模型信息
|
|
641
|
+
model_info = None
|
|
642
|
+
model_id = cfg.get("model_id", "")
|
|
643
|
+
if model_id:
|
|
644
|
+
for me in self.core.config.models_library:
|
|
645
|
+
if me.id == model_id:
|
|
646
|
+
model_info = {"id": me.id, "name": me.name, "provider": me.provider,
|
|
647
|
+
"model": me.model, "base_url": me.base_url, "enabled": me.enabled}
|
|
648
|
+
break
|
|
649
|
+
result = {"path": path, **cfg, "soul": soul, "identity": identity, "user": user, "children": children}
|
|
650
|
+
if model_info:
|
|
651
|
+
result["model_info"] = model_info
|
|
652
|
+
return web.json_response(result)
|
|
653
|
+
|
|
654
|
+
async def handle_update_agent(self, request):
|
|
655
|
+
"""PUT /api/agents/{path} - 更新 agent 配置"""
|
|
656
|
+
path = request.match_info["name"]
|
|
657
|
+
data = await request.json()
|
|
658
|
+
ad = self._agent_dir(path)
|
|
659
|
+
if not (ad / "config.json").exists():
|
|
660
|
+
return web.json_response({"error": "not found"}, status=404)
|
|
661
|
+
cfg = json.loads((ad / "config.json").read_text())
|
|
662
|
+
# 更新允许的字段
|
|
663
|
+
for k in ("description", "avatar_color", "avatar_emoji", "model", "system_prompt",
|
|
664
|
+
"execution_mode", "enabled", "sandbox_image", "sandbox_network", "sandbox_memory",
|
|
665
|
+
"platform", "platform_token", "platform_app_id", "platform_app_secret",
|
|
666
|
+
"model_id", "backup_model_ids"):
|
|
667
|
+
if k in data:
|
|
668
|
+
if k == "backup_model_ids":
|
|
669
|
+
cfg[k] = [x for x in data[k] if isinstance(x, str) and x.strip()]
|
|
670
|
+
else:
|
|
671
|
+
cfg[k] = data[k]
|
|
672
|
+
(ad / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
|
|
673
|
+
if "soul" in data: (ad / "soul.md").write_text(data["soul"])
|
|
674
|
+
if "identity" in data: (ad / "identity.md").write_text(data["identity"])
|
|
675
|
+
if "user" in data: (ad / "user.md").write_text(data["user"])
|
|
676
|
+
logger.info(f"更新 Agent: {path}")
|
|
677
|
+
return web.json_response({"ok": True})
|
|
678
|
+
|
|
679
|
+
async def handle_delete_agent(self, request):
|
|
680
|
+
"""DELETE /api/agents/{path} - 删除 agent 及其所有子 agent"""
|
|
681
|
+
path = request.match_info["name"]
|
|
682
|
+
if path == "default":
|
|
683
|
+
return web.json_response({"error": "cannot delete default agent"}, status=403)
|
|
684
|
+
ad = self._agent_dir(path)
|
|
685
|
+
if not (ad / "config.json").exists():
|
|
686
|
+
return web.json_response({"error": "not found"}, status=404)
|
|
687
|
+
if ad.exists():
|
|
688
|
+
shutil.rmtree(ad)
|
|
689
|
+
logger.info(f"删除 Agent: {path}")
|
|
690
|
+
return web.json_response({"ok": True})
|
|
691
|
+
|
|
692
|
+
async def handle_get_soul(self, request):
|
|
693
|
+
path = request.match_info["name"]
|
|
694
|
+
p = self._agent_dir(path) / "soul.md"
|
|
695
|
+
if not p.parent.exists() or not (p.parent / "config.json").exists():
|
|
696
|
+
return web.json_response({"error": "not found"}, status=404)
|
|
697
|
+
return web.json_response({"soul": p.read_text() if p.exists() else ""})
|
|
698
|
+
|
|
699
|
+
async def handle_set_soul(self, request):
|
|
700
|
+
data = await request.json(); ad = self._agent_dir(request.match_info["name"])
|
|
701
|
+
ad.mkdir(parents=True, exist_ok=True)
|
|
702
|
+
(ad / "soul.md").write_text(data.get("soul", ""))
|
|
703
|
+
return web.json_response({"ok": True})
|
|
704
|
+
|
|
705
|
+
async def handle_get_identity(self, request):
|
|
706
|
+
path = request.match_info["name"]
|
|
707
|
+
p = self._agent_dir(path) / "identity.md"
|
|
708
|
+
if not p.parent.exists() or not (p.parent / "config.json").exists():
|
|
709
|
+
return web.json_response({"error": "not found"}, status=404)
|
|
710
|
+
return web.json_response({"identity": p.read_text() if p.exists() else ""})
|
|
711
|
+
|
|
712
|
+
async def handle_set_identity(self, request):
|
|
713
|
+
data = await request.json(); ad = self._agent_dir(request.match_info["name"])
|
|
714
|
+
ad.mkdir(parents=True, exist_ok=True)
|
|
715
|
+
(ad / "identity.md").write_text(data.get("identity", ""))
|
|
716
|
+
return web.json_response({"ok": True})
|
|
717
|
+
|
|
718
|
+
async def handle_get_user(self, request):
|
|
719
|
+
path = request.match_info["name"]
|
|
720
|
+
p = self._agent_dir(path) / "user.md"
|
|
721
|
+
if not p.parent.exists() or not (p.parent / "config.json").exists():
|
|
722
|
+
return web.json_response({"error": "not found"}, status=404)
|
|
723
|
+
return web.json_response({"user": p.read_text() if p.exists() else ""})
|
|
724
|
+
|
|
725
|
+
async def handle_set_user(self, request):
|
|
726
|
+
data = await request.json(); ad = self._agent_dir(request.match_info["name"])
|
|
727
|
+
ad.mkdir(parents=True, exist_ok=True)
|
|
728
|
+
(ad / "user.md").write_text(data.get("user", ""))
|
|
729
|
+
return web.json_response({"ok": True})
|
|
730
|
+
|
|
731
|
+
# --- Executor ---
|
|
732
|
+
async def handle_get_executor(self, request):
|
|
733
|
+
info = self.core.executor.get_execution_info() if self.core.executor else {}
|
|
734
|
+
cfg = self.core.config.executor if self.core.config else None
|
|
735
|
+
return web.json_response({
|
|
736
|
+
**info,
|
|
737
|
+
"timeout": cfg.timeout if cfg else 300,
|
|
738
|
+
"auto_fix": cfg.auto_fix if cfg else True,
|
|
739
|
+
"max_output_length": cfg.max_output_length if cfg else 50000,
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
async def handle_update_executor(self, request):
|
|
743
|
+
data = await request.json()
|
|
744
|
+
cfg_path = self.core.config_mgr._config_file
|
|
745
|
+
cfg_data = json.loads(cfg_path.read_text()) if cfg_path.exists() else {}
|
|
746
|
+
exe = cfg_data.setdefault("executor", {})
|
|
747
|
+
for k in ("execution_mode", "timeout", "auto_fix", "max_output_length",
|
|
748
|
+
"sandbox_image", "sandbox_network", "sandbox_memory"):
|
|
749
|
+
if k in data:
|
|
750
|
+
exe[k] = data[k]
|
|
751
|
+
cfg_path.write_text(json.dumps(cfg_data, indent=2, ensure_ascii=False))
|
|
752
|
+
# 热更新内存配置
|
|
753
|
+
update_keys = {k: v for k, v in data.items() if hasattr(self.core.config_mgr.config.executor, k)}
|
|
754
|
+
self.core.config_mgr.update_executor(**update_keys)
|
|
755
|
+
# 实时切换执行引擎
|
|
756
|
+
if self.core.executor and "execution_mode" in data:
|
|
757
|
+
mode = data["execution_mode"]
|
|
758
|
+
ok = self.core.executor.set_execution_mode(mode)
|
|
759
|
+
if "sandbox_image" in data:
|
|
760
|
+
self.core.executor.sandbox_image = data["sandbox_image"]
|
|
761
|
+
if "sandbox_network" in data:
|
|
762
|
+
self.core.executor.sandbox_network = data["sandbox_network"]
|
|
763
|
+
if "sandbox_memory" in data:
|
|
764
|
+
self.core.executor.sandbox_memory = data["sandbox_memory"]
|
|
765
|
+
if not ok:
|
|
766
|
+
return web.json_response({"ok": False, "error": f"切换到 {mode} 失败(Docker 不可用)"})
|
|
767
|
+
logger.info(f"执行引擎配置已热更新: mode={data.get('execution_mode')}")
|
|
768
|
+
return web.json_response({"ok": True, "hot_reload": True})
|
|
769
|
+
|
|
770
|
+
# --- Agent Bindings ---
|
|
771
|
+
async def handle_agent_bindings(self, request):
|
|
772
|
+
"""GET /api/agents/{name}/bindings - 获取 Agent 的聊天平台绑定"""
|
|
773
|
+
path = request.match_info["name"]
|
|
774
|
+
cfg = self._read_agent_config(path)
|
|
775
|
+
if not cfg:
|
|
776
|
+
return web.json_response({"error": "not found"}, status=404)
|
|
777
|
+
bindings = {}
|
|
778
|
+
if cfg.get("platform"):
|
|
779
|
+
bindings["platform"] = cfg["platform"]
|
|
780
|
+
if cfg.get("platform_token"):
|
|
781
|
+
bindings["platform_token"] = cfg["platform_token"]
|
|
782
|
+
if cfg.get("platform_app_id"):
|
|
783
|
+
bindings["platform_app_id"] = cfg["platform_app_id"]
|
|
784
|
+
if cfg.get("platform_app_secret"):
|
|
785
|
+
bindings["platform_app_secret"] = cfg["platform_app_secret"]
|
|
786
|
+
# 模型绑定(含备用模型)
|
|
787
|
+
if cfg.get("model_id"):
|
|
788
|
+
model_info = None
|
|
789
|
+
for me in self.core.config.models_library:
|
|
790
|
+
if me.id == cfg["model_id"]:
|
|
791
|
+
model_info = {"id": me.id, "name": me.name, "provider": me.provider,
|
|
792
|
+
"model": me.model, "base_url": me.base_url, "enabled": me.enabled}
|
|
793
|
+
break
|
|
794
|
+
if model_info:
|
|
795
|
+
bindings["model"] = model_info
|
|
796
|
+
else:
|
|
797
|
+
bindings["model_id"] = cfg["model_id"]
|
|
798
|
+
bindings["model"] = None
|
|
799
|
+
elif cfg.get("model"):
|
|
800
|
+
bindings["model"] = {"model": cfg["model"]}
|
|
801
|
+
|
|
802
|
+
# 备用模型列表
|
|
803
|
+
backup_ids = cfg.get("backup_model_ids", [])
|
|
804
|
+
if backup_ids:
|
|
805
|
+
backup_models = []
|
|
806
|
+
for bid in backup_ids:
|
|
807
|
+
for me in self.core.config.models_library:
|
|
808
|
+
if me.id == bid:
|
|
809
|
+
backup_models.append({
|
|
810
|
+
"id": me.id, "name": me.name, "provider": me.provider,
|
|
811
|
+
"model": me.model, "base_url": me.base_url, "enabled": me.enabled
|
|
812
|
+
})
|
|
813
|
+
break
|
|
814
|
+
else:
|
|
815
|
+
backup_models.append({"id": bid, "name": bid, "not_found": True})
|
|
816
|
+
bindings["backup_models"] = backup_models
|
|
817
|
+
else:
|
|
818
|
+
bindings["backup_models"] = []
|
|
819
|
+
|
|
820
|
+
return web.json_response(bindings)
|
|
821
|
+
|
|
822
|
+
# --- Platforms ---
|
|
823
|
+
async def handle_list_platforms(self, request):
|
|
824
|
+
platforms = []
|
|
825
|
+
for pname in ["telegram", "discord", "feishu", "qq", "wechat"]:
|
|
826
|
+
pcfg = getattr(self.core.config, pname, None)
|
|
827
|
+
platforms.append({"name": pname, "enabled": bool(pcfg and (pcfg.token or pcfg.app_id))})
|
|
828
|
+
return web.json_response(platforms)
|
|
829
|
+
|
|
830
|
+
async def handle_update_platform(self, request):
|
|
831
|
+
name = request.match_info["name"]; data = await request.json()
|
|
832
|
+
cfg_path = self.core.config_mgr._config_file
|
|
833
|
+
cfg_data = json.loads(cfg_path.read_text()) if cfg_path.exists() else {}
|
|
834
|
+
cfg_data.setdefault("platforms", {})[name] = data
|
|
835
|
+
cfg_path.write_text(json.dumps(cfg_data, indent=2, ensure_ascii=False))
|
|
836
|
+
return web.json_response({"ok": True})
|
|
837
|
+
|
|
838
|
+
# --- Sessions ---
|
|
839
|
+
async def handle_list_sessions(self, request):
|
|
840
|
+
if not self.core.memory: return web.json_response([])
|
|
841
|
+
agent = request.query.get("agent", "")
|
|
842
|
+
if agent:
|
|
843
|
+
prefix = f"{agent}_"
|
|
844
|
+
rows = self.core.memory._get_conn().execute(
|
|
845
|
+
"SELECT DISTINCT session_id, COUNT(*) as cnt, MAX(created_at) as last FROM memories "
|
|
846
|
+
"WHERE category='short_term' AND session_id LIKE ? GROUP BY session_id ORDER BY last DESC LIMIT 100",
|
|
847
|
+
(prefix + "%",)).fetchall()
|
|
848
|
+
else:
|
|
849
|
+
rows = self.core.memory._get_conn().execute(
|
|
850
|
+
"SELECT DISTINCT session_id, COUNT(*) as cnt, MAX(created_at) as last FROM memories "
|
|
851
|
+
"WHERE category='short_term' GROUP BY session_id ORDER BY last DESC LIMIT 100").fetchall()
|
|
852
|
+
return web.json_response([{"id": r["session_id"], "messages": r["cnt"], "last": r["last"]} for r in rows])
|
|
853
|
+
|
|
854
|
+
async def handle_agent_sessions(self, request):
|
|
855
|
+
"""GET /api/agents/{name}/sessions - Convenience endpoint for agent-scoped sessions."""
|
|
856
|
+
name = request.match_info["name"]
|
|
857
|
+
if not self.core.memory:
|
|
858
|
+
return web.json_response({"agent": name, "sessions": []})
|
|
859
|
+
prefix = f"{name}_"
|
|
860
|
+
rows = self.core.memory._get_conn().execute(
|
|
861
|
+
"SELECT DISTINCT session_id, COUNT(*) as cnt, MAX(created_at) as last FROM memories "
|
|
862
|
+
"WHERE category='short_term' AND session_id LIKE ? GROUP BY session_id ORDER BY last DESC LIMIT 100",
|
|
863
|
+
(prefix + "%",)).fetchall()
|
|
864
|
+
sessions = [{"id": r["session_id"], "messages": r["cnt"], "last": r["last"]} for r in rows]
|
|
865
|
+
# Agent info
|
|
866
|
+
ad = self._agent_dir(name)
|
|
867
|
+
agent_info = {"name": name, "avatar_color": _agent_color(name)}
|
|
868
|
+
if (ad / "config.json").exists():
|
|
869
|
+
agent_info.update(json.loads((ad / "config.json").read_text()))
|
|
870
|
+
return web.json_response({**agent_info, "sessions": sessions})
|
|
871
|
+
|
|
872
|
+
async def handle_get_messages(self, request):
|
|
873
|
+
sid = request.match_info["sid"]
|
|
874
|
+
if not self.core.memory: return web.json_response([])
|
|
875
|
+
entries = self.core.memory.get_conversation(sid, limit=100)
|
|
876
|
+
return web.json_response([{"role": e.role, "content": e.content[:500], "time": e.created_at} for e in entries])
|
|
877
|
+
|
|
878
|
+
async def handle_clear_session(self, request):
|
|
879
|
+
if self.core.memory: self.core.memory.clear_conversation(request.match_info["sid"])
|
|
880
|
+
return web.json_response({"ok": True})
|
|
881
|
+
|
|
882
|
+
# --- Memory ---
|
|
883
|
+
async def handle_memory_stats(self, request):
|
|
884
|
+
return web.json_response(self.core.memory.get_stats() if self.core.memory else {})
|
|
885
|
+
|
|
886
|
+
async def handle_memory_search(self, request):
|
|
887
|
+
q = request.query.get("q", ""); cat = request.query.get("category", "")
|
|
888
|
+
if not self.core.memory: return web.json_response([])
|
|
889
|
+
results = self.core.memory.search(q, category=cat, limit=20)
|
|
890
|
+
return web.json_response([{"id": e.id, "key": e.key, "content": e.content[:300],
|
|
891
|
+
"category": e.category, "importance": e.importance} for e in results])
|
|
892
|
+
|
|
893
|
+
async def handle_list_long_term(self, request):
|
|
894
|
+
if not self.core.memory: return web.json_response([])
|
|
895
|
+
entries = self.core.memory.get_long_term(limit=50)
|
|
896
|
+
return web.json_response([{"id": e.id, "key": e.key, "content": e.content[:300],
|
|
897
|
+
"summary": e.summary, "importance": e.importance} for e in entries])
|
|
898
|
+
|
|
899
|
+
async def handle_delete_long_term(self, request):
|
|
900
|
+
if self.core.memory:
|
|
901
|
+
self.core.memory._get_conn().execute("DELETE FROM memories WHERE id=?", (request.match_info["mid"],))
|
|
902
|
+
self.core.memory._get_conn().commit()
|
|
903
|
+
return web.json_response({"ok": True})
|
|
904
|
+
|
|
905
|
+
async def handle_memory_cleanup(self, request):
|
|
906
|
+
return web.json_response({"cleaned": self.core.memory.cleanup_expired() if self.core.memory else 0})
|
|
907
|
+
|
|
908
|
+
# --- LLM ---
|
|
909
|
+
async def handle_get_llm(self, request):
|
|
910
|
+
c = self.core.config.llm
|
|
911
|
+
return web.json_response({"provider": c.provider, "model": c.model, "base_url": c.base_url,
|
|
912
|
+
"temperature": c.temperature, "max_tokens": c.max_tokens, "timeout": c.timeout,
|
|
913
|
+
"max_retries": c.max_retries, "api_key_set": bool(c.api_key)})
|
|
914
|
+
|
|
915
|
+
async def handle_update_llm(self, request):
|
|
916
|
+
data = await request.json()
|
|
917
|
+
# 1. 写入文件
|
|
918
|
+
cfg_path = self.core.config_mgr._config_file
|
|
919
|
+
cfg_data = json.loads(cfg_path.read_text()) if cfg_path.exists() else {}
|
|
920
|
+
llm = cfg_data.setdefault("llm", {})
|
|
921
|
+
for k in ("provider", "model", "base_url", "temperature", "max_tokens", "timeout", "max_retries"):
|
|
922
|
+
if k in data: llm[k] = data[k]
|
|
923
|
+
if data.get("api_key"): llm["api_key"] = data["api_key"]
|
|
924
|
+
cfg_path.write_text(json.dumps(cfg_data, indent=2, ensure_ascii=False))
|
|
925
|
+
# 2. 热更新内存中的配置
|
|
926
|
+
update_keys = {k: v for k, v in data.items() if k != "api_key" or data.get("api_key")}
|
|
927
|
+
self.core.config_mgr.update_llm(**update_keys)
|
|
928
|
+
# 3. 实时更新 LLM 客户端
|
|
929
|
+
if self.core.llm:
|
|
930
|
+
new_cfg = self.core.config_mgr.config.llm
|
|
931
|
+
self.core.llm.provider = new_cfg.provider
|
|
932
|
+
self.core.llm.model = new_cfg.model
|
|
933
|
+
self.core.llm.base_url = new_cfg.base_url
|
|
934
|
+
self.core.llm.temperature = new_cfg.temperature
|
|
935
|
+
self.core.llm.max_tokens = new_cfg.max_tokens
|
|
936
|
+
self.core.llm.timeout = new_cfg.timeout
|
|
937
|
+
self.core.llm.max_retries = new_cfg.max_retries
|
|
938
|
+
if data.get("api_key"):
|
|
939
|
+
self.core.llm.api_key = data["api_key"]
|
|
940
|
+
logger.info(f"LLM 配置已热更新: provider={data.get('provider')}, model={data.get('model')}")
|
|
941
|
+
return web.json_response({"ok": True, "hot_reload": True})
|
|
942
|
+
|
|
943
|
+
async def handle_test_llm(self, request):
|
|
944
|
+
try:
|
|
945
|
+
msg = await self.core.llm.chat([Message(role="user", content="Hi, reply OK")])
|
|
946
|
+
return web.json_response({"ok": True, "response": msg.content[:100] if msg.content else ""})
|
|
947
|
+
except Exception as e:
|
|
948
|
+
return web.json_response({"ok": False, "error": str(e)})
|
|
949
|
+
|
|
950
|
+
async def handle_llm_usage(self, request):
|
|
951
|
+
return web.json_response(self.core.llm.get_usage_stats() if self.core.llm else {})
|
|
952
|
+
|
|
953
|
+
# --- Models Library ---
|
|
954
|
+
async def handle_list_models(self, request):
|
|
955
|
+
"""GET /api/models - 列出模型库中所有模型"""
|
|
956
|
+
models = []
|
|
957
|
+
for m in self.core.config.models_library:
|
|
958
|
+
models.append({
|
|
959
|
+
"id": m.id, "name": m.name, "provider": m.provider,
|
|
960
|
+
"model": m.model, "base_url": m.base_url,
|
|
961
|
+
"max_tokens": m.max_tokens, "temperature": m.temperature,
|
|
962
|
+
"enabled": m.enabled,
|
|
963
|
+
"has_api_key": bool(m.api_key),
|
|
964
|
+
})
|
|
965
|
+
return web.json_response(models)
|
|
966
|
+
|
|
967
|
+
async def handle_add_model(self, request):
|
|
968
|
+
"""POST /api/models - 添加模型到库"""
|
|
969
|
+
data = await request.json()
|
|
970
|
+
model_id = data.get("id", "").strip()
|
|
971
|
+
if not model_id:
|
|
972
|
+
return web.json_response({"error": "模型 ID 不能为空"}, status=400)
|
|
973
|
+
# 检查重复
|
|
974
|
+
for m in self.core.config.models_library:
|
|
975
|
+
if m.id == model_id:
|
|
976
|
+
return web.json_response({"error": f"模型 '{model_id}' 已存在"}, status=409)
|
|
977
|
+
entry = ModelEntry(
|
|
978
|
+
id=model_id,
|
|
979
|
+
name=data.get("name", model_id),
|
|
980
|
+
provider=data.get("provider", "openai"),
|
|
981
|
+
model=data.get("model", model_id),
|
|
982
|
+
base_url=data.get("base_url", ""),
|
|
983
|
+
api_key=data.get("api_key", ""),
|
|
984
|
+
max_tokens=data.get("max_tokens", 4096),
|
|
985
|
+
temperature=data.get("temperature", 0.1),
|
|
986
|
+
enabled=data.get("enabled", True),
|
|
987
|
+
)
|
|
988
|
+
self.core.config.models_library.append(entry)
|
|
989
|
+
self.core.config_mgr.save()
|
|
990
|
+
logger.info(f"添加模型到库: {model_id}")
|
|
991
|
+
return web.json_response({"ok": True, "id": model_id})
|
|
992
|
+
|
|
993
|
+
async def handle_update_model(self, request):
|
|
994
|
+
"""PUT /api/models/{model_id} - 更新模型配置"""
|
|
995
|
+
model_id = request.match_info["model_id"]
|
|
996
|
+
data = await request.json()
|
|
997
|
+
found = False
|
|
998
|
+
for m in self.core.config.models_library:
|
|
999
|
+
if m.id == model_id:
|
|
1000
|
+
for k in ("name", "provider", "model", "base_url", "max_tokens", "temperature", "enabled"):
|
|
1001
|
+
if k in data:
|
|
1002
|
+
setattr(m, k, data[k])
|
|
1003
|
+
if data.get("api_key"):
|
|
1004
|
+
m.api_key = data["api_key"]
|
|
1005
|
+
found = True
|
|
1006
|
+
break
|
|
1007
|
+
if not found:
|
|
1008
|
+
return web.json_response({"error": f"模型 '{model_id}' 未找到"}, status=404)
|
|
1009
|
+
self.core.config_mgr.save()
|
|
1010
|
+
logger.info(f"更新模型库: {model_id}")
|
|
1011
|
+
return web.json_response({"ok": True})
|
|
1012
|
+
|
|
1013
|
+
async def handle_delete_model(self, request):
|
|
1014
|
+
"""DELETE /api/models/{model_id} - 从库中删除模型"""
|
|
1015
|
+
model_id = request.match_info["model_id"]
|
|
1016
|
+
original_len = len(self.core.config.models_library)
|
|
1017
|
+
self.core.config.models_library = [
|
|
1018
|
+
m for m in self.core.config.models_library if m.id != model_id
|
|
1019
|
+
]
|
|
1020
|
+
if len(self.core.config.models_library) == original_len:
|
|
1021
|
+
return web.json_response({"error": f"模型 '{model_id}' 未找到"}, status=404)
|
|
1022
|
+
self.core.config_mgr.save()
|
|
1023
|
+
logger.info(f"从模型库删除: {model_id}")
|
|
1024
|
+
return web.json_response({"ok": True})
|
|
1025
|
+
|
|
1026
|
+
# --- Skills ---
|
|
1027
|
+
async def handle_list_skills(self, request):
|
|
1028
|
+
return web.json_response(self.core.skill_registry.list_skills_info() if self.core.skill_registry else [])
|
|
1029
|
+
|
|
1030
|
+
async def handle_get_skill(self, request):
|
|
1031
|
+
s = self.core.skill_registry.get(request.match_info["name"]) if self.core.skill_registry else None
|
|
1032
|
+
if not s: return web.json_response({"error": "not found"}, status=404)
|
|
1033
|
+
return web.json_response(s.to_openclaw_format())
|
|
1034
|
+
|
|
1035
|
+
# --- Workdir ---
|
|
1036
|
+
async def handle_get_workdir(self, request):
|
|
1037
|
+
return web.json_response({"path": str(self.core.config_mgr.data_dir / "workspace")})
|
|
1038
|
+
|
|
1039
|
+
async def handle_set_workdir(self, request):
|
|
1040
|
+
data = await request.json(); path = data.get("path", "")
|
|
1041
|
+
if path: Path(path).mkdir(parents=True, exist_ok=True)
|
|
1042
|
+
cfg_path = self.core.config_mgr._config_file
|
|
1043
|
+
cfg_data = json.loads(cfg_path.read_text()) if cfg_path.exists() else {}
|
|
1044
|
+
cfg_data["workspace"] = path
|
|
1045
|
+
cfg_path.write_text(json.dumps(cfg_data, indent=2, ensure_ascii=False))
|
|
1046
|
+
return web.json_response({"ok": True})
|
|
1047
|
+
|
|
1048
|
+
async def handle_list_workdir(self, request):
|
|
1049
|
+
wd = self.core.config_mgr.data_dir / "workspace"
|
|
1050
|
+
if not wd.exists(): return web.json_response([])
|
|
1051
|
+
items = []
|
|
1052
|
+
for f in sorted(wd.iterdir())[:200]:
|
|
1053
|
+
try: items.append({"name": f.name, "type": "dir" if f.is_dir() else "file", "size": f.stat().st_size if f.is_file() else 0})
|
|
1054
|
+
except: pass
|
|
1055
|
+
return web.json_response(items)
|
|
1056
|
+
|
|
1057
|
+
# --- Logs ---
|
|
1058
|
+
async def handle_get_logs(self, request):
|
|
1059
|
+
log_dir = self.core.config_mgr.logs_dir
|
|
1060
|
+
lines = int(request.query.get("lines", "200"))
|
|
1061
|
+
level = request.query.get("level", "").upper()
|
|
1062
|
+
logs = []
|
|
1063
|
+
for lf in sorted(log_dir.glob("myagent*.log"), reverse=True):
|
|
1064
|
+
try:
|
|
1065
|
+
text = lf.read_text(encoding="utf-8", errors="ignore")
|
|
1066
|
+
for line in text.strip().split("\n")[-lines:]:
|
|
1067
|
+
if level and level not in line: continue
|
|
1068
|
+
logs.append(line)
|
|
1069
|
+
if len(logs) >= lines: break
|
|
1070
|
+
except: pass
|
|
1071
|
+
return web.json_response(logs[-lines:])
|
|
1072
|
+
|
|
1073
|
+
async def handle_log_stream(self, request):
|
|
1074
|
+
resp = web.StreamResponse()
|
|
1075
|
+
resp.content_type = "text/event-stream"
|
|
1076
|
+
resp.headers["Cache-Control"] = "no-cache"
|
|
1077
|
+
await resp.prepare(request)
|
|
1078
|
+
log_file = self.core.config_mgr.logs_dir / "myagent.log"
|
|
1079
|
+
last_pos = 0
|
|
1080
|
+
try:
|
|
1081
|
+
while True:
|
|
1082
|
+
try:
|
|
1083
|
+
if log_file.exists():
|
|
1084
|
+
size = log_file.stat().st_size
|
|
1085
|
+
if size > last_pos:
|
|
1086
|
+
with open(log_file, "r", encoding="utf-8", errors="ignore") as f:
|
|
1087
|
+
f.seek(last_pos); new_data = f.read(); last_pos = size
|
|
1088
|
+
if new_data: await resp.write(f"data: {json.dumps(new_data.strip())}\n\n")
|
|
1089
|
+
except: pass
|
|
1090
|
+
await asyncio.sleep(0.5)
|
|
1091
|
+
except asyncio.CancelledError: pass
|
|
1092
|
+
return resp
|
|
1093
|
+
|
|
1094
|
+
# ── 配置管理 (热重载 / 导入 / 导出) ──
|
|
1095
|
+
# ── Agent 间通信 ──
|
|
1096
|
+
async def handle_get_communication(self, request):
|
|
1097
|
+
"""GET /api/communication - 获取通信配置"""
|
|
1098
|
+
comm = self.core.config.communication
|
|
1099
|
+
return web.json_response({
|
|
1100
|
+
"enabled": comm.enabled,
|
|
1101
|
+
"server_url": comm.server_url,
|
|
1102
|
+
"agent_id": comm.agent_id,
|
|
1103
|
+
"max_friends": comm.max_friends,
|
|
1104
|
+
"auto_accept": comm.auto_accept,
|
|
1105
|
+
})
|
|
1106
|
+
|
|
1107
|
+
async def handle_update_communication(self, request):
|
|
1108
|
+
"""PUT /api/communication - 更新通信配置"""
|
|
1109
|
+
data = await request.json()
|
|
1110
|
+
cfg_path = self.core.config_mgr._config_file
|
|
1111
|
+
cfg_data = json.loads(cfg_path.read_text()) if cfg_path.exists() else {}
|
|
1112
|
+
comm = cfg_data.setdefault("communication", {})
|
|
1113
|
+
for k in ("enabled", "server_url", "max_friends", "auto_accept"):
|
|
1114
|
+
if k in data:
|
|
1115
|
+
comm[k] = data[k]
|
|
1116
|
+
cfg_path.write_text(json.dumps(cfg_data, indent=2, ensure_ascii=False))
|
|
1117
|
+
# Hot-reload in memory
|
|
1118
|
+
if "enabled" in data:
|
|
1119
|
+
self.core.config.communication.enabled = data["enabled"]
|
|
1120
|
+
if "server_url" in data:
|
|
1121
|
+
self.core.config.communication.server_url = data["server_url"]
|
|
1122
|
+
if "max_friends" in data:
|
|
1123
|
+
self.core.config.communication.max_friends = data["max_friends"]
|
|
1124
|
+
if "auto_accept" in data:
|
|
1125
|
+
self.core.config.communication.auto_accept = data["auto_accept"]
|
|
1126
|
+
logger.info(f"通信配置已更新: enabled={data.get('enabled')}")
|
|
1127
|
+
return web.json_response({"ok": True})
|
|
1128
|
+
|
|
1129
|
+
async def handle_comm_status(self, request):
|
|
1130
|
+
"""GET /api/communication/status - 获取通信状态"""
|
|
1131
|
+
mgr = getattr(self.core, "communication_manager", None)
|
|
1132
|
+
if not mgr:
|
|
1133
|
+
return web.json_response({"enabled": False, "running": False})
|
|
1134
|
+
return web.json_response(mgr.get_status())
|
|
1135
|
+
|
|
1136
|
+
async def handle_list_peers(self, request):
|
|
1137
|
+
"""GET /api/communication/peers - 列出所有 peers"""
|
|
1138
|
+
mgr = getattr(self.core, "communication_manager", None)
|
|
1139
|
+
if not mgr:
|
|
1140
|
+
return web.json_response([])
|
|
1141
|
+
peers = mgr.get_peer_list()
|
|
1142
|
+
return web.json_response([p.to_dict() for p in peers])
|
|
1143
|
+
|
|
1144
|
+
async def handle_add_peer(self, request):
|
|
1145
|
+
"""POST /api/communication/peers - 添加 peer"""
|
|
1146
|
+
mgr = getattr(self.core, "communication_manager", None)
|
|
1147
|
+
if not mgr:
|
|
1148
|
+
return web.json_response({"error": "communication not enabled"}, status=400)
|
|
1149
|
+
data = await request.json()
|
|
1150
|
+
agent_id = data.get("agent_id", "").strip()
|
|
1151
|
+
public_key = data.get("public_key", "").strip()
|
|
1152
|
+
display_name = data.get("display_name", "")
|
|
1153
|
+
if not agent_id or not public_key:
|
|
1154
|
+
return web.json_response({"error": "agent_id and public_key are required"}, status=400)
|
|
1155
|
+
ok = mgr.add_peer(agent_id=agent_id, public_key=public_key, display_name=display_name)
|
|
1156
|
+
if ok:
|
|
1157
|
+
return web.json_response({"ok": True, "agent_id": agent_id})
|
|
1158
|
+
else:
|
|
1159
|
+
return web.json_response({"error": "peer already exists"}, status=409)
|
|
1160
|
+
|
|
1161
|
+
async def handle_remove_peer(self, request):
|
|
1162
|
+
"""DELETE /api/communication/peers/{agent_id} - 移除 peer"""
|
|
1163
|
+
mgr = getattr(self.core, "communication_manager", None)
|
|
1164
|
+
if not mgr:
|
|
1165
|
+
return web.json_response({"error": "communication not enabled"}, status=400)
|
|
1166
|
+
agent_id = request.match_info["agent_id"]
|
|
1167
|
+
ok = mgr.remove_peer(agent_id)
|
|
1168
|
+
if ok:
|
|
1169
|
+
return web.json_response({"ok": True})
|
|
1170
|
+
else:
|
|
1171
|
+
return web.json_response({"error": "peer not found"}, status=404)
|
|
1172
|
+
|
|
1173
|
+
async def handle_comm_messages(self, request):
|
|
1174
|
+
"""GET /api/communication/messages - 获取最近消息"""
|
|
1175
|
+
mgr = getattr(self.core, "communication_manager", None)
|
|
1176
|
+
if not mgr:
|
|
1177
|
+
return web.json_response([])
|
|
1178
|
+
limit = int(request.query.get("limit", "50"))
|
|
1179
|
+
from_agent = request.query.get("from_agent", "")
|
|
1180
|
+
to_agent = request.query.get("to_agent", "")
|
|
1181
|
+
messages = mgr.get_messages(limit=limit, from_agent=from_agent, to_agent=to_agent)
|
|
1182
|
+
return web.json_response(messages)
|
|
1183
|
+
|
|
1184
|
+
async def handle_send_message(self, request):
|
|
1185
|
+
"""POST /api/communication/messages - 发送消息"""
|
|
1186
|
+
mgr = getattr(self.core, "communication_manager", None)
|
|
1187
|
+
if not mgr:
|
|
1188
|
+
return web.json_response({"error": "communication not enabled"}, status=400)
|
|
1189
|
+
data = await request.json()
|
|
1190
|
+
to_agent = data.get("to_agent", "").strip()
|
|
1191
|
+
content = data.get("content", "").strip()
|
|
1192
|
+
msg_type = data.get("msg_type", "text")
|
|
1193
|
+
if not to_agent or not content:
|
|
1194
|
+
return web.json_response({"error": "to_agent and content are required"}, status=400)
|
|
1195
|
+
try:
|
|
1196
|
+
msg = await mgr.send_message(to_agent=to_agent, content=content, msg_type=msg_type)
|
|
1197
|
+
return web.json_response({"ok": True, "message": msg.to_dict()})
|
|
1198
|
+
except Exception as e:
|
|
1199
|
+
logger.error(f"Send message error: {e}")
|
|
1200
|
+
return web.json_response({"error": str(e)}, status=500)
|
|
1201
|
+
|
|
1202
|
+
async def handle_ack_message(self, request):
|
|
1203
|
+
"""POST /api/communication/messages/{msg_id}/ack - 确认消息"""
|
|
1204
|
+
msg_id = request.match_info["msg_id"]
|
|
1205
|
+
# For now, just return ok. In the future, this could update delivery status.
|
|
1206
|
+
return web.json_response({"ok": True, "msg_id": msg_id})
|
|
1207
|
+
|
|
1208
|
+
async def handle_get_config(self, request):
|
|
1209
|
+
"""GET /api/config - 获取完整配置(敏感字段脱敏)"""
|
|
1210
|
+
cfg = self.core.config_mgr.get_full_config()
|
|
1211
|
+
return web.json_response(cfg)
|
|
1212
|
+
|
|
1213
|
+
def _build_model_chain(self, agent_cfg: dict | None, agent_path: str) -> list[dict]:
|
|
1214
|
+
"""构建模型链: [主模型, 备用模型1, 备用模型2, ...]"""
|
|
1215
|
+
if not agent_cfg:
|
|
1216
|
+
return []
|
|
1217
|
+
|
|
1218
|
+
chain = []
|
|
1219
|
+
llm_defaults = self.core.config.llm
|
|
1220
|
+
|
|
1221
|
+
# 主模型
|
|
1222
|
+
model_id = agent_cfg.get("model_id")
|
|
1223
|
+
if model_id:
|
|
1224
|
+
for me in self.core.config.models_library:
|
|
1225
|
+
if me.id == model_id:
|
|
1226
|
+
chain.append({
|
|
1227
|
+
"id": me.id,
|
|
1228
|
+
"name": me.name,
|
|
1229
|
+
"provider": me.provider or llm_defaults.provider,
|
|
1230
|
+
"model": me.model or model_id,
|
|
1231
|
+
"base_url": me.base_url or llm_defaults.base_url,
|
|
1232
|
+
"api_key": me.api_key or llm_defaults.api_key,
|
|
1233
|
+
"temperature": me.temperature if me.temperature is not None else llm_defaults.temperature,
|
|
1234
|
+
"max_tokens": me.max_tokens if me.max_tokens else llm_defaults.max_tokens,
|
|
1235
|
+
"is_backup": False,
|
|
1236
|
+
})
|
|
1237
|
+
break
|
|
1238
|
+
|
|
1239
|
+
# 兼容旧的 model 字段
|
|
1240
|
+
if not chain and agent_cfg.get("model"):
|
|
1241
|
+
chain.append({
|
|
1242
|
+
"id": "",
|
|
1243
|
+
"name": agent_cfg["model"],
|
|
1244
|
+
"model": agent_cfg["model"],
|
|
1245
|
+
"is_backup": False,
|
|
1246
|
+
})
|
|
1247
|
+
|
|
1248
|
+
# 备用模型列表
|
|
1249
|
+
backup_ids = agent_cfg.get("backup_model_ids", [])
|
|
1250
|
+
for bid in backup_ids:
|
|
1251
|
+
if isinstance(bid, str) and bid.strip():
|
|
1252
|
+
for me in self.core.config.models_library:
|
|
1253
|
+
if me.id == bid:
|
|
1254
|
+
chain.append({
|
|
1255
|
+
"id": me.id,
|
|
1256
|
+
"name": me.name,
|
|
1257
|
+
"provider": me.provider or llm_defaults.provider,
|
|
1258
|
+
"model": me.model or bid,
|
|
1259
|
+
"base_url": me.base_url or llm_defaults.base_url,
|
|
1260
|
+
"api_key": me.api_key or llm_defaults.api_key,
|
|
1261
|
+
"temperature": me.temperature if me.temperature is not None else llm_defaults.temperature,
|
|
1262
|
+
"max_tokens": me.max_tokens if me.max_tokens else llm_defaults.max_tokens,
|
|
1263
|
+
"is_backup": True,
|
|
1264
|
+
})
|
|
1265
|
+
break
|
|
1266
|
+
|
|
1267
|
+
return chain
|
|
1268
|
+
|
|
1269
|
+
async def _try_model_chain(self, model_chain: list[dict], message: str, session_id: str) -> str:
|
|
1270
|
+
"""依次尝试模型链中的模型,直到成功或全部失败"""
|
|
1271
|
+
if not model_chain:
|
|
1272
|
+
return await self.core.process_message(message, session_id)
|
|
1273
|
+
|
|
1274
|
+
llm = self.core.llm
|
|
1275
|
+
last_error = ""
|
|
1276
|
+
used_model_name = ""
|
|
1277
|
+
|
|
1278
|
+
for i, mc in enumerate(model_chain):
|
|
1279
|
+
is_backup = mc.get("is_backup", False)
|
|
1280
|
+
model_label = f"{'备用' if is_backup else '主'}模型 {mc.get('name', mc.get('model', '?'))}"
|
|
1281
|
+
logger.info(f"尝试 {model_label} ({i+1}/{len(model_chain)}): provider={mc.get('provider')}, model={mc.get('model')}")
|
|
1282
|
+
|
|
1283
|
+
# 保存原始值
|
|
1284
|
+
orig = {
|
|
1285
|
+
"provider": llm.provider,
|
|
1286
|
+
"model": llm.model,
|
|
1287
|
+
"base_url": llm.base_url,
|
|
1288
|
+
"api_key": llm.api_key,
|
|
1289
|
+
"temperature": llm.temperature,
|
|
1290
|
+
"max_tokens": llm.max_tokens,
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
try:
|
|
1294
|
+
# 切换到当前模型
|
|
1295
|
+
if "provider" in mc:
|
|
1296
|
+
llm.provider = mc["provider"]
|
|
1297
|
+
if "model" in mc:
|
|
1298
|
+
llm.model = mc["model"]
|
|
1299
|
+
if "base_url" in mc:
|
|
1300
|
+
llm.base_url = mc["base_url"]
|
|
1301
|
+
if "api_key" in mc:
|
|
1302
|
+
llm.api_key = mc["api_key"]
|
|
1303
|
+
if "temperature" in mc:
|
|
1304
|
+
llm.temperature = mc["temperature"]
|
|
1305
|
+
if "max_tokens" in mc:
|
|
1306
|
+
llm.max_tokens = mc["max_tokens"]
|
|
1307
|
+
|
|
1308
|
+
# 强制重置客户端(切换 provider 可能需要不同的 client 实例)
|
|
1309
|
+
llm._client = None
|
|
1310
|
+
|
|
1311
|
+
response = await self.core.process_message(message, session_id)
|
|
1312
|
+
|
|
1313
|
+
# 检查是否成功(如果回复包含错误标记)
|
|
1314
|
+
if response and not response.startswith("⚠️ LLM 调用失败") and not response.startswith("❌"):
|
|
1315
|
+
used_model_name = model_label
|
|
1316
|
+
if is_backup:
|
|
1317
|
+
logger.warning(f"🔄 主模型失败,成功切换到 {model_label}")
|
|
1318
|
+
return response
|
|
1319
|
+
|
|
1320
|
+
last_error = response
|
|
1321
|
+
logger.warning(f"{model_label} 返回无效响应,尝试下一个...")
|
|
1322
|
+
|
|
1323
|
+
except Exception as e:
|
|
1324
|
+
last_error = str(e)
|
|
1325
|
+
logger.warning(f"{model_label} 调用异常: {e},尝试下一个...")
|
|
1326
|
+
|
|
1327
|
+
finally:
|
|
1328
|
+
# 恢复原始模型配置
|
|
1329
|
+
llm.provider = orig["provider"]
|
|
1330
|
+
llm.model = orig["model"]
|
|
1331
|
+
llm.base_url = orig["base_url"]
|
|
1332
|
+
llm.api_key = orig["api_key"]
|
|
1333
|
+
llm.temperature = orig["temperature"]
|
|
1334
|
+
llm.max_tokens = orig["max_tokens"]
|
|
1335
|
+
llm._client = None
|
|
1336
|
+
|
|
1337
|
+
# 所有模型都失败
|
|
1338
|
+
return f"⚠️ 所有模型均调用失败 (共 {len(model_chain)} 个)。最后错误: {last_error}"
|
|
1339
|
+
|
|
1340
|
+
async def handle_reload_config(self, request):
|
|
1341
|
+
"""POST /api/config/reload - 从配置文件热重载(无需重启)
|
|
1342
|
+
|
|
1343
|
+
增强功能:
|
|
1344
|
+
- 广播通知所有正在执行任务的 Agent 暂停
|
|
1345
|
+
- 等待 Agent 安全暂停后执行配置更新
|
|
1346
|
+
- 更新完成后通知 Agent 恢复执行
|
|
1347
|
+
"""
|
|
1348
|
+
try:
|
|
1349
|
+
old_provider = self.core.config_mgr.config.llm.provider
|
|
1350
|
+
old_model = self.core.config_mgr.config.llm.model
|
|
1351
|
+
|
|
1352
|
+
# ── 广播通知:让所有活跃 Agent 暂停 ──
|
|
1353
|
+
broadcaster = self.core.config_broadcaster
|
|
1354
|
+
paused_count = 0
|
|
1355
|
+
if broadcaster:
|
|
1356
|
+
paused_count = await broadcaster.request_reload()
|
|
1357
|
+
logger.info(f"热重载: 已暂停 {paused_count} 个活跃任务")
|
|
1358
|
+
|
|
1359
|
+
# ── 执行配置重载 ──
|
|
1360
|
+
new_config = self.core.config_mgr.reload()
|
|
1361
|
+
logger.info("配置已热重载")
|
|
1362
|
+
|
|
1363
|
+
# ── 更新运行中的组件 ──
|
|
1364
|
+
changes = []
|
|
1365
|
+
if self.core.llm:
|
|
1366
|
+
llm_cfg = new_config.llm
|
|
1367
|
+
self.core.llm.provider = llm_cfg.provider
|
|
1368
|
+
self.core.llm.model = llm_cfg.model
|
|
1369
|
+
self.core.llm.base_url = llm_cfg.base_url
|
|
1370
|
+
self.core.llm.api_key = llm_cfg.api_key
|
|
1371
|
+
self.core.llm.temperature = llm_cfg.temperature
|
|
1372
|
+
self.core.llm.max_tokens = llm_cfg.max_tokens
|
|
1373
|
+
self.core.llm.timeout = llm_cfg.timeout
|
|
1374
|
+
self.core.llm.max_retries = llm_cfg.max_retries
|
|
1375
|
+
self.core.llm.anthropic_api_key = llm_cfg.anthropic_api_key
|
|
1376
|
+
# 强制重建客户端
|
|
1377
|
+
self.core.llm._client = None
|
|
1378
|
+
if llm_cfg.provider != old_provider or llm_cfg.model != old_model:
|
|
1379
|
+
changes.append(f"LLM: {old_provider}/{old_model} -> {llm_cfg.provider}/{llm_cfg.model}")
|
|
1380
|
+
|
|
1381
|
+
# 模型库变化
|
|
1382
|
+
old_models = set(m.id for m in self.core.config.models_library)
|
|
1383
|
+
new_models = set(m.id for m in new_config.models_library)
|
|
1384
|
+
if old_models != new_models:
|
|
1385
|
+
added = new_models - old_models
|
|
1386
|
+
removed = old_models - new_models
|
|
1387
|
+
parts = []
|
|
1388
|
+
if added:
|
|
1389
|
+
parts.append(f"新增模型: {', '.join(added)}")
|
|
1390
|
+
if removed:
|
|
1391
|
+
parts.append(f"移除模型: {', '.join(removed)}")
|
|
1392
|
+
if parts:
|
|
1393
|
+
changes.append("模型库: " + "; ".join(parts))
|
|
1394
|
+
|
|
1395
|
+
if self.core.executor:
|
|
1396
|
+
exe_cfg = new_config.executor
|
|
1397
|
+
self.core.executor.timeout = exe_cfg.timeout
|
|
1398
|
+
self.core.executor.max_retries = exe_cfg.max_retries
|
|
1399
|
+
self.core.executor.auto_fix = exe_cfg.auto_fix
|
|
1400
|
+
self.core.executor.max_output_length = exe_cfg.max_output_length
|
|
1401
|
+
if exe_cfg.sandbox_image:
|
|
1402
|
+
self.core.executor.sandbox_image = exe_cfg.sandbox_image
|
|
1403
|
+
if exe_cfg.execution_mode:
|
|
1404
|
+
self.core.executor.set_execution_mode(exe_cfg.execution_mode)
|
|
1405
|
+
|
|
1406
|
+
# 更新 app 引用
|
|
1407
|
+
self.core.config = new_config
|
|
1408
|
+
|
|
1409
|
+
# ── 广播通知:让 Agent 恢复执行 ──
|
|
1410
|
+
if broadcaster:
|
|
1411
|
+
await broadcaster.complete_reload()
|
|
1412
|
+
logger.info(f"热重载: {paused_count} 个任务已恢复")
|
|
1413
|
+
|
|
1414
|
+
logger.info(f"热重载完成,变更: {changes or '无显著变更'}")
|
|
1415
|
+
return web.json_response({
|
|
1416
|
+
"ok": True,
|
|
1417
|
+
"message": "配置已热重载",
|
|
1418
|
+
"changes": changes,
|
|
1419
|
+
"paused_tasks": paused_count,
|
|
1420
|
+
"broadcaster_stats": broadcaster.get_stats() if broadcaster else {},
|
|
1421
|
+
"config": self.core.config_mgr.get_full_config(),
|
|
1422
|
+
})
|
|
1423
|
+
except Exception as e:
|
|
1424
|
+
logger.error(f"热重载失败: {e}", exc_info=True)
|
|
1425
|
+
# 确保恢复广播器状态
|
|
1426
|
+
if self.core.config_broadcaster:
|
|
1427
|
+
self.core.config_broadcaster.force_reset()
|
|
1428
|
+
return web.json_response({"ok": False, "error": str(e)}, status=500)
|
|
1429
|
+
|
|
1430
|
+
async def handle_export_config(self, request):
|
|
1431
|
+
"""POST /api/config/export - 导出配置为 JSON 文件下载"""
|
|
1432
|
+
try:
|
|
1433
|
+
data = await request.json()
|
|
1434
|
+
except Exception:
|
|
1435
|
+
data = {}
|
|
1436
|
+
include_secrets = data.get("include_secrets", False)
|
|
1437
|
+
|
|
1438
|
+
try:
|
|
1439
|
+
export_data = self.core.config_mgr.export_config(include_secrets=include_secrets)
|
|
1440
|
+
filename = f"myagent_config_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
1441
|
+
|
|
1442
|
+
resp = web.Response(
|
|
1443
|
+
body=json.dumps(export_data, ensure_ascii=False, indent=2),
|
|
1444
|
+
content_type="application/json",
|
|
1445
|
+
headers={
|
|
1446
|
+
"Content-Disposition": f"attachment; filename=\"{filename}\"",
|
|
1447
|
+
},
|
|
1448
|
+
)
|
|
1449
|
+
logger.info(f"配置已导出: {filename} (secrets={include_secrets})")
|
|
1450
|
+
return resp
|
|
1451
|
+
except Exception as e:
|
|
1452
|
+
logger.error(f"导出配置失败: {e}", exc_info=True)
|
|
1453
|
+
return web.json_response({"ok": False, "error": str(e)}, status=500)
|
|
1454
|
+
|
|
1455
|
+
async def handle_import_config(self, request):
|
|
1456
|
+
"""POST /api/config/import - 从上传的 JSON 导入配置"""
|
|
1457
|
+
try:
|
|
1458
|
+
data = await request.json()
|
|
1459
|
+
except Exception:
|
|
1460
|
+
return web.json_response({"ok": False, "error": "无效的 JSON 数据"}, status=400)
|
|
1461
|
+
|
|
1462
|
+
overwrite = data.get("_overwrite", False) if isinstance(data, dict) else False
|
|
1463
|
+
|
|
1464
|
+
try:
|
|
1465
|
+
result = self.core.config_mgr.import_config(data, overwrite=overwrite)
|
|
1466
|
+
if not result["ok"]:
|
|
1467
|
+
return web.json_response(result, status=400)
|
|
1468
|
+
|
|
1469
|
+
# 热重载运行中的组件
|
|
1470
|
+
new_config = self.core.config_mgr.config
|
|
1471
|
+
if self.core.llm:
|
|
1472
|
+
llm_cfg = new_config.llm
|
|
1473
|
+
self.core.llm.provider = llm_cfg.provider
|
|
1474
|
+
self.core.llm.model = llm_cfg.model
|
|
1475
|
+
self.core.llm.base_url = llm_cfg.base_url
|
|
1476
|
+
self.core.llm.api_key = llm_cfg.api_key
|
|
1477
|
+
self.core.llm.temperature = llm_cfg.temperature
|
|
1478
|
+
self.core.llm.max_tokens = llm_cfg.max_tokens
|
|
1479
|
+
self.core.llm.timeout = llm_cfg.timeout
|
|
1480
|
+
self.core.llm.max_retries = llm_cfg.max_retries
|
|
1481
|
+
self.core.config = new_config
|
|
1482
|
+
|
|
1483
|
+
logger.info(f"配置已导入: {result['message']}")
|
|
1484
|
+
return web.json_response(result)
|
|
1485
|
+
except Exception as e:
|
|
1486
|
+
logger.error(f"导入配置失败: {e}", exc_info=True)
|
|
1487
|
+
return web.json_response({"ok": False, "error": str(e)}, status=500)
|
|
1488
|
+
|
|
1489
|
+
# ── 组织管理 ──
|
|
1490
|
+
|
|
1491
|
+
def _get_org_manager(self):
|
|
1492
|
+
"""获取组织管理器实例(懒加载)"""
|
|
1493
|
+
if not hasattr(self, '_org_manager'):
|
|
1494
|
+
from organization.manager import OrganizationManager
|
|
1495
|
+
self._org_manager = OrganizationManager(
|
|
1496
|
+
data_dir=self.core.config_mgr.data_dir
|
|
1497
|
+
)
|
|
1498
|
+
self._org_manager.initialize()
|
|
1499
|
+
return self._org_manager
|
|
1500
|
+
|
|
1501
|
+
def _get_org_rag(self):
|
|
1502
|
+
"""获取组织知识库 RAG 实例(懒加载)"""
|
|
1503
|
+
if not hasattr(self, '_org_rag'):
|
|
1504
|
+
from knowledge.rag import KnowledgeRAG
|
|
1505
|
+
org_mgr = self._get_org_manager()
|
|
1506
|
+
self._org_rag = KnowledgeRAG(
|
|
1507
|
+
kb_dir=org_mgr._knowledge_dir
|
|
1508
|
+
)
|
|
1509
|
+
self._org_rag.build_index()
|
|
1510
|
+
return self._org_rag
|
|
1511
|
+
|
|
1512
|
+
def _get_agent_knowledge_dir(self, agent_path: str) -> Path:
|
|
1513
|
+
"""获取 Agent 的知识库目录"""
|
|
1514
|
+
return self._agent_dir(agent_path) / "knowledge"
|
|
1515
|
+
|
|
1516
|
+
def _get_agent_rag(self, agent_path: str):
|
|
1517
|
+
"""获取 Agent 知识库 RAG 实例"""
|
|
1518
|
+
if not hasattr(self, '_agent_rags'):
|
|
1519
|
+
self._agent_rags = {}
|
|
1520
|
+
if agent_path not in self._agent_rags:
|
|
1521
|
+
from knowledge.rag import KnowledgeRAG
|
|
1522
|
+
kb_dir = self._get_agent_knowledge_dir(agent_path)
|
|
1523
|
+
kb_dir.mkdir(parents=True, exist_ok=True)
|
|
1524
|
+
rag = KnowledgeRAG(kb_dir=kb_dir)
|
|
1525
|
+
rag.build_index()
|
|
1526
|
+
self._agent_rags[agent_path] = rag
|
|
1527
|
+
return self._agent_rags[agent_path]
|
|
1528
|
+
|
|
1529
|
+
def _check_org_admin(self, agent_path: str = "") -> bool:
|
|
1530
|
+
"""检查 agent 是否是组织知识库管理员"""
|
|
1531
|
+
org_cfg = self.core.config.organization
|
|
1532
|
+
admin = org_cfg.knowledge_admin or "default"
|
|
1533
|
+
return agent_path == admin
|
|
1534
|
+
|
|
1535
|
+
async def handle_get_organization(self, request):
|
|
1536
|
+
"""GET /api/organization - 获取组织配置和组织信息"""
|
|
1537
|
+
org_cfg = self.core.config.organization
|
|
1538
|
+
org_mgr = self._get_org_manager()
|
|
1539
|
+
org_info = org_mgr.get_org_info()
|
|
1540
|
+
return web.json_response({
|
|
1541
|
+
"enabled": org_cfg.enabled,
|
|
1542
|
+
"knowledge_admin": org_cfg.knowledge_admin,
|
|
1543
|
+
"info": org_info,
|
|
1544
|
+
})
|
|
1545
|
+
|
|
1546
|
+
async def handle_update_organization(self, request):
|
|
1547
|
+
"""PUT /api/organization - 更新组织配置"""
|
|
1548
|
+
data = await request.json()
|
|
1549
|
+
cfg_path = self.core.config_mgr._config_file
|
|
1550
|
+
cfg_data = json.loads(cfg_path.read_text()) if cfg_path.exists() else {}
|
|
1551
|
+
org = cfg_data.setdefault("organization", {})
|
|
1552
|
+
if "enabled" in data:
|
|
1553
|
+
org["enabled"] = bool(data["enabled"])
|
|
1554
|
+
if "knowledge_admin" in data:
|
|
1555
|
+
org["knowledge_admin"] = str(data["knowledge_admin"])
|
|
1556
|
+
cfg_path.write_text(json.dumps(cfg_data, indent=2, ensure_ascii=False))
|
|
1557
|
+
# 热更新内存配置
|
|
1558
|
+
self.core.config.organization.enabled = org.get("enabled", False)
|
|
1559
|
+
self.core.config.organization.knowledge_admin = org.get("knowledge_admin", "")
|
|
1560
|
+
logger.info(f"组织配置已更新: enabled={org.get('enabled')}, admin={org.get('knowledge_admin')}")
|
|
1561
|
+
return web.json_response({"ok": True})
|
|
1562
|
+
|
|
1563
|
+
async def handle_get_org_info(self, request):
|
|
1564
|
+
"""GET /api/organization/info - 获取组织信息"""
|
|
1565
|
+
org_mgr = self._get_org_manager()
|
|
1566
|
+
content = org_mgr.get_org_info()
|
|
1567
|
+
return web.json_response({"content": content})
|
|
1568
|
+
|
|
1569
|
+
async def handle_update_org_info(self, request):
|
|
1570
|
+
"""PUT /api/organization/info - 更新组织信息"""
|
|
1571
|
+
data = await request.json()
|
|
1572
|
+
content = data.get("content", "")
|
|
1573
|
+
org_mgr = self._get_org_manager()
|
|
1574
|
+
ok = org_mgr.update_org_info(content)
|
|
1575
|
+
if ok:
|
|
1576
|
+
return web.json_response({"ok": True})
|
|
1577
|
+
return web.json_response({"ok": False, "error": "更新失败"}, status=500)
|
|
1578
|
+
|
|
1579
|
+
async def handle_list_org_knowledge(self, request):
|
|
1580
|
+
"""GET /api/organization/knowledge - 列出组织知识库文件"""
|
|
1581
|
+
org_mgr = self._get_org_manager()
|
|
1582
|
+
files = org_mgr.list_knowledge_files()
|
|
1583
|
+
return web.json_response(files)
|
|
1584
|
+
|
|
1585
|
+
async def handle_upload_org_knowledge(self, request):
|
|
1586
|
+
"""POST /api/organization/knowledge/upload - 上传文件到组织知识库"""
|
|
1587
|
+
# 权限检查
|
|
1588
|
+
agent = request.query.get("agent", "default")
|
|
1589
|
+
if not self._check_org_admin(agent):
|
|
1590
|
+
return web.json_response(
|
|
1591
|
+
{"ok": False, "error": "权限不足:只有知识库管理员才能上传"},
|
|
1592
|
+
status=403,
|
|
1593
|
+
)
|
|
1594
|
+
|
|
1595
|
+
data = await request.json()
|
|
1596
|
+
# 支持单个文件或批量文件
|
|
1597
|
+
files = data.get("files", [])
|
|
1598
|
+
if not files:
|
|
1599
|
+
# 兼容单文件格式
|
|
1600
|
+
filename = data.get("filename", data.get("name", ""))
|
|
1601
|
+
content = data.get("content", "")
|
|
1602
|
+
if not filename or content is None:
|
|
1603
|
+
return web.json_response({"ok": False, "error": "缺少 filename 或 content"}, status=400)
|
|
1604
|
+
files = [{"name": filename, "content": content}]
|
|
1605
|
+
|
|
1606
|
+
org_mgr = self._get_org_manager()
|
|
1607
|
+
results = []
|
|
1608
|
+
for f in files:
|
|
1609
|
+
name = f.get("name", "")
|
|
1610
|
+
content = f.get("content", "")
|
|
1611
|
+
result = org_mgr.upload_file(name, content, uploaded_by=agent)
|
|
1612
|
+
results.append(result)
|
|
1613
|
+
|
|
1614
|
+
# 刷新 RAG 索引
|
|
1615
|
+
if hasattr(self, '_org_rag'):
|
|
1616
|
+
self._org_rag.build_index()
|
|
1617
|
+
|
|
1618
|
+
return web.json_response({"ok": True, "results": results})
|
|
1619
|
+
|
|
1620
|
+
async def handle_delete_org_knowledge(self, request):
|
|
1621
|
+
"""DELETE /api/organization/knowledge?path=xxx - 删除组织知识库文件"""
|
|
1622
|
+
# 权限检查
|
|
1623
|
+
agent = request.query.get("agent", "default")
|
|
1624
|
+
if not self._check_org_admin(agent):
|
|
1625
|
+
return web.json_response(
|
|
1626
|
+
{"ok": False, "error": "权限不足:只有知识库管理员才能删除"},
|
|
1627
|
+
status=403,
|
|
1628
|
+
)
|
|
1629
|
+
|
|
1630
|
+
file_path = request.query.get("path", "").strip()
|
|
1631
|
+
if not file_path:
|
|
1632
|
+
return web.json_response({"ok": False, "error": "缺少 path 参数"}, status=400)
|
|
1633
|
+
|
|
1634
|
+
org_mgr = self._get_org_manager()
|
|
1635
|
+
result = org_mgr.delete_file(file_path)
|
|
1636
|
+
|
|
1637
|
+
# 刷新 RAG 索引
|
|
1638
|
+
if result.get("ok") and hasattr(self, '_org_rag'):
|
|
1639
|
+
self._org_rag.build_index()
|
|
1640
|
+
|
|
1641
|
+
return web.json_response(result)
|
|
1642
|
+
|
|
1643
|
+
# ── Agent 知识库 ──
|
|
1644
|
+
|
|
1645
|
+
async def handle_list_agent_knowledge(self, request):
|
|
1646
|
+
"""GET /api/agents/{name}/knowledge - 列出 Agent 个人知识库文件"""
|
|
1647
|
+
agent_path = request.match_info["name"]
|
|
1648
|
+
kb_dir = self._get_agent_knowledge_dir(agent_path)
|
|
1649
|
+
if not kb_dir.exists():
|
|
1650
|
+
return web.json_response([])
|
|
1651
|
+
files = []
|
|
1652
|
+
for item in sorted(kb_dir.iterdir()):
|
|
1653
|
+
if item.is_file():
|
|
1654
|
+
try:
|
|
1655
|
+
stat = item.stat()
|
|
1656
|
+
files.append({
|
|
1657
|
+
"name": item.name,
|
|
1658
|
+
"path": item.name,
|
|
1659
|
+
"type": "file",
|
|
1660
|
+
"size": stat.st_size,
|
|
1661
|
+
})
|
|
1662
|
+
except OSError:
|
|
1663
|
+
pass
|
|
1664
|
+
elif item.is_dir():
|
|
1665
|
+
sub_files = [f for f in item.iterdir() if f.is_file()]
|
|
1666
|
+
files.append({
|
|
1667
|
+
"name": item.name,
|
|
1668
|
+
"path": item.name,
|
|
1669
|
+
"type": "dir",
|
|
1670
|
+
"size": 0,
|
|
1671
|
+
"file_count": len(sub_files),
|
|
1672
|
+
})
|
|
1673
|
+
return web.json_response(files)
|
|
1674
|
+
|
|
1675
|
+
async def handle_upload_agent_knowledge(self, request):
|
|
1676
|
+
"""POST /api/agents/{name}/knowledge/upload - 上传到 Agent 知识库"""
|
|
1677
|
+
agent_path = request.match_info["name"]
|
|
1678
|
+
data = await request.json()
|
|
1679
|
+
files = data.get("files", [])
|
|
1680
|
+
if not files:
|
|
1681
|
+
filename = data.get("filename", data.get("name", ""))
|
|
1682
|
+
content = data.get("content", "")
|
|
1683
|
+
if not filename or content is None:
|
|
1684
|
+
return web.json_response({"ok": False, "error": "缺少 filename 或 content"}, status=400)
|
|
1685
|
+
files = [{"name": filename, "content": content}]
|
|
1686
|
+
|
|
1687
|
+
kb_dir = self._get_agent_knowledge_dir(agent_path)
|
|
1688
|
+
kb_dir.mkdir(parents=True, exist_ok=True)
|
|
1689
|
+
results = []
|
|
1690
|
+
for f in files:
|
|
1691
|
+
name = f.get("name", "")
|
|
1692
|
+
content = f.get("content", "")
|
|
1693
|
+
# 安全校验
|
|
1694
|
+
safe_name = Path(name).name
|
|
1695
|
+
if ".." in name or name.startswith("/"):
|
|
1696
|
+
results.append({"ok": False, "message": "非法文件名"})
|
|
1697
|
+
continue
|
|
1698
|
+
if "/" in name:
|
|
1699
|
+
parts = name.split("/")
|
|
1700
|
+
parent = kb_dir / "/".join(parts[:-1])
|
|
1701
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
1702
|
+
target = parent / parts[-1]
|
|
1703
|
+
else:
|
|
1704
|
+
target = kb_dir / safe_name
|
|
1705
|
+
try:
|
|
1706
|
+
target.write_text(content, encoding="utf-8")
|
|
1707
|
+
results.append({"ok": True, "path": name})
|
|
1708
|
+
except Exception as e:
|
|
1709
|
+
results.append({"ok": False, "message": str(e)})
|
|
1710
|
+
|
|
1711
|
+
# 刷新 RAG 索引
|
|
1712
|
+
if hasattr(self, '_agent_rags') and agent_path in self._agent_rags:
|
|
1713
|
+
self._agent_rags[agent_path].build_index()
|
|
1714
|
+
|
|
1715
|
+
return web.json_response({"ok": True, "results": results})
|
|
1716
|
+
|
|
1717
|
+
async def handle_delete_agent_knowledge(self, request):
|
|
1718
|
+
"""DELETE /api/agents/{name}/knowledge?path=xxx - 删除 Agent 知识库文件"""
|
|
1719
|
+
agent_path = request.match_info["name"]
|
|
1720
|
+
file_path = request.query.get("path", "").strip()
|
|
1721
|
+
if not file_path:
|
|
1722
|
+
return web.json_response({"ok": False, "error": "缺少 path 参数"}, status=400)
|
|
1723
|
+
|
|
1724
|
+
kb_dir = self._get_agent_knowledge_dir(agent_path)
|
|
1725
|
+
target = kb_dir / file_path
|
|
1726
|
+
|
|
1727
|
+
if ".." in file_path or file_path.startswith("/"):
|
|
1728
|
+
return web.json_response({"ok": False, "error": "非法路径"}, status=400)
|
|
1729
|
+
|
|
1730
|
+
if not target.exists():
|
|
1731
|
+
return web.json_response({"ok": False, "error": "文件不存在"}, status=404)
|
|
1732
|
+
|
|
1733
|
+
try:
|
|
1734
|
+
if target.is_dir():
|
|
1735
|
+
shutil.rmtree(target)
|
|
1736
|
+
else:
|
|
1737
|
+
target.unlink()
|
|
1738
|
+
except Exception as e:
|
|
1739
|
+
return web.json_response({"ok": False, "message": str(e)})
|
|
1740
|
+
|
|
1741
|
+
# 刷新 RAG 索引
|
|
1742
|
+
if hasattr(self, '_agent_rags') and agent_path in self._agent_rags:
|
|
1743
|
+
self._agent_rags[agent_path].build_index()
|
|
1744
|
+
|
|
1745
|
+
return web.json_response({"ok": True, "message": "已删除"})
|
|
1746
|
+
|
|
1747
|
+
# ── 知识库 RAG 搜索 ──
|
|
1748
|
+
|
|
1749
|
+
async def handle_knowledge_search(self, request):
|
|
1750
|
+
"""POST /api/knowledge/search - RAG 知识搜索
|
|
1751
|
+
|
|
1752
|
+
请求体:
|
|
1753
|
+
{
|
|
1754
|
+
"query": "搜索关键词",
|
|
1755
|
+
"scope": "org", # "org" 或 "agent:{path}"
|
|
1756
|
+
"top_k": 5
|
|
1757
|
+
}
|
|
1758
|
+
"""
|
|
1759
|
+
data = await request.json()
|
|
1760
|
+
query = data.get("query", "").strip()
|
|
1761
|
+
if not query:
|
|
1762
|
+
return web.json_response({"error": "query 不能为空"}, status=400)
|
|
1763
|
+
|
|
1764
|
+
scope = data.get("scope", "org")
|
|
1765
|
+
top_k = int(data.get("top_k", 5))
|
|
1766
|
+
|
|
1767
|
+
try:
|
|
1768
|
+
if scope.startswith("agent:"):
|
|
1769
|
+
agent_path = scope[6:]
|
|
1770
|
+
rag = self._get_agent_rag(agent_path)
|
|
1771
|
+
else:
|
|
1772
|
+
rag = self._get_org_rag()
|
|
1773
|
+
|
|
1774
|
+
results = rag.search(query, top_k=top_k)
|
|
1775
|
+
return web.json_response({
|
|
1776
|
+
"query": query,
|
|
1777
|
+
"scope": scope,
|
|
1778
|
+
"total_chunks": rag.total_chunks,
|
|
1779
|
+
"results": [r.to_dict() for r in results],
|
|
1780
|
+
})
|
|
1781
|
+
except Exception as e:
|
|
1782
|
+
logger.error(f"知识库搜索失败: {e}", exc_info=True)
|
|
1783
|
+
return web.json_response({"error": str(e)}, status=500)
|
|
1784
|
+
|
|
1785
|
+
# ── 群聊管理 ──
|
|
1786
|
+
async def handle_list_groups(self, request):
|
|
1787
|
+
"""GET /api/groups - 列出所有群"""
|
|
1788
|
+
mgr = self._get_group_manager()
|
|
1789
|
+
groups = [g.to_dict() for g in mgr.list_groups()]
|
|
1790
|
+
return web.json_response(groups)
|
|
1791
|
+
|
|
1792
|
+
async def handle_create_group(self, request):
|
|
1793
|
+
"""POST /api/groups - 创建群"""
|
|
1794
|
+
data = await request.json()
|
|
1795
|
+
name = data.get("name", "").strip()
|
|
1796
|
+
if not name:
|
|
1797
|
+
return web.json_response({"error": "群名不能为空"}, status=400)
|
|
1798
|
+
owner = data.get("owner", "default")
|
|
1799
|
+
description = data.get("description", "")
|
|
1800
|
+
avatar_emoji = data.get("avatar_emoji", "👥")
|
|
1801
|
+
avatar_color = data.get("avatar_color", "")
|
|
1802
|
+
member_paths = data.get("members", [])
|
|
1803
|
+
mgr = self._get_group_manager()
|
|
1804
|
+
group = mgr.create_group(
|
|
1805
|
+
name=name, owner=owner, description=description,
|
|
1806
|
+
avatar_emoji=avatar_emoji, avatar_color=avatar_color,
|
|
1807
|
+
member_paths=member_paths,
|
|
1808
|
+
)
|
|
1809
|
+
return web.json_response({"ok": True, "group": group.to_dict()})
|
|
1810
|
+
|
|
1811
|
+
async def handle_get_group(self, request):
|
|
1812
|
+
"""GET /api/groups/{gid} - 获取群详情"""
|
|
1813
|
+
gid = request.match_info["gid"]
|
|
1814
|
+
mgr = self._get_group_manager()
|
|
1815
|
+
group = mgr.get_group(gid)
|
|
1816
|
+
if not group:
|
|
1817
|
+
return web.json_response({"error": "群不存在"}, status=404)
|
|
1818
|
+
return web.json_response(group.to_dict())
|
|
1819
|
+
|
|
1820
|
+
async def handle_update_group(self, request):
|
|
1821
|
+
"""PUT /api/groups/{gid} - 更新群信息"""
|
|
1822
|
+
gid = request.match_info["gid"]
|
|
1823
|
+
data = await request.json()
|
|
1824
|
+
mgr = self._get_group_manager()
|
|
1825
|
+
group = mgr.update_group(
|
|
1826
|
+
gid,
|
|
1827
|
+
name=data.get("name", ""),
|
|
1828
|
+
description=data.get("description", ""),
|
|
1829
|
+
avatar_emoji=data.get("avatar_emoji", ""),
|
|
1830
|
+
avatar_color=data.get("avatar_color", ""),
|
|
1831
|
+
)
|
|
1832
|
+
if not group:
|
|
1833
|
+
return web.json_response({"error": "群不存在"}, status=404)
|
|
1834
|
+
return web.json_response({"ok": True, "group": group.to_dict()})
|
|
1835
|
+
|
|
1836
|
+
async def handle_delete_group(self, request):
|
|
1837
|
+
"""DELETE /api/groups/{gid} - 解散群"""
|
|
1838
|
+
gid = request.match_info["gid"]
|
|
1839
|
+
data = {}
|
|
1840
|
+
try:
|
|
1841
|
+
data = await request.json()
|
|
1842
|
+
except Exception:
|
|
1843
|
+
pass
|
|
1844
|
+
operator = data.get("operator", "")
|
|
1845
|
+
mgr = self._get_group_manager()
|
|
1846
|
+
ok = mgr.delete_group(gid, operator=operator)
|
|
1847
|
+
if not ok:
|
|
1848
|
+
return web.json_response({"error": "解散失败(群不存在或权限不足)"}, status=400)
|
|
1849
|
+
return web.json_response({"ok": True})
|
|
1850
|
+
|
|
1851
|
+
async def handle_group_stats(self, request):
|
|
1852
|
+
"""GET /api/groups/{gid}/stats - 获取群统计"""
|
|
1853
|
+
gid = request.match_info["gid"]
|
|
1854
|
+
mgr = self._get_group_manager()
|
|
1855
|
+
stats = mgr.get_group_stats(gid)
|
|
1856
|
+
if not stats:
|
|
1857
|
+
return web.json_response({"error": "群不存在"}, status=404)
|
|
1858
|
+
return web.json_response(stats)
|
|
1859
|
+
|
|
1860
|
+
# ── 群成员管理 ──
|
|
1861
|
+
async def handle_add_member(self, request):
|
|
1862
|
+
"""POST /api/groups/{gid}/members - 添加成员"""
|
|
1863
|
+
gid = request.match_info["gid"]
|
|
1864
|
+
data = await request.json()
|
|
1865
|
+
agent_path = data.get("agent_path", "").strip()
|
|
1866
|
+
if not agent_path:
|
|
1867
|
+
return web.json_response({"error": "agent_path 不能为空"}, status=400)
|
|
1868
|
+
role = data.get("role", "member")
|
|
1869
|
+
nickname = data.get("nickname", "")
|
|
1870
|
+
operator = data.get("operator", "")
|
|
1871
|
+
mgr = self._get_group_manager()
|
|
1872
|
+
group = mgr.add_member(gid, agent_path, role=role, nickname=nickname, operator=operator)
|
|
1873
|
+
if not group:
|
|
1874
|
+
return web.json_response({"error": "添加失败(群不存在、已存在或权限不足)"}, status=400)
|
|
1875
|
+
return web.json_response({"ok": True, "group": group.to_dict()})
|
|
1876
|
+
|
|
1877
|
+
async def handle_remove_member(self, request):
|
|
1878
|
+
"""DELETE /api/groups/{gid}/members/{agent_path} - 移除成员"""
|
|
1879
|
+
gid = request.match_info["gid"]
|
|
1880
|
+
agent_path = request.match_info["agent_path"]
|
|
1881
|
+
data = {}
|
|
1882
|
+
try:
|
|
1883
|
+
data = await request.json()
|
|
1884
|
+
except Exception:
|
|
1885
|
+
pass
|
|
1886
|
+
operator = data.get("operator", "")
|
|
1887
|
+
mgr = self._get_group_manager()
|
|
1888
|
+
group = mgr.remove_member(gid, agent_path, operator=operator)
|
|
1889
|
+
if not group:
|
|
1890
|
+
return web.json_response({"error": "移除失败"}, status=400)
|
|
1891
|
+
return web.json_response({"ok": True, "group": group.to_dict()})
|
|
1892
|
+
|
|
1893
|
+
async def handle_set_member_role(self, request):
|
|
1894
|
+
"""PUT /api/groups/{gid}/members/{agent_path}/role - 设置成员角色"""
|
|
1895
|
+
gid = request.match_info["gid"]
|
|
1896
|
+
agent_path = request.match_info["agent_path"]
|
|
1897
|
+
data = await request.json()
|
|
1898
|
+
role = data.get("role", "member")
|
|
1899
|
+
operator = data.get("operator", "")
|
|
1900
|
+
mgr = self._get_group_manager()
|
|
1901
|
+
group = mgr.set_member_role(gid, agent_path, role=role, operator=operator)
|
|
1902
|
+
if not group:
|
|
1903
|
+
return web.json_response({"error": "设置失败(群不存在、无权限或目标不存在)"}, status=400)
|
|
1904
|
+
return web.json_response({"ok": True, "group": group.to_dict()})
|
|
1905
|
+
|
|
1906
|
+
async def handle_set_member_muted(self, request):
|
|
1907
|
+
"""PUT /api/groups/{gid}/members/{agent_path}/mute - 设置禁言"""
|
|
1908
|
+
gid = request.match_info["gid"]
|
|
1909
|
+
agent_path = request.match_info["agent_path"]
|
|
1910
|
+
data = await request.json()
|
|
1911
|
+
muted = data.get("muted", False)
|
|
1912
|
+
operator = data.get("operator", "")
|
|
1913
|
+
mgr = self._get_group_manager()
|
|
1914
|
+
group = mgr.set_member_muted(gid, agent_path, muted=muted, operator=operator)
|
|
1915
|
+
if not group:
|
|
1916
|
+
return web.json_response({"error": "设置失败"}, status=400)
|
|
1917
|
+
return web.json_response({"ok": True, "group": group.to_dict()})
|
|
1918
|
+
|
|
1919
|
+
# ── 群消息 ──
|
|
1920
|
+
async def handle_get_group_messages(self, request):
|
|
1921
|
+
"""GET /api/groups/{gid}/messages - 获取群消息"""
|
|
1922
|
+
gid = request.match_info["gid"]
|
|
1923
|
+
mgr = self._get_group_manager()
|
|
1924
|
+
limit = int(request.query.get("limit", "100"))
|
|
1925
|
+
before = float(request.query.get("before", "0"))
|
|
1926
|
+
messages = mgr.get_messages(gid, limit=limit, before=before)
|
|
1927
|
+
return web.json_response([m.to_dict() for m in messages])
|
|
1928
|
+
|
|
1929
|
+
async def handle_send_group_message(self, request):
|
|
1930
|
+
"""POST /api/groups/{gid}/messages - 发送群消息(广播到所有成员agent)"""
|
|
1931
|
+
gid = request.match_info["gid"]
|
|
1932
|
+
data = await request.json()
|
|
1933
|
+
content = data.get("message", "").strip()
|
|
1934
|
+
if not content:
|
|
1935
|
+
return web.json_response({"error": "消息不能为空"}, status=400)
|
|
1936
|
+
|
|
1937
|
+
mgr = self._get_group_manager()
|
|
1938
|
+
group = mgr.get_group(gid)
|
|
1939
|
+
if not group:
|
|
1940
|
+
return web.json_response({"error": "群不存在"}, status=404)
|
|
1941
|
+
|
|
1942
|
+
# 1. 保存用户消息
|
|
1943
|
+
from groups.manager import GroupMessage
|
|
1944
|
+
user_msg = GroupMessage(
|
|
1945
|
+
group_id=gid,
|
|
1946
|
+
sender="user",
|
|
1947
|
+
sender_name="用户",
|
|
1948
|
+
sender_avatar="👤",
|
|
1949
|
+
content=content,
|
|
1950
|
+
)
|
|
1951
|
+
mgr.add_message(user_msg)
|
|
1952
|
+
|
|
1953
|
+
# 2. 广播到所有非禁言成员agent,并行处理
|
|
1954
|
+
responses = []
|
|
1955
|
+
active_members = [m for m in group.members if not m.muted]
|
|
1956
|
+
|
|
1957
|
+
import asyncio
|
|
1958
|
+
async def process_agent_member(member):
|
|
1959
|
+
try:
|
|
1960
|
+
agent_path = member.agent_path
|
|
1961
|
+
agent_cfg = self._read_agent_config(agent_path)
|
|
1962
|
+
model_chain = self._build_model_chain(agent_cfg, agent_path)
|
|
1963
|
+
session_id = f"group_{gid}_{agent_path}"
|
|
1964
|
+
|
|
1965
|
+
if model_chain and self.core.llm:
|
|
1966
|
+
response = await self._try_model_chain(model_chain, content, session_id)
|
|
1967
|
+
else:
|
|
1968
|
+
response = await self.core.process_message(content, session_id)
|
|
1969
|
+
|
|
1970
|
+
# 获取agent的显示信息
|
|
1971
|
+
avatar = "🤖"
|
|
1972
|
+
display_name = agent_path
|
|
1973
|
+
if agent_cfg:
|
|
1974
|
+
avatar = agent_cfg.get("avatar_emoji", "🤖") or "🤖"
|
|
1975
|
+
display_name = agent_cfg.get("name", agent_path)
|
|
1976
|
+
|
|
1977
|
+
# 保存agent回复到群消息
|
|
1978
|
+
agent_msg = GroupMessage(
|
|
1979
|
+
group_id=gid,
|
|
1980
|
+
sender="agent",
|
|
1981
|
+
sender_name=display_name,
|
|
1982
|
+
sender_avatar=avatar,
|
|
1983
|
+
content=response,
|
|
1984
|
+
agent_path=agent_path,
|
|
1985
|
+
)
|
|
1986
|
+
mgr.add_message(agent_msg)
|
|
1987
|
+
|
|
1988
|
+
return {
|
|
1989
|
+
"ok": True,
|
|
1990
|
+
"agent_path": agent_path,
|
|
1991
|
+
"name": display_name,
|
|
1992
|
+
"avatar": avatar,
|
|
1993
|
+
"response": response,
|
|
1994
|
+
}
|
|
1995
|
+
except Exception as e:
|
|
1996
|
+
logger.error(f"群消息处理失败 ({member.agent_path}): {e}")
|
|
1997
|
+
return {
|
|
1998
|
+
"ok": False,
|
|
1999
|
+
"agent_path": member.agent_path,
|
|
2000
|
+
"name": member.agent_path,
|
|
2001
|
+
"avatar": "❌",
|
|
2002
|
+
"response": f"处理失败: {str(e)}",
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
# 并行调用所有成员agent
|
|
2006
|
+
tasks = [process_agent_member(m) for m in active_members]
|
|
2007
|
+
responses = await asyncio.gather(*tasks, return_exceptions=True)
|
|
2008
|
+
|
|
2009
|
+
# 处理异常结果
|
|
2010
|
+
final_responses = []
|
|
2011
|
+
for r in responses:
|
|
2012
|
+
if isinstance(r, Exception):
|
|
2013
|
+
final_responses.append({
|
|
2014
|
+
"ok": False, "agent_path": "unknown",
|
|
2015
|
+
"name": "unknown", "avatar": "❌",
|
|
2016
|
+
"response": f"异常: {str(r)}",
|
|
2017
|
+
})
|
|
2018
|
+
else:
|
|
2019
|
+
final_responses.append(r)
|
|
2020
|
+
|
|
2021
|
+
return web.json_response({
|
|
2022
|
+
"ok": True,
|
|
2023
|
+
"group_id": gid,
|
|
2024
|
+
"user_message": user_msg.to_dict(),
|
|
2025
|
+
"responses": final_responses,
|
|
2026
|
+
})
|
|
2027
|
+
|
|
2028
|
+
async def handle_clear_group_messages(self, request):
|
|
2029
|
+
"""DELETE /api/groups/{gid}/messages - 清空群消息"""
|
|
2030
|
+
gid = request.match_info["gid"]
|
|
2031
|
+
mgr = self._get_group_manager()
|
|
2032
|
+
ok = mgr.clear_messages(gid)
|
|
2033
|
+
return web.json_response({"ok": ok})
|
|
2034
|
+
|
|
2035
|
+
async def start(self, port: int = 8765):
|
|
2036
|
+
self._runner = web.AppRunner(self.app)
|
|
2037
|
+
await self._runner.setup()
|
|
2038
|
+
site = web.TCPSite(self._runner, "127.0.0.1", port)
|
|
2039
|
+
await site.start()
|
|
2040
|
+
logger.info(f"管理后台: http://127.0.0.1:{port}/ui/")
|
|
2041
|
+
|
|
2042
|
+
async def stop(self):
|
|
2043
|
+
if self._runner: await self._runner.cleanup()
|