with '#' will be ignored, and an empty message aborts the commit. On branch main Your branch is up to date with 'origin/main'. Changes to be committed: new file: .claude/skills/algorithmic-art/.openskills.json new file: .claude/skills/algorithmic-art/LICENSE.txt new file: .claude/skills/algorithmic-art/SKILL.md new file: .claude/skills/algorithmic-art/templates/generator_template.js new file: .claude/skills/algorithmic-art/templates/viewer.html new file: .claude/skills/brand-guidelines/.openskills.json new file: .claude/skills/brand-guidelines/LICENSE.txt new file: .claude/skills/brand-guidelines/SKILL.md new file: .claude/skills/canvas-design/.openskills.json new file: .claude/skills/canvas-design/LICENSE.txt new file: .claude/skills/canvas-design/SKILL.md new file: .claude/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf new file: .claude/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/DMMono-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/Gloock-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf new file: .claude/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf new file: .claude/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf new file: .claude/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf new file: .claude/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf new file: .claude/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/Italiana-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/Jura-Light.ttf new file: .claude/skills/canvas-design/canvas-fonts/Jura-Medium.ttf new file: .claude/skills/canvas-design/canvas-fonts/Jura-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/Lora-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf new file: .claude/skills/canvas-design/canvas-fonts/Lora-Italic.ttf new file: .claude/skills/canvas-design/canvas-fonts/Lora-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/Lora-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/Outfit-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf new file: .claude/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf new file: .claude/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf new file: .claude/skills/canvas-design/canvas-fonts/Tektur-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf new file: .claude/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf new file: .claude/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf new file: .claude/skills/doc-coauthoring/.openskills.json new file: .claude/skills/doc-coauthoring/SKILL.md new file: .claude/skills/docx/.openskills.json new file: .claude/skills/docx/LICENSE.txt new file: .claude/skills/docx/SKILL.md new file: .claude/skills/docx/scripts/__init__.py new file: .claude/skills/docx/scripts/accept_changes.py new file: .claude/skills/docx/scripts/comment.py new file: .claude/skills/docx/scripts/office/helpers/__init__.py new file: .claude/skills/docx/scripts/office/helpers/merge_runs.py new file: .claude/skills/docx/scripts/office/helpers/simplify_redlines.py new file: .claude/skills/docx/scripts/office/pack.py new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd new file: .claude/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd new file: .claude/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd new file: .claude/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd new file: .claude/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd new file: .claude/skills/docx/scripts/office/schemas/mce/mc.xsd new file: .claude/skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd new file: .claude/skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd new file: .claude/skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd new file: .claude/skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd new file: .claude/skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd new file: .claude/skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd new file: .claude/skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd new file: .claude/skills/docx/scripts/office/soffice.py new file: .claude/skills/docx/scripts/office/unpack.py new file: .claude/skills/docx/scripts/office/validate.py new file: .claude/skills/docx/scripts/office/validators/__init__.py new file: .claude/skills/docx/scripts/office/validators/base.py new file: .claude/skills/docx/scripts/office/validators/docx.py new file: .claude/skills/docx/scripts/office/validators/pptx.py new file: .claude/skills/docx/scripts/office/validators/redlining.py new file: .claude/skills/docx/scripts/templates/comments.xml new file: .claude/skills/docx/scripts/templates/commentsExtended.xml new file: .claude/skills/docx/scripts/templates/commentsExtensible.xml new file: .claude/skills/docx/scripts/templates/commentsIds.xml new file: .claude/skills/docx/scripts/templates/people.xml new file: .claude/skills/frontend-design/.openskills.json new file: .claude/skills/frontend-design/LICENSE.txt new file: .claude/skills/frontend-design/SKILL.md new file: .claude/skills/internal-comms/.openskills.json new file: .claude/skills/internal-comms/LICENSE.txt new file: .claude/skills/internal-comms/SKILL.md new file: .claude/skills/internal-comms/examples/3p-updates.md new file: .claude/skills/internal-comms/examples/company-newsletter.md new file: .claude/skills/internal-comms/examples/faq-answers.md new file: .claude/skills/internal-comms/examples/general-comms.md new file: .claude/skills/mcp-builder/.openskills.json new file: .claude/skills/mcp-builder/LICENSE.txt new file: .claude/skills/mcp-builder/SKILL.md new file: .claude/skills/mcp-builder/reference/evaluation.md new file: .claude/skills/mcp-builder/reference/mcp_best_practices.md new file: .claude/skills/mcp-builder/reference/node_mcp_server.md new file: .claude/skills/mcp-builder/reference/python_mcp_server.md new file: .claude/skills/mcp-builder/scripts/connections.py new file: .claude/skills/mcp-builder/scripts/evaluation.py new file: .claude/skills/mcp-builder/scripts/example_evaluation.xml new file: .claude/skills/mcp-builder/scripts/requirements.txt new file: .claude/skills/pdf/.openskills.json new file: .claude/skills/pdf/LICENSE.txt new file: .claude/skills/pdf/SKILL.md new file: .claude/skills/pdf/forms.md new file: .claude/skills/pdf/reference.md new file: .claude/skills/pdf/scripts/check_bounding_boxes.py new file: .claude/skills/pdf/scripts/check_fillable_fields.py new file: .claude/skills/pdf/scripts/convert_pdf_to_images.py new file: .claude/skills/pdf/scripts/create_validation_image.py new file: .claude/skills/pdf/scripts/extract_form_field_info.py new file: .claude/skills/pdf/scripts/extract_form_structure.py new file: .claude/skills/pdf/scripts/fill_fillable_fields.py new file: .claude/skills/pdf/scripts/fill_pdf_form_with_annotations.py new file: .claude/skills/pptx/.openskills.json new file: .claude/skills/pptx/LICENSE.txt new file: .claude/skills/pptx/SKILL.md new file: .claude/skills/pptx/editing.md new file: .claude/skills/pptx/pptxgenjs.md new file: .claude/skills/pptx/scripts/__init__.py new file: .claude/skills/pptx/scripts/add_slide.py new file: .claude/skills/pptx/scripts/clean.py new file: .claude/skills/pptx/scripts/office/helpers/__init__.py new file: .claude/skills/pptx/scripts/office/helpers/merge_runs.py new file: .claude/skills/pptx/scripts/office/helpers/simplify_redlines.py new file: .claude/skills/pptx/scripts/office/pack.py new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd new file: .claude/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd new file: .claude/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd new file: .claude/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd new file: .claude/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd new file: .claude/skills/pptx/scripts/office/schemas/mce/mc.xsd new file: .claude/skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd new file: .claude/skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd new file: .claude/skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd new file: .claude/skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd new file: .claude/skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd new file: .claude/skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd new file: .claude/skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd new file: .claude/skills/pptx/scripts/office/soffice.py new file: .claude/skills/pptx/scripts/office/unpack.py new file: .claude/skills/pptx/scripts/office/validate.py new file: .claude/skills/pptx/scripts/office/validators/__init__.py new file: .claude/skills/pptx/scripts/office/validators/base.py new file: .claude/skills/pptx/scripts/office/validators/docx.py new file: .claude/skills/pptx/scripts/office/validators/pptx.py new file: .claude/skills/pptx/scripts/office/validators/redlining.py new file: .claude/skills/pptx/scripts/thumbnail.py new file: .claude/skills/skill-creator/.openskills.json new file: .claude/skills/skill-creator/LICENSE.txt new file: .claude/skills/skill-creator/SKILL.md new file: .claude/skills/skill-creator/agents/analyzer.md new file: .claude/skills/skill-creator/agents/comparator.md new file: .claude/skills/skill-creator/agents/grader.md new file: .claude/skills/skill-creator/assets/eval_review.html new file: .claude/skills/skill-creator/eval-viewer/generate_review.py new file: .claude/skills/skill-creator/eval-viewer/viewer.html new file: .claude/skills/skill-creator/references/schemas.md new file: .claude/skills/skill-creator/scripts/__init__.py new file: .claude/skills/skill-creator/scripts/aggregate_benchmark.py new file: .claude/skills/skill-creator/scripts/generate_report.py new file: .claude/skills/skill-creator/scripts/improve_description.py new file: .claude/skills/skill-creator/scripts/package_skill.py new file: .claude/skills/skill-creator/scripts/quick_validate.py new file: .claude/skills/skill-creator/scripts/run_eval.py new file: .claude/skills/skill-creator/scripts/run_loop.py new file: .claude/skills/skill-creator/scripts/utils.py new file: .claude/skills/slack-gif-creator/.openskills.json new file: .claude/skills/slack-gif-creator/LICENSE.txt new file: .claude/skills/slack-gif-creator/SKILL.md new file: .claude/skills/slack-gif-creator/core/easing.py new file: .claude/skills/slack-gif-creator/core/frame_composer.py new file: .claude/skills/slack-gif-creator/core/gif_builder.py new file: .claude/skills/slack-gif-creator/core/validators.py new file: .claude/skills/slack-gif-creator/requirements.txt new file: .claude/skills/template/.openskills.json new file: .claude/skills/template/SKILL.md new file: .claude/skills/theme-factory/.openskills.json new file: .claude/skills/theme-factory/LICENSE.txt new file: .claude/skills/theme-factory/SKILL.md new file: .claude/skills/theme-factory/theme-showcase.pdf new file: .claude/skills/theme-factory/themes/arctic-frost.md new file: .claude/skills/theme-factory/themes/botanical-garden.md new file: .claude/skills/theme-factory/themes/desert-rose.md new file: .claude/skills/theme-factory/themes/forest-canopy.md new file: .claude/skills/theme-factory/themes/golden-hour.md new file: .claude/skills/theme-factory/themes/midnight-galaxy.md new file: .claude/skills/theme-factory/themes/modern-minimalist.md new file: .claude/skills/theme-factory/themes/ocean-depths.md new file: .claude/skills/theme-factory/themes/sunset-boulevard.md new file: .claude/skills/theme-factory/themes/tech-innovation.md new file: .claude/skills/web-artifacts-builder/.openskills.json new file: .claude/skills/web-artifacts-builder/LICENSE.txt new file: .claude/skills/web-artifacts-builder/SKILL.md new file: .claude/skills/web-artifacts-builder/scripts/bundle-artifact.sh new file: .claude/skills/web-artifacts-builder/scripts/init-artifact.sh new file: .claude/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz new file: .claude/skills/webapp-testing/.openskills.json new file: .claude/skills/webapp-testing/LICENSE.txt new file: .claude/skills/webapp-testing/SKILL.md new file: .claude/skills/webapp-testing/examples/console_logging.py new file: .claude/skills/webapp-testing/examples/element_discovery.py new file: .claude/skills/webapp-testing/examples/static_html_automation.py new file: .claude/skills/webapp-testing/scripts/with_server.py new file: .claude/skills/xlsx/.openskills.json new file: .claude/skills/xlsx/LICENSE.txt new file: .claude/skills/xlsx/SKILL.md new file: .claude/skills/xlsx/scripts/office/helpers/__init__.py new file: .claude/skills/xlsx/scripts/office/helpers/merge_runs.py new file: .claude/skills/xlsx/scripts/office/helpers/simplify_redlines.py new file: .claude/skills/xlsx/scripts/office/pack.py new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd new file: .claude/skills/xlsx/scripts/office/schemas/mce/mc.xsd new file: .claude/skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd new file: .claude/skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd new file: .claude/skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd new file: .claude/skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd new file: .claude/skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd new file: .claude/skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd new file: .claude/skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd new file: .claude/skills/xlsx/scripts/office/soffice.py new file: .claude/skills/xlsx/scripts/office/unpack.py new file: .claude/skills/xlsx/scripts/office/validate.py new file: .claude/skills/xlsx/scripts/office/validators/__init__.py new file: .claude/skills/xlsx/scripts/office/validators/base.py new file: .claude/skills/xlsx/scripts/office/validators/docx.py new file: .claude/skills/xlsx/scripts/office/validators/pptx.py new file: .claude/skills/xlsx/scripts/office/validators/redlining.py new file: .claude/skills/xlsx/scripts/recalc.py new file: .env.example new file: .gitignore new file: config/mcp.json new file: config/models.json new file: config/personalities.json new file: docs/AGENTS.md new file: docs/AI_IMPLEMENTATION.md new file: docs/AI_INTEGRATION_COMPLETE.md new file: docs/AI_QUICKSTART.md new file: docs/AI_SUMMARY.md new file: docs/CHANGELOG.md new file: docs/CONFIG_GUIDE.md new file: docs/FIXES.md new file: docs/PROJECT_REFACTOR.md new file: docs/README.md new file: docs/README_INDEX.md new file: examples/ai_example.py new file: main.py new file: pytest.ini new file: requirements.txt new file: scripts/migrate_to_vector_db.py new file: skills/cmd_zip_skill/README.md new file: skills/cmd_zip_skill/__init__.py new file: skills/cmd_zip_skill/main.py new file: skills/cmd_zip_skill/skill.json new file: skills/cmd_zip_skill_1772465404375/README.md new file: skills/cmd_zip_skill_1772465404375/__init__.py new file: skills/cmd_zip_skill_1772465404375/main.py new file: skills/cmd_zip_skill_1772465404375/skill.json new file: skills/cmd_zip_skill_1772465434774/README.md new file: skills/cmd_zip_skill_1772465434774/__init__.py new file: skills/cmd_zip_skill_1772465434774/main.py new file: skills/cmd_zip_skill_1772465434774/skill.json new file: skills/cmd_zip_skill_1772465467809/README.md new file: skills/cmd_zip_skill_1772465467809/__init__.py new file: skills/cmd_zip_skill_1772465467809/main.py new file: skills/cmd_zip_skill_1772465467809/skill.json new file: skills/cmd_zip_skill_1772465652075/README.md new file: skills/cmd_zip_skill_1772465652075/__init__.py new file: skills/cmd_zip_skill_1772465652075/main.py new file: skills/cmd_zip_skill_1772465652075/skill.json new file: skills/cmd_zip_skill_1772465685352/README.md new file: skills/cmd_zip_skill_1772465685352/__init__.py new file: skills/cmd_zip_skill_1772465685352/main.py new file: skills/cmd_zip_skill_1772465685352/skill.json new file: skills/cmd_zip_skill_1772465936294/README.md new file: skills/cmd_zip_skill_1772465936294/__init__.py new file: skills/cmd_zip_skill_1772465936294/main.py new file: skills/cmd_zip_skill_1772465936294/skill.json new file: skills/cmd_zip_skill_1772465966322/README.md new file: skills/cmd_zip_skill_1772465966322/__init__.py new file: skills/cmd_zip_skill_1772465966322/main.py new file: skills/cmd_zip_skill_1772465966322/skill.json new file: skills/cmd_zip_skill_1772466071278/README.md new file: skills/cmd_zip_skill_1772466071278/__init__.py new file: skills/cmd_zip_skill_1772466071278/main.py new file: skills/cmd_zip_skill_1772466071278/skill.json new file: skills/skills_creator/README.md new file: skills/skills_creator/__init__.py new file: skills/skills_creator/main.py new file: skills/skills_creator/skill.json new file: src/__init__.py new file: src/ai/__init__.py new file: src/ai/base.py new file: src/ai/client.py new file: src/ai/docs/README.md new file: src/ai/mcp/__init__.py new file: src/ai/mcp/base.py new file: src/ai/mcp/servers/__init__.py new file: src/ai/mcp/servers/filesystem.py new file: src/ai/memory.py new file: src/ai/models/__init__.py new file: src/ai/models/anthropic_model.py new file: src/ai/models/openai_model.py new file: src/ai/personality.py new file: src/ai/skills/__init__.py new file: src/ai/skills/base.py new file: src/ai/task_manager.py new file: src/ai/vector_store/__init__.py new file: src/ai/vector_store/base.py new file: src/ai/vector_store/chroma_store.py new file: src/ai/vector_store/json_store.py new file: src/core/__init__.py new file: src/core/bot.py new file: src/core/config.py new file: src/handlers/__init__.py new file: src/handlers/message_handler.py new file: src/handlers/message_handler_ai.py new file: src/utils/__init__.py new file: src/utils/logger.py new file: start.bat new file: tests/test_ai.py
553 lines
18 KiB
Python
553 lines
18 KiB
Python
"""
|
||
Skills 系统 - 可扩展技能插件框架。
|
||
"""
|
||
|
||
from dataclasses import dataclass
|
||
import importlib
|
||
import inspect
|
||
import json
|
||
from pathlib import Path
|
||
import re
|
||
import shutil
|
||
import sys
|
||
import tempfile
|
||
import time
|
||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||
import urllib.request
|
||
import zipfile
|
||
import os
|
||
import stat
|
||
|
||
from src.utils.logger import setup_logger
|
||
|
||
logger = setup_logger("SkillsSystem")
|
||
|
||
|
||
@dataclass
|
||
class SkillMetadata:
|
||
"""技能元数据。"""
|
||
|
||
name: str
|
||
version: str
|
||
description: str
|
||
author: str
|
||
dependencies: List[str]
|
||
enabled: bool = True
|
||
|
||
|
||
class Skill:
|
||
"""技能基类。"""
|
||
|
||
def __init__(self):
|
||
self.metadata: Optional[SkillMetadata] = None
|
||
self.tools: Dict[str, Callable] = {}
|
||
self.manager = None
|
||
|
||
async def initialize(self):
|
||
"""初始化技能。"""
|
||
|
||
async def cleanup(self):
|
||
"""清理技能。"""
|
||
|
||
def get_tools(self) -> Dict[str, Callable]:
|
||
"""获取技能提供的工具。"""
|
||
|
||
return self.tools
|
||
|
||
def register_tool(self, name: str, func: Callable):
|
||
"""注册工具。"""
|
||
|
||
self.tools[name] = func
|
||
|
||
|
||
class SkillsManager:
|
||
"""技能管理器。"""
|
||
|
||
_SKILL_KEY_PATTERN = re.compile(r"[^a-zA-Z0-9_]")
|
||
_GITHUB_SHORTCUT_PATTERN = re.compile(
|
||
r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:#[A-Za-z0-9_.-]+)?$"
|
||
)
|
||
|
||
def __init__(self, skills_dir: Path):
|
||
self.skills_dir = skills_dir
|
||
self.skills: Dict[str, Skill] = {}
|
||
self.skills_dir.mkdir(parents=True, exist_ok=True)
|
||
logger.info(f"✅ Skills 目录: {skills_dir}")
|
||
|
||
@classmethod
|
||
def normalize_skill_key(cls, raw_name: str) -> str:
|
||
"""将任意输入规范化为可导入的 Python 包名。"""
|
||
|
||
key = raw_name.strip().lower().replace("-", "_").replace(" ", "_")
|
||
key = cls._SKILL_KEY_PATTERN.sub("_", key)
|
||
key = re.sub(r"_+", "_", key).strip("_")
|
||
|
||
if not key:
|
||
raise ValueError("技能名不能为空")
|
||
|
||
if key[0].isdigit():
|
||
key = f"skill_{key}"
|
||
|
||
return key
|
||
|
||
def _get_skill_path(self, skill_name: str) -> Path:
|
||
return self.skills_dir / self.normalize_skill_key(skill_name)
|
||
|
||
@staticmethod
|
||
def _on_rmtree_error(func, path, exc_info):
|
||
"""Handle Windows readonly/locked file deletion errors."""
|
||
try:
|
||
os.chmod(path, stat.S_IWRITE)
|
||
func(path)
|
||
except Exception:
|
||
# Keep original failure path for upper retry logic.
|
||
pass
|
||
|
||
def _read_metadata(self, skill_path: Path, fallback_name: str) -> Dict[str, Any]:
|
||
metadata_file = skill_path / "skill.json"
|
||
if metadata_file.exists():
|
||
with open(metadata_file, "r", encoding="utf-8") as f:
|
||
metadata = json.load(f)
|
||
else:
|
||
metadata = {}
|
||
|
||
metadata.setdefault("name", fallback_name)
|
||
metadata.setdefault("version", "1.0.0")
|
||
metadata.setdefault("description", f"{fallback_name} skill")
|
||
metadata.setdefault("author", "unknown")
|
||
metadata.setdefault("dependencies", [])
|
||
metadata.setdefault("enabled", True)
|
||
|
||
with open(metadata_file, "w", encoding="utf-8") as f:
|
||
json.dump(metadata, f, ensure_ascii=False, indent=2)
|
||
|
||
return metadata
|
||
|
||
def _ensure_skill_package_layout(self, skill_path: Path, skill_key: str):
|
||
"""确保技能目录满足运行最小结构。"""
|
||
|
||
skill_path.mkdir(parents=True, exist_ok=True)
|
||
|
||
init_file = skill_path / "__init__.py"
|
||
if not init_file.exists():
|
||
init_file.write_text("", encoding="utf-8")
|
||
|
||
main_file = skill_path / "main.py"
|
||
if not main_file.exists():
|
||
template = f'''"""{skill_key} skill"""
|
||
from src.ai.skills.base import Skill
|
||
|
||
|
||
class {"".join(p.capitalize() for p in skill_key.split("_"))}Skill(Skill):
|
||
async def initialize(self):
|
||
self.register_tool("ping", self.ping)
|
||
|
||
async def ping(self, text: str = "ok") -> str:
|
||
return text
|
||
|
||
async def cleanup(self):
|
||
pass
|
||
'''
|
||
main_file.write_text(template, encoding="utf-8")
|
||
|
||
self._read_metadata(skill_path, skill_key)
|
||
|
||
async def load_skill(self, skill_name: str) -> bool:
|
||
"""加载技能。"""
|
||
|
||
try:
|
||
skill_name = self.normalize_skill_key(skill_name)
|
||
|
||
if skill_name in self.skills:
|
||
logger.info(f"✅ 技能已加载: {skill_name}")
|
||
return True
|
||
|
||
skill_path = self._get_skill_path(skill_name)
|
||
if not skill_path.exists():
|
||
logger.error(f"❌ 技能不存在: {skill_name}")
|
||
return False
|
||
|
||
metadata_file = skill_path / "skill.json"
|
||
if not metadata_file.exists():
|
||
logger.error(f"❌ 技能元数据不存在: {skill_name}")
|
||
return False
|
||
|
||
with open(metadata_file, "r", encoding="utf-8") as f:
|
||
metadata_dict = json.load(f)
|
||
|
||
metadata = SkillMetadata(**metadata_dict)
|
||
|
||
if not metadata.enabled:
|
||
logger.info(f"⏸️ 技能已禁用: {skill_name}")
|
||
return False
|
||
|
||
module_path = f"skills.{skill_name}.main"
|
||
importlib.invalidate_caches()
|
||
|
||
try:
|
||
old_dont_write = sys.dont_write_bytecode
|
||
sys.dont_write_bytecode = True
|
||
try:
|
||
if module_path in sys.modules:
|
||
module = importlib.reload(sys.modules[module_path])
|
||
else:
|
||
module = importlib.import_module(module_path)
|
||
finally:
|
||
sys.dont_write_bytecode = old_dont_write
|
||
except Exception as exc:
|
||
logger.error(f"❌ 无法导入技能模块 {module_path}: {exc}")
|
||
return False
|
||
|
||
skill_class = None
|
||
for _, obj in inspect.getmembers(module):
|
||
if inspect.isclass(obj) and issubclass(obj, Skill) and obj != Skill:
|
||
skill_class = obj
|
||
break
|
||
|
||
if not skill_class:
|
||
logger.error(f"❌ 技能中未找到 Skill 子类: {skill_name}")
|
||
return False
|
||
|
||
skill = skill_class()
|
||
skill.metadata = metadata
|
||
skill.manager = self
|
||
|
||
await skill.initialize()
|
||
self.skills[skill_name] = skill
|
||
|
||
logger.info(f"✅ 加载技能: {skill_name} v{metadata.version}")
|
||
return True
|
||
|
||
except Exception as exc:
|
||
logger.error(f"❌ 加载技能失败 {skill_name}: {exc}")
|
||
return False
|
||
|
||
async def load_all_skills(self):
|
||
"""加载所有可用技能。"""
|
||
|
||
for skill_name in self.list_available_skills():
|
||
await self.load_skill(skill_name)
|
||
|
||
async def unload_skill(self, skill_name: str) -> bool:
|
||
"""仅卸载内存中的技能。"""
|
||
|
||
skill_name = self.normalize_skill_key(skill_name)
|
||
if skill_name not in self.skills:
|
||
return False
|
||
|
||
skill = self.skills[skill_name]
|
||
await skill.cleanup()
|
||
del self.skills[skill_name]
|
||
|
||
sys.modules.pop(f"skills.{skill_name}.main", None)
|
||
sys.modules.pop(f"skills.{skill_name}", None)
|
||
importlib.invalidate_caches()
|
||
|
||
logger.info(f"✅ 卸载技能: {skill_name}")
|
||
return True
|
||
|
||
async def uninstall_skill(self, skill_name: str, delete_files: bool = True) -> bool:
|
||
"""卸载技能并可选删除文件。"""
|
||
|
||
skill_name = self.normalize_skill_key(skill_name)
|
||
|
||
if skill_name in self.skills:
|
||
await self.unload_skill(skill_name)
|
||
|
||
if not delete_files:
|
||
return True
|
||
|
||
skill_path = self._get_skill_path(skill_name)
|
||
if not skill_path.exists():
|
||
return False
|
||
|
||
removed = False
|
||
for _ in range(3):
|
||
try:
|
||
shutil.rmtree(skill_path, ignore_errors=False, onerror=self._on_rmtree_error)
|
||
except PermissionError:
|
||
pass
|
||
|
||
if not skill_path.exists():
|
||
removed = True
|
||
break
|
||
|
||
time.sleep(0.2)
|
||
|
||
if not removed:
|
||
try:
|
||
metadata_file = skill_path / "skill.json"
|
||
metadata = {}
|
||
if metadata_file.exists():
|
||
with open(metadata_file, "r", encoding="utf-8") as f:
|
||
metadata = json.load(f)
|
||
metadata["enabled"] = False
|
||
with open(metadata_file, "w", encoding="utf-8") as f:
|
||
json.dump(metadata, f, ensure_ascii=False, indent=2)
|
||
logger.warning(f"⚠️ 删除目录失败,已软卸载技能: {skill_name}")
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
importlib.invalidate_caches()
|
||
logger.info(f"✅ 删除技能目录: {skill_name}")
|
||
return True
|
||
|
||
def get_skill(self, skill_name: str) -> Optional[Skill]:
|
||
"""获取已加载技能实例。"""
|
||
|
||
skill_name = self.normalize_skill_key(skill_name)
|
||
return self.skills.get(skill_name)
|
||
|
||
def list_skills(self) -> List[str]:
|
||
"""列出已加载技能。"""
|
||
|
||
return sorted(self.skills.keys())
|
||
|
||
def list_available_skills(self) -> List[str]:
|
||
"""列出可加载技能目录。"""
|
||
|
||
if not self.skills_dir.exists():
|
||
return []
|
||
|
||
available: List[str] = []
|
||
for skill_dir in self.skills_dir.iterdir():
|
||
if not skill_dir.is_dir() or skill_dir.name.startswith("_"):
|
||
continue
|
||
|
||
if (skill_dir / "skill.json").exists() and (skill_dir / "main.py").exists():
|
||
try:
|
||
with open(skill_dir / "skill.json", "r", encoding="utf-8") as f:
|
||
metadata = json.load(f)
|
||
if not metadata.get("enabled", True):
|
||
continue
|
||
available.append(self.normalize_skill_key(skill_dir.name))
|
||
except ValueError:
|
||
continue
|
||
except Exception:
|
||
continue
|
||
|
||
return sorted(set(available))
|
||
|
||
def get_all_tools(self) -> Dict[str, Callable]:
|
||
"""获取全部技能工具。"""
|
||
|
||
all_tools: Dict[str, Callable] = {}
|
||
for skill_name, skill in self.skills.items():
|
||
for tool_name, tool_func in skill.get_tools().items():
|
||
all_tools[f"{skill_name}.{tool_name}"] = tool_func
|
||
return all_tools
|
||
|
||
async def reload_skill(self, skill_name: str) -> bool:
|
||
"""重载技能。"""
|
||
|
||
skill_name = self.normalize_skill_key(skill_name)
|
||
if skill_name in self.skills:
|
||
await self.unload_skill(skill_name)
|
||
return await self.load_skill(skill_name)
|
||
|
||
def _resolve_network_url(self, source: str) -> str:
|
||
"""支持 URL 与 GitHub 简写。"""
|
||
|
||
source = source.strip()
|
||
if source.startswith(("http://", "https://")):
|
||
return source
|
||
|
||
if self._GITHUB_SHORTCUT_PATTERN.match(source):
|
||
repo, _, branch = source.partition("#")
|
||
branch = branch or "main"
|
||
return f"https://codeload.github.com/{repo}/zip/refs/heads/{branch}"
|
||
|
||
raise ValueError("source 必须是 URL 或 owner/repo[#branch]")
|
||
|
||
def _download_zip(self, url: str, output_zip: Path):
|
||
"""下载 zip 包到本地。"""
|
||
|
||
req = urllib.request.Request(url, headers={"User-Agent": "QQBot-Skills/1.0"})
|
||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||
data = resp.read()
|
||
output_zip.write_bytes(data)
|
||
|
||
def _find_skill_candidates(self, root_dir: Path) -> List[Tuple[str, Path]]:
|
||
"""在目录中扫描技能候选项。"""
|
||
|
||
candidates: List[Tuple[str, Path]] = []
|
||
for metadata_file in root_dir.rglob("skill.json"):
|
||
candidate_dir = metadata_file.parent
|
||
if not (candidate_dir / "main.py").exists():
|
||
continue
|
||
|
||
try:
|
||
with open(metadata_file, "r", encoding="utf-8") as f:
|
||
metadata = json.load(f)
|
||
raw_name = str(metadata.get("name") or candidate_dir.name)
|
||
except Exception:
|
||
raw_name = candidate_dir.name
|
||
|
||
try:
|
||
skill_key = self.normalize_skill_key(raw_name)
|
||
except ValueError:
|
||
continue
|
||
|
||
candidates.append((skill_key, candidate_dir))
|
||
|
||
uniq: Dict[str, Path] = {}
|
||
for key, path in candidates:
|
||
uniq[key] = path
|
||
|
||
return sorted(uniq.items(), key=lambda x: x[0])
|
||
|
||
def install_skill_from_source(
|
||
self,
|
||
source: str,
|
||
skill_name: Optional[str] = None,
|
||
overwrite: bool = False,
|
||
) -> Tuple[bool, str]:
|
||
"""从网络或本地源安装技能目录(仅落盘,不自动加载)。"""
|
||
|
||
desired_key = self.normalize_skill_key(skill_name) if skill_name else None
|
||
|
||
with tempfile.TemporaryDirectory(prefix="qqbot_skill_") as tmp:
|
||
tmp_dir = Path(tmp)
|
||
extract_root: Optional[Path] = None
|
||
|
||
source_path = Path(source)
|
||
if source_path.exists():
|
||
if source_path.is_dir():
|
||
extract_root = source_path
|
||
elif source_path.is_file() and source_path.suffix.lower() == ".zip":
|
||
with zipfile.ZipFile(source_path, "r") as zf:
|
||
zf.extractall(tmp_dir / "extract")
|
||
extract_root = tmp_dir / "extract"
|
||
else:
|
||
return False, "本地 source 仅支持目录或 zip 文件"
|
||
else:
|
||
try:
|
||
url = self._resolve_network_url(source)
|
||
except ValueError as exc:
|
||
return False, str(exc)
|
||
|
||
download_zip = tmp_dir / "download.zip"
|
||
try:
|
||
self._download_zip(url, download_zip)
|
||
except Exception as exc:
|
||
# GitHub 简写默认 main 失败时尝试 master
|
||
if "codeload.github.com" in url and url.endswith("/main"):
|
||
fallback = url[:-4] + "master"
|
||
try:
|
||
self._download_zip(fallback, download_zip)
|
||
except Exception:
|
||
return False, f"下载技能失败: {exc}"
|
||
else:
|
||
return False, f"下载技能失败: {exc}"
|
||
|
||
try:
|
||
with zipfile.ZipFile(download_zip, "r") as zf:
|
||
zf.extractall(tmp_dir / "extract")
|
||
except Exception as exc:
|
||
return False, f"解压技能失败: {exc}"
|
||
|
||
extract_root = tmp_dir / "extract"
|
||
|
||
candidates = self._find_skill_candidates(extract_root)
|
||
if not candidates:
|
||
return False, "未找到可安装技能(需包含 skill.json 与 main.py)"
|
||
|
||
selected_key: Optional[str] = None
|
||
selected_path: Optional[Path] = None
|
||
|
||
if desired_key:
|
||
for key, path in candidates:
|
||
if key == desired_key:
|
||
selected_key, selected_path = key, path
|
||
break
|
||
|
||
if not selected_path:
|
||
names = ", ".join([k for k, _ in candidates])
|
||
return False, f"源中未找到技能 {desired_key},可选: {names}"
|
||
else:
|
||
if len(candidates) > 1:
|
||
names = ", ".join([k for k, _ in candidates])
|
||
return False, f"检测到多个技能,请指定 skill_name。可选: {names}"
|
||
selected_key, selected_path = candidates[0]
|
||
|
||
assert selected_key is not None and selected_path is not None
|
||
target_path = self._get_skill_path(selected_key)
|
||
|
||
if target_path.exists():
|
||
if not overwrite:
|
||
return False, f"技能已存在: {selected_key}"
|
||
shutil.rmtree(target_path)
|
||
|
||
shutil.copytree(
|
||
selected_path,
|
||
target_path,
|
||
ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".git", ".github"),
|
||
)
|
||
self._ensure_skill_package_layout(target_path, selected_key)
|
||
importlib.invalidate_caches()
|
||
|
||
logger.info(f"✅ 安装技能成功: {selected_key} <- {source}")
|
||
return True, selected_key
|
||
|
||
|
||
def create_skill_template(
|
||
skill_name: str,
|
||
output_dir: Path,
|
||
description: str = "技能描述",
|
||
author: str = "QQBot",
|
||
):
|
||
"""创建技能模板。"""
|
||
|
||
skill_key = SkillsManager.normalize_skill_key(skill_name)
|
||
skill_dir = output_dir / skill_key
|
||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
metadata = {
|
||
"name": skill_key,
|
||
"version": "1.0.0",
|
||
"description": description,
|
||
"author": author,
|
||
"dependencies": [],
|
||
"enabled": True,
|
||
}
|
||
|
||
with open(skill_dir / "skill.json", "w", encoding="utf-8") as f:
|
||
json.dump(metadata, f, ensure_ascii=False, indent=2)
|
||
|
||
class_name = "".join(word.capitalize() for word in skill_key.split("_")) + "Skill"
|
||
main_code = f'''"""{skill_key} skill"""
|
||
from src.ai.skills.base import Skill
|
||
|
||
|
||
class {class_name}(Skill):
|
||
async def initialize(self):
|
||
self.register_tool("example_tool", self.example_tool)
|
||
|
||
async def example_tool(self, text: str) -> str:
|
||
return f"{skill_key} 收到: {{text}}"
|
||
|
||
async def cleanup(self):
|
||
pass
|
||
'''
|
||
|
||
with open(skill_dir / "main.py", "w", encoding="utf-8") as f:
|
||
f.write(main_code)
|
||
|
||
with open(skill_dir / "__init__.py", "w", encoding="utf-8") as f:
|
||
f.write("")
|
||
|
||
readme = f"""# {skill_key}
|
||
|
||
## 描述
|
||
{description}
|
||
|
||
## 工具
|
||
- example_tool(text)
|
||
"""
|
||
|
||
with open(skill_dir / "README.md", "w", encoding="utf-8") as f:
|
||
f.write(readme)
|
||
|
||
logger.info(f"✅ 创建技能模板: {skill_dir}")
|