Please enter the commit message for your changes. Lines starting

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
This commit is contained in:
Mimikko-zeus
2026-03-03 01:23:23 +08:00
parent b7940f2ff6
commit ae208af6a9
453 changed files with 99883 additions and 0 deletions

628
tests/test_ai.py Normal file
View File

@@ -0,0 +1,628 @@
"""AI integration tests."""
import asyncio
import json
import os
from pathlib import Path
import shutil
import stat
import sys
import tempfile
import time
import zipfile
from dotenv import load_dotenv
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from src.ai import AIClient
from src.ai.base import ModelConfig, ModelProvider
from src.ai.memory import MemorySystem
from src.ai.skills import SkillsManager, create_skill_template
from src.handlers.message_handler_ai import MessageHandler
load_dotenv(project_root / ".env")
TEST_DATA_DIR = Path("data/ai_test")
def _safe_rmtree(path: Path):
if not path.exists():
return
def _onerror(func, target, exc_info):
try:
os.chmod(target, stat.S_IWRITE)
func(target)
except Exception:
pass
for _ in range(3):
try:
shutil.rmtree(path, onerror=_onerror)
return
except PermissionError:
time.sleep(0.2)
def _safe_unlink(path: Path):
if not path.exists():
return
for _ in range(3):
try:
path.unlink()
return
except PermissionError:
time.sleep(0.2)
def _read_env(name: str, default=None):
value = os.getenv(name)
if value is None:
return default
value = value.strip()
if not value or value.startswith("#"):
return default
return value
def get_ai_config() -> ModelConfig:
provider_map = {
"openai": ModelProvider.OPENAI,
"anthropic": ModelProvider.ANTHROPIC,
"deepseek": ModelProvider.DEEPSEEK,
"qwen": ModelProvider.QWEN,
"siliconflow": ModelProvider.OPENAI,
}
provider_str = (_read_env("AI_PROVIDER", "openai") or "openai").lower()
provider = provider_map.get(provider_str, ModelProvider.OPENAI)
return ModelConfig(
provider=provider,
model_name=_read_env("AI_MODEL", "gpt-3.5-turbo") or "gpt-3.5-turbo",
api_key=_read_env("AI_API_KEY", "") or "",
api_base=_read_env("AI_API_BASE"),
temperature=0.7,
)
def get_embed_config() -> ModelConfig:
provider_map = {
"openai": ModelProvider.OPENAI,
"anthropic": ModelProvider.ANTHROPIC,
"deepseek": ModelProvider.DEEPSEEK,
"qwen": ModelProvider.QWEN,
"siliconflow": ModelProvider.OPENAI,
}
provider_str = (_read_env("AI_EMBED_PROVIDER", "openai") or "openai").lower()
provider = provider_map.get(provider_str, ModelProvider.OPENAI)
api_key = _read_env("AI_EMBED_API_KEY") or _read_env("AI_API_KEY", "") or ""
api_base = _read_env("AI_EMBED_API_BASE") or _read_env("AI_API_BASE")
return ModelConfig(
provider=provider,
model_name=_read_env("AI_EMBED_MODEL", "text-embedding-3-small")
or "text-embedding-3-small",
api_key=api_key,
api_base=api_base,
temperature=0.0,
)
class FakeMessage:
def __init__(self, content: str):
from types import SimpleNamespace
self.content = content
self.author = SimpleNamespace(id="test_user")
self.replies = []
async def reply(self, content: str):
self.replies.append(content)
def make_handler() -> MessageHandler:
from types import SimpleNamespace
fake_bot = SimpleNamespace(robot=SimpleNamespace(id="test_bot"))
handler = MessageHandler(fake_bot)
handler.ai_client = AIClient(get_ai_config(), data_dir=TEST_DATA_DIR)
handler.skills_manager = SkillsManager(Path("skills"))
handler.model_profiles_path = TEST_DATA_DIR / "models_test.json"
TEST_DATA_DIR.mkdir(parents=True, exist_ok=True)
_safe_unlink(handler.model_profiles_path)
handler._ai_initialized = True
return handler
async def _test_basic_chat():
print("=== test_basic_chat ===")
config = get_ai_config()
if not config.api_key:
print("skip: AI_API_KEY not configured")
return
embed_config = get_embed_config()
client = AIClient(config, embed_config=embed_config, data_dir=TEST_DATA_DIR)
response = await client.chat(
user_id="test_user",
user_message="你好,请介绍一下你自己",
use_memory=False,
use_tools=False,
)
assert response
print("ok: chat response length", len(response))
async def _test_memory():
print("=== test_memory ===")
config = get_ai_config()
if not config.api_key:
print("skip: AI_API_KEY not configured")
return
client = AIClient(config, embed_config=get_embed_config(), data_dir=TEST_DATA_DIR)
await client.chat(user_id="test_user", user_message="鎴戝彨寮犱笁", use_memory=True)
await client.chat(user_id="test_user", user_message="what is my name", use_memory=True)
short_term, long_term = await client.memory.get_context("test_user")
assert len(short_term) >= 2
# 重要性改为模型评估后,是否入长期记忆取决于模型打分,不再固定断言数量。
assert isinstance(long_term, list)
print("ok: memory short/long", len(short_term), len(long_term))
async def _test_personality():
print("=== test_personality ===")
client = AIClient(get_ai_config(), data_dir=TEST_DATA_DIR)
names = client.list_personalities()
assert names
assert client.set_personality(names[0])
key = "roleplay_test"
added = client.personality.add_personality(
key,
client.personality.get_personality("default"),
)
assert added
assert key in client.list_personalities()
assert client.personality.remove_personality(key)
assert key not in client.list_personalities()
print("ok: personality add/remove")
async def _test_skills():
print("=== test_skills ===")
manager = SkillsManager(Path("skills"))
assert await manager.load_skill("weather")
tools = manager.get_all_tools()
assert "weather.get_weather" in tools
weather = await tools["weather.get_weather"](city="鍖椾含")
assert weather
assert await manager.load_skill("skills_creator")
tools = manager.get_all_tools()
assert "skills_creator.create_skill" in tools
await manager.unload_skill("weather")
await manager.unload_skill("skills_creator")
print("ok: skills load/unload")
async def _test_skill_commands():
print("=== test_skill_commands ===")
handler = make_handler()
skill_key = f"cmd_zip_skill_{int(time.time() * 1000)}"
# Prepare a zip package source for install testing
tmp_root = TEST_DATA_DIR / "tmp_skill_pkg"
if tmp_root.exists():
_safe_rmtree(tmp_root)
tmp_root.mkdir(parents=True, exist_ok=True)
create_skill_template(skill_key, tmp_root, description="zip skill", author="test")
zip_path = TEST_DATA_DIR / f"{skill_key}.zip"
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for file in (tmp_root / skill_key).rglob("*"):
if file.is_file():
zf.write(file, file.relative_to(tmp_root))
install_msg = FakeMessage(f"/skills install {zip_path}")
await handler._handle_command(install_msg, install_msg.content)
assert install_msg.replies, "install command no reply"
list_msg = FakeMessage("/skills")
await handler._handle_command(list_msg, list_msg.content)
assert list_msg.replies, "list command no reply"
reload_msg = FakeMessage(f"/skills reload {skill_key}")
await handler._handle_command(reload_msg, reload_msg.content)
assert reload_msg.replies, "reload command no reply"
uninstall_msg = FakeMessage(f"/skills uninstall {skill_key}")
await handler._handle_command(uninstall_msg, uninstall_msg.content)
assert uninstall_msg.replies, "uninstall command no reply"
if tmp_root.exists():
_safe_rmtree(tmp_root)
_safe_unlink(zip_path)
print("ok: skills install/reload/uninstall command")
async def _test_personality_commands():
print("=== test_personality_commands ===")
handler = make_handler()
intro = "You are a hot-blooded anime hero. Speak directly and stay in-character."
add_cmd = (
"/personality add roleplay_hero "
f"{intro}"
)
add_msg = FakeMessage(add_cmd)
await handler._handle_command(add_msg, add_msg.content)
assert add_msg.replies
set_msg = FakeMessage("/personality set roleplay_hero")
await handler._handle_command(set_msg, set_msg.content)
assert set_msg.replies
assert intro in handler.ai_client.personality.get_system_prompt()
remove_msg = FakeMessage("/personality remove roleplay_hero")
await handler._handle_command(remove_msg, remove_msg.content)
assert remove_msg.replies
assert "roleplay_hero" not in handler.ai_client.list_personalities()
print("ok: personality add/set/remove command")
async def _test_model_commands():
print("=== test_model_commands ===")
handler = make_handler()
list_msg = FakeMessage("/models")
await handler._handle_command(list_msg, list_msg.content)
assert list_msg.replies
assert "default" in list_msg.replies[-1].lower()
add_msg = FakeMessage("/models add roleplay_llm openai gpt-4o-mini")
await handler._handle_command(add_msg, add_msg.content)
assert add_msg.replies
assert handler.active_model_key == "roleplay_llm"
assert "roleplay_llm" in handler.model_profiles
switch_msg = FakeMessage("/models switch default")
await handler._handle_command(switch_msg, switch_msg.content)
assert switch_msg.replies
assert handler.active_model_key == "default"
current_msg = FakeMessage("/models current")
await handler._handle_command(current_msg, current_msg.content)
assert current_msg.replies
old_config = handler.ai_client.config
shortcut_model = "Qwen/Qwen2.5-7B-Instruct"
shortcut_key = handler._normalize_model_key(shortcut_model)
shortcut_add_msg = FakeMessage(f"/models add {shortcut_model}")
await handler._handle_command(shortcut_add_msg, shortcut_add_msg.content)
assert shortcut_add_msg.replies
assert handler.active_model_key == shortcut_key
assert handler.ai_client.config.model_name == shortcut_model
assert handler.ai_client.config.provider == old_config.provider
assert handler.ai_client.config.api_base == old_config.api_base
assert handler.ai_client.config.api_key == old_config.api_key
shortcut_remove_msg = FakeMessage(f"/models remove {shortcut_key}")
await handler._handle_command(shortcut_remove_msg, shortcut_remove_msg.content)
assert shortcut_remove_msg.replies
assert shortcut_key not in handler.model_profiles
remove_msg = FakeMessage("/models remove roleplay_llm")
await handler._handle_command(remove_msg, remove_msg.content)
assert remove_msg.replies
assert "roleplay_llm" not in handler.model_profiles
_safe_unlink(handler.model_profiles_path)
print("ok: model add/switch/remove command")
async def _test_memory_commands():
print("=== test_memory_commands ===")
handler = make_handler()
user_id = "test_user"
await handler.ai_client.clear_all_memory(user_id)
add_msg = FakeMessage("/memory add this is a long-term memory test")
await handler._handle_command(add_msg, add_msg.content)
assert add_msg.replies
assert "已新增长期记忆" in add_msg.replies[-1]
memory_id = add_msg.replies[-1].split(": ", 1)[1].split(" ", 1)[0]
assert memory_id
list_msg = FakeMessage("/memory list 5")
await handler._handle_command(list_msg, list_msg.content)
assert list_msg.replies
assert memory_id in list_msg.replies[-1]
get_msg = FakeMessage(f"/memory get {memory_id}")
await handler._handle_command(get_msg, get_msg.content)
assert get_msg.replies
assert memory_id in get_msg.replies[-1]
search_msg = FakeMessage("/memory search 长期记忆")
await handler._handle_command(search_msg, search_msg.content)
assert search_msg.replies
assert memory_id in search_msg.replies[-1]
update_msg = FakeMessage(f"/memory update {memory_id} 这是更新后的长期记忆")
await handler._handle_command(update_msg, update_msg.content)
assert update_msg.replies
assert "已更新长期记忆" in update_msg.replies[-1]
# Build short-term memory then clear only short-term.
await handler.ai_client.memory.add_message(
user_id=user_id,
role="user",
content="short memory for clear short test",
)
assert handler.ai_client.memory.short_term.get(user_id)
clear_short_msg = FakeMessage("/clear short")
await handler._handle_command(clear_short_msg, clear_short_msg.content)
assert clear_short_msg.replies
assert not handler.ai_client.memory.short_term.get(user_id)
# Long-term memory should still exist after clearing short-term only.
still_exists = await handler.ai_client.get_long_term_memory(user_id, memory_id)
assert still_exists is not None
delete_msg = FakeMessage(f"/memory delete {memory_id}")
await handler._handle_command(delete_msg, delete_msg.content)
assert delete_msg.replies
assert "已删除长期记忆" in delete_msg.replies[-1]
removed = await handler.ai_client.get_long_term_memory(user_id, memory_id)
assert removed is None
print("ok: memory command CRUD + clear short")
async def _test_plain_text_output():
print("=== test_plain_text_output ===")
handler = make_handler()
md_text = "# 标题\n**加粗** 和 `代码`\n- 列表\n[链接](https://example.com)"
plain = handler._plain_text(md_text)
assert "#" not in plain
assert "**" not in plain
assert "`" not in plain
assert "[" not in plain
assert "](" not in plain
print("ok: markdown stripped")
async def _test_skills_creator_autoload():
print("=== test_skills_creator_autoload ===")
from types import SimpleNamespace
fake_bot = SimpleNamespace(robot=SimpleNamespace(id="test_bot"))
handler = MessageHandler(fake_bot)
handler.model_profiles_path = TEST_DATA_DIR / "models_autoload_test.json"
_safe_unlink(handler.model_profiles_path)
await handler._init_ai()
assert handler.skills_manager is not None
assert "skills_creator" in handler.skills_manager.list_skills()
tool_names = [tool.name for tool in handler.ai_client.tools.list()]
assert "skills_creator.create_skill" in tool_names
print("ok: skills_creator autoloaded")
async def _test_mcp():
print("=== test_mcp ===")
from src.ai.mcp import MCPManager
from src.ai.mcp.servers import FileSystemMCPServer
manager = MCPManager(Path("config/mcp.json"))
fs_server = FileSystemMCPServer(root_path=Path("data"))
await manager.register_server(fs_server)
tools = await manager.get_all_tools_for_ai()
assert len(tools) >= 1
print("ok: mcp tools", len(tools))
async def _test_long_task():
print("=== test_long_task ===")
client = AIClient(get_ai_config(), data_dir=TEST_DATA_DIR)
async def step1():
await asyncio.sleep(0.1)
return "step1"
async def step2():
await asyncio.sleep(0.1)
return "step2"
client.task_manager.register_action("step1", step1)
client.task_manager.register_action("step2", step2)
task_id = await client.create_long_task(
user_id="test_user",
title="test",
description="test task",
steps=[
{"description": "s1", "action": "step1", "params": {}},
{"description": "s2", "action": "step2", "params": {}},
],
)
await client.start_task(task_id)
await asyncio.sleep(0.5)
status = client.get_task_status(task_id)
assert status is not None
assert status["status"] in {"completed", "running"}
print("ok: long task", status["status"])
async def _test_memory_importance_evaluator():
print("=== test_memory_importance_evaluator ===")
called = {"value": False}
async def fake_importance_eval(content, metadata):
called["value"] = True
assert "用户:" in content
assert "助手:" in content
return 0.91
store_path = TEST_DATA_DIR / "importance_test.json"
_safe_unlink(store_path)
memory = MemorySystem(
storage_path=store_path,
importance_evaluator=fake_importance_eval,
use_vector_db=False,
)
stored = await memory.add_qa_pair(
user_id="u1",
question="请记住我的昵称是小明",
answer="好的,我记住了你的昵称是小明",
metadata={"source": "test"},
)
assert called["value"]
assert stored is not None
assert "用户:" in stored.content
assert "助手:" in stored.content
assert "小明" in stored.content
long_term = await memory.list_long_term("u1")
assert len(long_term) == 1
# add_message 仅写入短期记忆,不触发长期记忆评分写入。
await memory.add_message(user_id="u1", role="user", content="单条短期消息")
long_term_after_single = await memory.list_long_term("u1")
assert len(long_term_after_single) == 1
memory_without_eval = MemorySystem(
storage_path=TEST_DATA_DIR / "importance_fallback_test.json",
use_vector_db=False,
)
fallback_score = await memory_without_eval._evaluate_importance("任意内容", None)
assert fallback_score == 0.5
await memory.close()
await memory_without_eval.close()
_safe_unlink(store_path)
_safe_unlink(TEST_DATA_DIR / "importance_fallback_test.json")
print("ok: memory importance evaluator")
def test_basic_chat():
asyncio.run(_test_basic_chat())
def test_memory():
asyncio.run(_test_memory())
def test_personality():
asyncio.run(_test_personality())
def test_skills():
asyncio.run(_test_skills())
def test_skill_commands():
asyncio.run(_test_skill_commands())
def test_personality_commands():
asyncio.run(_test_personality_commands())
def test_model_commands():
asyncio.run(_test_model_commands())
def test_memory_commands():
asyncio.run(_test_memory_commands())
def test_plain_text_output():
asyncio.run(_test_plain_text_output())
def test_skills_creator_autoload():
asyncio.run(_test_skills_creator_autoload())
def test_mcp():
asyncio.run(_test_mcp())
def test_long_task():
asyncio.run(_test_long_task())
def test_memory_importance_evaluator():
asyncio.run(_test_memory_importance_evaluator())
async def main():
print("寮€濮?AI 鍔熻兘娴嬭瘯")
await _test_personality()
await _test_skills()
await _test_skill_commands()
await _test_personality_commands()
await _test_model_commands()
await _test_memory_commands()
await _test_plain_text_output()
await _test_skills_creator_autoload()
await _test_mcp()
await _test_long_task()
await _test_memory_importance_evaluator()
config = get_ai_config()
if config.api_key:
await _test_basic_chat()
await _test_memory()
else:
print("跳过需要 API Key 的对话/记忆测试")
print("娴嬭瘯瀹屾垚")
if __name__ == "__main__":
asyncio.run(main())