@huicai/cli 2026.804.0 → 2026.1019.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +391 -32
- package/README.md +176 -10
- package/bin/huicai.js +166 -73
- package/package.json +27 -8
- package/skills/ab-testing/trigger-cases.json +30 -0
- package/skills/academic-writing/trigger-cases.json +30 -0
- package/skills/accounting-ops/trigger-cases.json +30 -0
- package/skills/ai-agent/trigger-cases.json +30 -0
- package/skills/ai-aiops/trigger-cases.json +30 -0
- package/skills/ai-coding/trigger-cases.json +30 -0
- package/skills/ai-compliance/trigger-cases.json +30 -0
- package/skills/ai-eval/trigger-cases.json +30 -0
- package/skills/ai-llm-app/trigger-cases.json +30 -0
- package/skills/ai-mlops/trigger-cases.json +30 -0
- package/skills/ai-multimodal/trigger-cases.json +30 -0
- package/skills/ai-prompt/trigger-cases.json +30 -0
- package/skills/ai-rag/trigger-cases.json +30 -0
- package/skills/anomaly-detection/trigger-cases.json +30 -0
- package/skills/api-design/trigger-cases.json +31 -0
- package/skills/api-integration/trigger-cases.json +31 -0
- package/skills/arch-design/trigger-cases.json +12 -0
- package/skills/arch-diagram/SKILL.md +118 -0
- package/skills/arch-diagram/trigger-cases.json +30 -0
- package/skills/arch-distributed/trigger-cases.json +30 -0
- package/skills/arch-multitenant/trigger-cases.json +30 -0
- package/skills/audio-brief/SKILL.md +65 -0
- package/skills/audio-brief/trigger-cases.json +30 -0
- package/skills/audit-assist/trigger-cases.json +30 -0
- package/skills/audit-chain/trigger-cases.json +31 -0
- package/skills/banking-credit/trigger-cases.json +30 -0
- package/skills/brand-design/trigger-cases.json +30 -0
- package/skills/business-plan/trigger-cases.json +30 -0
- package/skills/capital-ops/trigger-cases.json +30 -0
- package/skills/cicd-pipeline/trigger-cases.json +31 -0
- package/skills/code-archaeology/SKILL.md +55 -0
- package/skills/code-archaeology/trigger-cases.json +9 -0
- package/skills/code-quality/SKILL.md +138 -0
- package/skills/code-quality/references/languages.md +107 -0
- package/skills/code-quality/scripts/__pycache__/code_battery.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_check.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_check.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_fix.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_fix.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_java.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_java.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_lens.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_lens.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_python.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_python.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_testplan.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/code_battery.py +1072 -0
- package/skills/code-quality/scripts/code_check.py +1269 -0
- package/skills/code-quality/scripts/code_csharp.py +371 -0
- package/skills/code-quality/scripts/code_fix.py +201 -0
- package/skills/code-quality/scripts/code_java.py +695 -0
- package/skills/code-quality/scripts/code_jsts.py +313 -0
- package/skills/code-quality/scripts/code_lens.py +1079 -0
- package/skills/code-quality/scripts/code_python.py +536 -0
- package/skills/code-quality/scripts/code_testplan.py +239 -0
- package/skills/code-quality/trigger-cases.json +45 -0
- package/skills/code-review/SKILL.md +24 -0
- package/skills/code-review/trigger-cases.json +35 -0
- package/skills/commit-and-pr/trigger-cases.json +31 -0
- package/skills/content-creation/trigger-cases.json +30 -0
- package/skills/corporate-governance/trigger-cases.json +30 -0
- package/skills/crawl-chain/trigger-cases.json +31 -0
- package/skills/customer-service/trigger-cases.json +30 -0
- package/skills/data-analysis/trigger-cases.json +30 -0
- package/skills/data-chain/SKILL.md +1 -1
- package/skills/data-chain/trigger-cases.json +31 -0
- package/skills/data-governance/trigger-cases.json +30 -0
- package/skills/data-report-pro/trigger-cases.json +30 -0
- package/skills/data-streaming/trigger-cases.json +30 -0
- package/skills/data-wrangling/SKILL.md +1 -1
- package/skills/data-wrangling/trigger-cases.json +30 -0
- package/skills/db-es/trigger-cases.json +30 -0
- package/skills/db-migration/trigger-cases.json +30 -0
- package/skills/db-mysql/trigger-cases.json +30 -0
- package/skills/db-postgres/trigger-cases.json +30 -0
- package/skills/db-redis/trigger-cases.json +30 -0
- package/skills/debug-root-cause/trigger-cases.json +30 -0
- package/skills/dependency-audit/SKILL.md +56 -0
- package/skills/dependency-audit/trigger-cases.json +7 -0
- package/skills/dependency-upgrade/trigger-cases.json +31 -0
- package/skills/dev-cpp/trigger-cases.json +30 -0
- package/skills/dev-cross-platform/trigger-cases.json +30 -0
- package/skills/dev-csharp/SKILL.md +23 -0
- package/skills/dev-csharp/trigger-cases.json +30 -0
- package/skills/dev-frontend/SKILL.md +19 -0
- package/skills/dev-frontend/trigger-cases.json +30 -0
- package/skills/dev-go/trigger-cases.json +30 -0
- package/skills/dev-harmony/trigger-cases.json +30 -0
- package/skills/dev-java/SKILL.md +20 -0
- package/skills/dev-java/trigger-cases.json +30 -0
- package/skills/dev-mobile/trigger-cases.json +30 -0
- package/skills/dev-node/trigger-cases.json +30 -0
- package/skills/dev-php/trigger-cases.json +30 -0
- package/skills/dev-python/SKILL.md +21 -0
- package/skills/dev-python/trigger-cases.json +30 -0
- package/skills/dev-rust/trigger-cases.json +30 -0
- package/skills/dev-toolchain/trigger-cases.json +31 -0
- package/skills/doc-chain/trigger-cases.json +31 -0
- package/skills/doc-compare/trigger-cases.json +30 -0
- package/skills/docx-author/SKILL.md +127 -25
- package/skills/docx-author/scripts/__pycache__/docx_battery.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_build.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_check.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_layout.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/zh_prose.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/docx_battery.py +586 -0
- package/skills/docx-author/scripts/docx_build.py +995 -0
- package/skills/docx-author/scripts/docx_check.py +1306 -0
- package/skills/docx-author/scripts/docx_doc.py +14 -154
- package/skills/docx-author/scripts/docx_layout.py +490 -0
- package/skills/docx-author/scripts/zh_prose.py +639 -0
- package/skills/docx-author/trigger-cases.json +58 -0
- package/skills/echo-brief/SKILL.md +84 -0
- package/skills/echo-brief/references/echo-format.md +68 -0
- package/skills/echo-brief/trigger-cases.json +40 -0
- package/skills/ecommerce-ops/trigger-cases.json +30 -0
- package/skills/education-training/trigger-cases.json +30 -0
- package/skills/ehs-safety/trigger-cases.json +30 -0
- package/skills/email-writing/trigger-cases.json +30 -0
- package/skills/eng-ai/trigger-cases.json +30 -0
- package/skills/eng-architecture/trigger-cases.json +31 -0
- package/skills/eng-automation/trigger-cases.json +31 -0
- package/skills/eng-collaboration/trigger-cases.json +30 -0
- package/skills/eng-complex-env/trigger-cases.json +30 -0
- package/skills/eng-data-platform/trigger-cases.json +31 -0
- package/skills/eng-database/trigger-cases.json +31 -0
- package/skills/eng-develop/trigger-cases.json +30 -0
- package/skills/eng-docs/SKILL.md +24 -0
- package/skills/eng-docs/trigger-cases.json +30 -0
- package/skills/eng-efficiency/trigger-cases.json +30 -0
- package/skills/eng-infra/trigger-cases.json +30 -0
- package/skills/eng-integration/trigger-cases.json +31 -0
- package/skills/eng-legacy/trigger-cases.json +31 -0
- package/skills/eng-observability/trigger-cases.json +31 -0
- package/skills/eng-ops/trigger-cases.json +31 -0
- package/skills/eng-performance/trigger-cases.json +31 -0
- package/skills/eng-planning/trigger-cases.json +31 -0
- package/skills/eng-quality/SKILL.md +8 -0
- package/skills/eng-quality/trigger-cases.json +31 -0
- package/skills/eng-release/trigger-cases.json +31 -0
- package/skills/eng-reliability/trigger-cases.json +31 -0
- package/skills/eng-requirements/trigger-cases.json +12 -0
- package/skills/eng-research/trigger-cases.json +30 -0
- package/skills/eng-security/trigger-cases.json +31 -0
- package/skills/eng-ux/trigger-cases.json +30 -0
- package/skills/engineering-chain/trigger-cases.json +30 -0
- package/skills/env-offline/trigger-cases.json +31 -0
- package/skills/env-private-deploy/trigger-cases.json +31 -0
- package/skills/env-xinchuang/trigger-cases.json +31 -0
- package/skills/equipment-tpm/trigger-cases.json +30 -0
- package/skills/esg-report/trigger-cases.json +30 -0
- package/skills/event-management/trigger-cases.json +30 -0
- package/skills/excel-analysis-chain/SKILL.md +17 -4
- package/skills/excel-analysis-chain/trigger-cases.json +31 -0
- package/skills/explain-codebase/trigger-cases.json +11 -0
- package/skills/file-ingest/trigger-cases.json +30 -0
- package/skills/finance-analysis/trigger-cases.json +31 -0
- package/skills/finance-chain/trigger-cases.json +31 -0
- package/skills/financial-modeling/trigger-cases.json +31 -0
- package/skills/find-simplifications/SKILL.md +43 -0
- package/skills/find-simplifications/trigger-cases.json +23 -0
- package/skills/foreign-trade/trigger-cases.json +30 -0
- package/skills/funnel-cohort/trigger-cases.json +30 -0
- package/skills/gov-document/trigger-cases.json +30 -0
- package/skills/gui-change-evidence/SKILL.md +47 -0
- package/skills/gui-change-evidence/trigger-cases.json +23 -0
- package/skills/hr-chain/trigger-cases.json +31 -0
- package/skills/hr-operations/trigger-cases.json +30 -0
- package/skills/info-disclosure/trigger-cases.json +31 -0
- package/skills/infra-gateway/trigger-cases.json +31 -0
- package/skills/infra-k8s/trigger-cases.json +31 -0
- package/skills/infra-mq/trigger-cases.json +30 -0
- package/skills/insurance-advisory/trigger-cases.json +30 -0
- package/skills/internal-audit/trigger-cases.json +30 -0
- package/skills/ip-management/trigger-cases.json +31 -0
- package/skills/ipo-listing/trigger-cases.json +31 -0
- package/skills/it-audit/trigger-cases.json +31 -0
- package/skills/itsm-ops/trigger-cases.json +31 -0
- package/skills/knowledge-sop/trigger-cases.json +31 -0
- package/skills/legal-chain/trigger-cases.json +31 -0
- package/skills/legal-contract/trigger-cases.json +30 -0
- package/skills/litigation-support/trigger-cases.json +31 -0
- package/skills/logistics-warehouse/trigger-cases.json +30 -0
- package/skills/long-doc-analysis/trigger-cases.json +31 -0
- package/skills/management-accounting/trigger-cases.json +30 -0
- package/skills/manufacturing-chain/trigger-cases.json +31 -0
- package/skills/manufacturing-quality/trigger-cases.json +30 -0
- package/skills/marketing-analytics/trigger-cases.json +31 -0
- package/skills/marketing-chain/trigger-cases.json +31 -0
- package/skills/meeting-minutes/trigger-cases.json +30 -0
- package/skills/metrics-system/trigger-cases.json +31 -0
- package/skills/negotiation-prep/trigger-cases.json +30 -0
- package/skills/obs-prometheus/trigger-cases.json +31 -0
- package/skills/office-automation/trigger-cases.json +31 -0
- package/skills/okr-review/trigger-cases.json +30 -0
- package/skills/ops-fleet/trigger-cases.json +31 -0
- package/skills/ops-linux/trigger-cases.json +31 -0
- package/skills/optimization-solver/trigger-cases.json +30 -0
- package/skills/patent-disclosure/SKILL.md +105 -0
- package/skills/patent-disclosure/references/figures.md +110 -0
- package/skills/patent-disclosure/references/outline.md +162 -0
- package/skills/patent-disclosure/scripts/patent_docx.py +230 -0
- package/skills/patent-disclosure/trigger-cases.json +31 -0
- package/skills/patent-drafting/SKILL.md +108 -0
- package/skills/patent-drafting/trigger-cases.json +31 -0
- package/skills/patent-portfolio/SKILL.md +81 -0
- package/skills/patent-portfolio/trigger-cases.json +31 -0
- package/skills/pdf-toolkit/trigger-cases.json +31 -0
- package/skills/perf-optimize/trigger-cases.json +31 -0
- package/skills/pivot-crosstab/trigger-cases.json +30 -0
- package/skills/policy-analysis/trigger-cases.json +31 -0
- package/skills/portfolio-analysis/trigger-cases.json +31 -0
- package/skills/ppt-chain/SKILL.md +14 -6
- package/skills/ppt-chain/trigger-cases.json +31 -0
- package/skills/pptx-author/SKILL.md +86 -33
- package/skills/pptx-author/scripts/__pycache__/pptx_check.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/__pycache__/pptx_deck.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/__pycache__/pptx_layout.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/pptx_battery.py +178 -0
- package/skills/pptx-author/scripts/pptx_build.py +119 -0
- package/skills/pptx-author/scripts/pptx_check.py +1182 -0
- package/skills/pptx-author/scripts/pptx_deck.py +736 -145
- package/skills/pptx-author/scripts/pptx_layout.py +384 -0
- package/skills/pptx-author/trigger-cases.json +15 -0
- package/skills/pr-comms/trigger-cases.json +30 -0
- package/skills/procurement-supply/trigger-cases.json +31 -0
- package/skills/product-design/trigger-cases.json +31 -0
- package/skills/production-planning/trigger-cases.json +31 -0
- package/skills/project-management/trigger-cases.json +31 -0
- package/skills/project-proposal/trigger-cases.json +31 -0
- package/skills/prose-standard/SKILL.md +53 -0
- package/skills/prose-standard/trigger-cases.json +23 -0
- package/skills/qa-api-automation/trigger-cases.json +31 -0
- package/skills/qa-mobile-automation/trigger-cases.json +31 -0
- package/skills/qa-ui-automation/trigger-cases.json +31 -0
- package/skills/quant-analysis/trigger-cases.json +31 -0
- package/skills/rag-triage/SKILL.md +64 -0
- package/skills/rag-triage/evals.json +57 -0
- package/skills/rag-triage/references/triage-playbook.md +75 -0
- package/skills/rag-triage/trigger-cases.json +31 -0
- package/skills/real-estate-property/trigger-cases.json +30 -0
- package/skills/refactor-safely/SKILL.md +1 -1
- package/skills/refactor-safely/trigger-cases.json +30 -0
- package/skills/regression-forecast/trigger-cases.json +31 -0
- package/skills/release-checklist/trigger-cases.json +31 -0
- package/skills/report-craft/SKILL.md +76 -0
- package/skills/report-craft/references/feasibility.md +118 -0
- package/skills/report-craft/references/review-checklist.md +51 -0
- package/skills/report-craft/references/visuals.md +114 -0
- package/skills/report-craft/references/writing.md +80 -0
- package/skills/report-craft/scripts/__pycache__/chart_render.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/__pycache__/fin_model.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/__pycache__/project_facts.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/chart_render.py +1222 -0
- package/skills/report-craft/scripts/fin_model.py +602 -0
- package/skills/report-craft/scripts/project_facts.py +580 -0
- package/skills/report-craft/scripts/report_battery.py +349 -0
- package/skills/report-craft/trigger-cases.json +15 -0
- package/skills/retail-store-ops/trigger-cases.json +30 -0
- package/skills/risk-compliance/trigger-cases.json +31 -0
- package/skills/scope-creep/SKILL.md +45 -0
- package/skills/scope-creep/trigger-cases.json +7 -0
- package/skills/sec-auth/trigger-cases.json +31 -0
- package/skills/sec-compliance/trigger-cases.json +31 -0
- package/skills/securities-chain/trigger-cases.json +31 -0
- package/skills/securities-research/trigger-cases.json +31 -0
- package/skills/security-audit/trigger-cases.json +31 -0
- package/skills/slides-html/SKILL.md +25 -10
- package/skills/slides-html/trigger-cases.json +11 -0
- package/skills/sql-analysis/SKILL.md +110 -0
- package/skills/sql-analysis/trigger-cases.json +34 -0
- package/skills/sql-tuning/trigger-cases.json +31 -0
- package/skills/statistical-testing/trigger-cases.json +31 -0
- package/skills/strategy-planning/trigger-cases.json +30 -0
- package/skills/supply-chain/trigger-cases.json +31 -0
- package/skills/tax-compliance/trigger-cases.json +30 -0
- package/skills/tech-doc/SKILL.md +262 -0
- package/skills/tech-doc/scripts/__pycache__/archive_battery.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_build.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_check.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_spec.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/doc_templates.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/archive_battery.py +860 -0
- package/skills/tech-doc/scripts/archive_build.py +599 -0
- package/skills/tech-doc/scripts/archive_check.py +1307 -0
- package/skills/tech-doc/scripts/archive_spec.py +499 -0
- package/skills/tech-doc/scripts/doc_templates.py +624 -0
- package/skills/tech-doc/trigger-cases.json +57 -0
- package/skills/tech-research/trigger-cases.json +31 -0
- package/skills/tender-bidding/trigger-cases.json +31 -0
- package/skills/translation-pro/trigger-cases.json +30 -0
- package/skills/treasury-mgmt/trigger-cases.json +31 -0
- package/skills/trim-cot-leakage/SKILL.md +58 -0
- package/skills/trim-cot-leakage/trigger-cases.json +23 -0
- package/skills/web-craft/SKILL.md +103 -0
- package/skills/web-craft/references/craft-checklist.md +75 -0
- package/skills/web-craft/references/design-direction.md +122 -0
- package/skills/web-craft/references/h5-mobile.md +100 -0
- package/skills/web-craft/references/motion.md +136 -0
- package/skills/web-craft/references/recipes.md +74 -0
- package/skills/web-craft/templates/wedding-invitation.html +1180 -0
- package/skills/web-craft/trigger-cases.json +14 -0
- package/skills/web-scraping/trigger-cases.json +34 -0
- package/skills/web-search/SKILL.md +11 -10
- package/skills/web-search/trigger-cases.json +31 -0
- package/skills/weekly-report/SKILL.md +134 -0
- package/skills/weekly-report/trigger-cases.json +11 -0
- package/skills/write-tests/trigger-cases.json +31 -0
- package/skills/xlsx-analysis/SKILL.md +143 -77
- package/skills/xlsx-analysis/scripts/__pycache__/data_profile.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_build.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_check.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_layout.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_toolkit.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/data_profile.py +391 -0
- package/skills/xlsx-analysis/scripts/xlsx_battery.py +389 -0
- package/skills/xlsx-analysis/scripts/xlsx_build.py +522 -0
- package/skills/xlsx-analysis/scripts/xlsx_check.py +924 -0
- package/skills/xlsx-analysis/scripts/xlsx_layout.py +235 -0
- package/skills/xlsx-analysis/scripts/xlsx_style.py +28 -66
- package/skills/xlsx-analysis/scripts/xlsx_toolkit.py +61 -5
- package/skills/xlsx-analysis/trigger-cases.json +59 -0
- package/squads/hc-defense.md +47 -0
- package/squads/hc-rdarchive.md +110 -0
- package/squads/hc-redteam.md +50 -0
- package/squads/hc-weekly.md +125 -0
- package/src/account.js +1 -0
- package/src/acp.js +1 -0
- package/src/advisor.js +1 -1
- package/src/agent.js +1 -1
- package/src/api.js +1 -1
- package/src/archaeology.js +1 -0
- package/src/ask-ui.js +1 -0
- package/src/assets-import.js +1 -1
- package/src/autofix.js +1 -1
- package/src/automation-tools.js +1 -0
- package/src/automation.js +1 -1
- package/src/autorun.js +1 -1
- package/src/banner.js +1 -1
- package/src/capability-probe.js +1 -0
- package/src/checkpoints.js +1 -1
- package/src/code-tools.js +1 -0
- package/src/codeindex.js +1 -1
- package/src/codemode.js +1 -0
- package/src/command-bridge.js +1 -0
- package/src/command-ui.js +1 -0
- package/src/commands.js +1 -1
- package/src/compaction.js +1 -1
- package/src/computer.js +1 -0
- package/src/config.js +1 -1
- package/src/consult.js +1 -1
- package/src/context-report.js +1 -1
- package/src/context-window.js +1 -0
- package/src/context.js +1 -1
- package/src/crew.js +1 -0
- package/src/crush.js +1 -0
- package/src/daemon.js +1 -1
- package/src/decisions.js +1 -1
- package/src/depaudit.js +1 -0
- package/src/device.js +1 -1
- package/src/diagram-draw.js +1 -0
- package/src/diagram-layouts.js +1 -0
- package/src/diagram-raster.js +1 -0
- package/src/diagram.js +1 -0
- package/src/dictation.js +1 -0
- package/src/diffview.js +1 -1
- package/src/doctor.js +1 -1
- package/src/docx-tools.js +1 -0
- package/src/errkind.js +1 -0
- package/src/evidence.js +1 -0
- package/src/executors.js +1 -1
- package/src/expert-catalog.js +1 -1
- package/src/experts-store.js +1 -1
- package/src/experts.js +1 -1
- package/src/external-agents.js +1 -0
- package/src/forks.js +1 -1
- package/src/fs-observe.js +1 -0
- package/src/gitflow.js +1 -1
- package/src/goalround.js +1 -0
- package/src/hooks.js +1 -1
- package/src/input.js +1 -1
- package/src/insights.js +1 -1
- package/src/jobs.js +1 -0
- package/src/kbsync.js +1 -1
- package/src/lsp.js +1 -0
- package/src/mcp-apps.js +1 -0
- package/src/mcp.js +1 -1
- package/src/memory.js +1 -1
- package/src/notify.js +1 -0
- package/src/permissions.js +1 -1
- package/src/plan-limits.js +1 -0
- package/src/policy.js +1 -0
- package/src/pptx-tools.js +1 -0
- package/src/presence.js +1 -0
- package/src/privatedir.js +1 -0
- package/src/pyruntime.js +1 -1
- package/src/radar.js +1 -0
- package/src/render.js +1 -1
- package/src/repl.js +1 -1
- package/src/report-tools.js +1 -0
- package/src/routing.js +1 -0
- package/src/sandbox-probe.js +1 -0
- package/src/sandbox-win-launcher.mjs +156 -0
- package/src/sandbox.js +1 -0
- package/src/scan.js +1 -1
- package/src/schedule.js +1 -1
- package/src/scopecreep.js +1 -0
- package/src/sdk.js +1 -0
- package/src/selection.js +1 -0
- package/src/session-events.js +1 -0
- package/src/session.js +1 -1
- package/src/sessionindex.js +1 -0
- package/src/sessionlog.js +1 -0
- package/src/skill-catalog.js +1 -0
- package/src/skill-lint.js +1 -0
- package/src/skill-optimize.js +1 -0
- package/src/skill-scan.js +1 -0
- package/src/skills.js +1 -1
- package/src/spill.js +1 -0
- package/src/squad.js +1 -1
- package/src/subagent.js +1 -1
- package/src/surface-state.js +1 -0
- package/src/tapd.js +1 -0
- package/src/terminal-select.js +1 -0
- package/src/terminal.js +1 -0
- package/src/test-runner.js +1 -0
- package/src/todo.js +1 -1
- package/src/toolpipe.js +1 -0
- package/src/tools.js +1 -1
- package/src/toon.js +1 -0
- package/src/transcript.js +1 -1
- package/src/tui/app.js +1 -1
- package/src/tui/command-menu.js +1 -0
- package/src/tui/progress.js +1 -0
- package/src/tui/scroll.js +1 -0
- package/src/tui/welcome.js +1 -0
- package/src/tui/workbench.js +1 -0
- package/src/ui-spec.js +1 -0
- package/src/uitree.js +1 -0
- package/src/uninstall.js +1 -1
- package/src/vision.js +1 -0
- package/src/visual.js +1 -0
- package/src/voice-form.js +1 -0
- package/src/voice.js +1 -0
- package/src/watch.js +1 -1
- package/src/web-audit.js +1 -0
- package/src/web-cdp.js +1 -0
- package/src/web-inspect.js +1 -0
- package/src/websearch.js +1 -0
- package/src/weekly.js +1 -0
- package/src/workflow.js +1 -0
- package/src/worklog.js +1 -0
- package/src/xlsx-tools.js +1 -0
- package/ui/command-picker.js +79 -0
- package/skills/ab-testing/scripts/__pycache__/ab_test.cpython-312.pyc +0 -0
- package/skills/accounting-ops/scripts/__pycache__/acct_tools.cpython-312.pyc +0 -0
- package/skills/anomaly-detection/scripts/__pycache__/anomaly.cpython-312.pyc +0 -0
- package/skills/audit-assist/scripts/__pycache__/audit_sampling.cpython-312.pyc +0 -0
- package/skills/banking-credit/scripts/__pycache__/credit_metrics.cpython-312.pyc +0 -0
- package/skills/data-report-pro/scripts/__pycache__/bigdata.cpython-312.pyc +0 -0
- package/skills/data-wrangling/scripts/__pycache__/join_tools.cpython-312.pyc +0 -0
- package/skills/ecommerce-ops/scripts/__pycache__/gmv_decompose.cpython-312.pyc +0 -0
- package/skills/funnel-cohort/scripts/__pycache__/funnel.cpython-312.pyc +0 -0
- package/skills/management-accounting/scripts/__pycache__/costing.cpython-312.pyc +0 -0
- package/skills/manufacturing-quality/scripts/__pycache__/spc.cpython-312.pyc +0 -0
- package/skills/marketing-analytics/scripts/__pycache__/rfm.cpython-312.pyc +0 -0
- package/skills/optimization-solver/scripts/__pycache__/solver.cpython-312.pyc +0 -0
- package/skills/pivot-crosstab/scripts/__pycache__/pivot_tools.cpython-312.pyc +0 -0
- package/skills/portfolio-analysis/scripts/__pycache__/portfolio.cpython-312.pyc +0 -0
- package/skills/procurement-supply/scripts/__pycache__/supply.cpython-312.pyc +0 -0
- package/skills/production-planning/scripts/__pycache__/mrp_capacity.cpython-312.pyc +0 -0
- package/skills/project-management/scripts/__pycache__/cpm.cpython-312.pyc +0 -0
- package/skills/quant-analysis/scripts/__pycache__/quant.cpython-312.pyc +0 -0
- package/skills/real-estate-property/scripts/__pycache__/property_calc.cpython-312.pyc +0 -0
- package/skills/regression-forecast/scripts/__pycache__/regress.cpython-312.pyc +0 -0
- package/skills/securities-research/scripts/__pycache__/valuation.cpython-312.pyc +0 -0
- package/skills/statistical-testing/scripts/__pycache__/stat_tests.cpython-312.pyc +0 -0
- package/skills/treasury-mgmt/scripts/__pycache__/cashflow_forecast.cpython-312.pyc +0 -0
package/src/subagent.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:fs";import e from"node:path";import n from"node:os";import{runTurn as r}from"./agent.js";import{createPermissions as o}from"./permissions.js";import{parseFrontmatter as i}from"./skills.js";import{getIndex as s}from"./codeindex.js";let a=0;const c=[];export async function withSubagentSlot(t,e,n){if(e>0)return n();a>=function(t){const e=Number(t?.subagentConcurrency);return Number.isFinite(e)&&e>0?Math.max(1,Math.floor(e)):8}(t)?await new Promise(t=>c.push(t)):a++;try{return await n()}finally{const t=c.shift();t?t():a--}}function l(t){try{s(t)}catch{}}export function loadAgents(r){const o=[r?e.join(r,".huicai","agents"):null,e.join(n.homedir(),".huicai","agents")].filter(Boolean),s=new Map;for(const n of o){let r;try{r=t.readdirSync(n).filter(t=>t.endsWith(".md"))}catch{continue}for(const o of r){const r=o.replace(/\.md$/,"").trim();if(r&&!s.has(r))try{const a=t.readFileSync(e.join(n,o),"utf8"),c=i(a);s.set(r,{name:(c.name||r).trim(),description:c.description||"",prompt:c.body||a})}catch{}}}return[...s.values()]}function u(t,e){return t+(e&&e.path?" "+e.path:e&&e.command?": "+String(e.command).split("\n")[0].slice(0,80):e&&e.objective?": "+String(e.objective).slice(0,60):e&&e.query?" "+String(e.query).slice(0,60):e&&e.pattern?" "+e.pattern:"")}export function makeSubEvents(t,e,n){if("function"!=typeof t)return;const r=(r,o)=>{try{t({scope:e,kind:n,type:r,...o})}catch{}};return{onToolStart:(t,e)=>r("tool",{name:t,text:u(t,e)}),onToolResult:(t,e)=>r("toolresult",{name:t,text:String(e).split("\n")[0].slice(0,160)}),onInfo:t=>r("info",{text:String(t).slice(0,200)}),onAssistantText:t=>r("text",{text:String(t).slice(0,400)})}}async function p({config:t,cwd:e,readOnlyExtras:n,depth:i,objective:s,context:a,agentDef:c,onSub:l,label:u}){if(!s)return"未提供子任务目标 objective";const p={...t,aux:!0};t.subagentModel&&t.subagentModel!==t.model&&(p.model=t.subagentModel);const g=c?c.prompt.trim()+"\n\n工作目录:"+e+(a&&String(a).trim()?"\n\n## 背景\n"+a:""):function(t,e){let n="你是一个专注的子智能体,只负责完成主智能体委派的单一子任务。要求:聚焦目标;用可用的只读工具(读文件 / 搜索 / 列目录等)收集信息;直接输出结构清晰、可被主智能体直接复用的最终结论,不要寒暄,也不要复述任务本身。\n工作目录:"+t+"\n";e&&String(e).trim()&&(n+="\n## 主智能体提供的背景\n"+e+"\n");return n}(e,a),m=[{role:"system",content:g},{role:"user",content:s}],d=o({yolo:!1,ask:null}),f=makeSubagentTool({config:p,cwd:e,extraTools:n,depth:i+1,onSub:l}),y=f?[...n,f]:n,h=makeSubEvents(l,u||String(s).slice(0,60),"agent"),b=await withSubagentSlot(t,i,()=>r({config:p,messages:m,permissions:d,cwd:e,extraTools:y,depth:i+1,maxIters:t.subagentMaxIters||40,io:{write:()=>{}},events:h}));if(b?.error)return`子智能体执行失败:${b.error}`;const x=[...m].reverse().find(t=>"assistant"===t.role&&t.content);return x?x.content:"(子智能体未产出结论)"}export function makeSubagentTool({config:t,cwd:e,extraTools:n=[],depth:r=0,onSub:o=null}){if(r>=2)return null;const i=loadAgents(e),s={type:"function",function:{name:"dispatch_agent",description:"把一个明确、独立、自足的子任务委派给子智能体:它在隔离上下文中自主使用(只读)工具完成后只回传结论。适合可并行的调研/检索/分析类子任务,让主线程上下文保持精简。可多次调用以分解复杂任务。",parameters:{type:"object",properties:{objective:{type:"string",description:"要子智能体完成的单一、明确、自足的子任务"},context:{type:"string",description:"可选:完成该子任务所需的背景信息"},...i.length?{agent:{type:"string",enum:i.map(t=>t.name),description:"可选:使用某个已定义的子智能体人格 — "+i.map(t=>t.name+(t.description?`(${t.description.slice(0,40)})`:"")).join(";")}}:{}},required:["objective"]}}},a=n.filter(t=>t.readOnly);return{readOnly:!0,spec:s,run:async n=>{l(e);const s=n?.agent?i.find(t=>t.name===n.agent):null,c=String(n?.objective||"").trim();return p({config:t,cwd:e,readOnlyExtras:a,depth:r,objective:c,context:n?.context,agentDef:s,onSub:o,label:c.slice(0,60)})}}}export function makeDispatchAgentsTool({config:t,cwd:e,extraTools:n=[],depth:r=0,onSub:o=null}){if(r>=2)return null;const i=loadAgents(e),s=n.filter(t=>t.readOnly);return{readOnly:!0,spec:{type:"function",function:{name:"dispatch_agents",description:"一次性把多个**独立、可并行**的子任务同时委派给多个子智能体(隔离上下文、只读工具),并行执行后聚合各自结论。适合把一个较大目标拆成 3-8 个可并行的调研/检索/分析子任务同时推进,显著减少串行等待(上限 12 个)。每个子任务需自足;多个子任务共用的背景放 shared_context,不要在每个 context 里重复抄一遍。复杂任务优先用它一次性铺开,而不是反复单发 dispatch_agent 串行等待。",parameters:{type:"object",properties:{tasks:{type:"array",description:"并行子任务数组(建议 3-8 个,上限 12)",items:{type:"object",properties:{objective:{type:"string",description:"单一、明确、自足的子任务"},context:{type:"string",description:"可选:完成该子任务所需背景"},...i.length?{agent:{type:"string",enum:i.map(t=>t.name),description:"可选:指定子智能体人格"}}:{}},required:["objective"]}},shared_context:{type:"string",description:"可选:所有子任务共用的背景(会附加到每个子任务,省去逐条重复)"}},required:["tasks"]}}},run:async n=>{const a=Array.isArray(n?.tasks)?n.tasks:[],c=a.slice(0,12);if(!c.length)return"未提供 tasks(需要一个子任务数组)";const u=a.length-c.length;l(e);const g=String(n?.shared_context||"").trim(),m=(await async function(t,e,n){const r=new Array(t.length);let o=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(e,t.length))},async()=>{for(;o<t.length;){const e=o++;r[e]=await n(t[e],e)}})),r}(c,c.length,async(n,a)=>{const c=n?.agent?i.find(t=>t.name===n.agent):null,l=String(n?.objective||"").trim(),u=String(n?.context||"").trim(),m=g&&u?`${g}\n\n${u}`:g||u;try{const i=await p({config:t,cwd:e,readOnlyExtras:s,depth:r,objective:l,context:m,agentDef:c,onSub:o,label:`#${a+1} ${l.slice(0,50)}`});return{objective:l.replace(/\s+/g," ").slice(0,80),agent:n?.agent,out:i}}catch(t){if(t&&"AbortError"===t.name)throw t;return{objective:l.replace(/\s+/g," ").slice(0,80),agent:n?.agent,out:`子任务执行异常:${t.message}`}}})).map((t,e)=>`【子任务 ${e+1}${t.agent?" · "+t.agent:""}】${t.objective}\n${t.out}`).join("\n\n---\n\n");return u>0?`${m}\n\n---\n\n(注意:本次提交了 ${a.length} 个子任务,超出上限 12,末尾 ${u} 个未执行,需要的话再发一批。)`:m}}}
|
|
1
|
+
import t from"node:fs";import e from"node:path";import n from"node:os";import{runTurn as r}from"./agent.js";import{createSession as o,saveSession as s,userKeyOf as i}from"./session.js";import*as a from"./jobs.js";import{createPermissions as c}from"./permissions.js";import{parseFrontmatter as l}from"./skills.js";import{getIndex as u}from"./codeindex.js";import{validateAgainstSchema as d}from"./workflow.js";import{VERIFY_DISCIPLINE as g}from"./crew.js";let p=0;const m=[];export async function withSubagentSlot(t,e,n,r){if(r?.throwIfAborted(),e>0)return n();p>=function(t){const e=Number(t?.subagentConcurrency);return Number.isFinite(e)&&e>0?Math.max(1,Math.floor(e)):8}(t)?await new Promise((t,e)=>{const n=()=>{r?.removeEventListener("abort",o),t()},o=()=>{const t=m.indexOf(n);t>=0&&m.splice(t,1),e(r.reason||new DOMException("已取消","AbortError"))};m.push(n),r?.addEventListener("abort",o,{once:!0})}):p++;try{return r?.throwIfAborted(),await n()}finally{const t=m.shift();t?t():p--}}function f(t){try{u(t)}catch{}}export function loadAgents(r){const o=[r?e.join(r,".huicai","agents"):null,e.join(n.homedir(),".huicai","agents")].filter(Boolean),s=new Map;for(const n of o){let r;try{r=t.readdirSync(n).filter(t=>t.endsWith(".md"))}catch{continue}for(const o of r){const r=o.replace(/\.md$/,"").trim();if(r&&!s.has(r))try{const i=t.readFileSync(e.join(n,o),"utf8"),a=l(i);s.set(r,{name:(a.name||r).trim(),description:a.description||"",prompt:a.body||i})}catch{}}}return[...s.values()]}function b(t,e){return t+(e&&e.path?" "+e.path:e&&e.command?": "+String(e.command).split("\n")[0].slice(0,80):e&&e.objective?": "+String(e.objective).slice(0,60):e&&e.query?" "+String(e.query).slice(0,60):e&&e.pattern?" "+e.pattern:"")}export function makeSubEvents(t,e,n){if("function"!=typeof t)return;const r=(r,o)=>{try{t({scope:e,kind:n,type:r,...o})}catch{}};return{onToolStart:(t,e)=>r("tool",{name:t,text:b(t,e)}),onToolResult:(t,e)=>r("toolresult",{name:t,text:String(e).split("\n")[0].slice(0,160)}),onInfo:t=>r("info",{text:String(t).slice(0,200)}),onAssistantText:t=>r("text",{text:String(t).slice(0,400)})}}async function h({config:t,cwd:e,readOnlyExtras:n,depth:o,objective:s,context:i,agentDef:a,onSub:l,label:u,structured:p,signal:m}){if(!s)return"未提供子任务目标objective";const f={...t,aux:!0};t.subagentModel&&t.subagentModel!==t.model&&(f.model=t.subagentModel);const b=a?a.prompt.trim()+"\n\n工作目录:"+e+(i&&String(i).trim()?"\n\n## 背景\n"+i:""):function(t,e){let n="你是一个专注的子智能体,只负责完成主智能体委派的单一子任务。要求:聚焦目标;用可用的只读工具(读文件 / 搜索 / 列目录等)收集信息;直接输出结构清晰、可被主智能体直接复用的最终结论,不要寒暄,也不要复述任务本身。\n工作目录:"+t+"\n";n+="\n"+g+"\n",e&&String(e).trim()&&(n+="\n## 主智能体提供的背景\n"+e+"\n");return n}(e,i),h=p?{value:void 0}:null,y=p?function(t,e){return{readOnly:!0,spec:{type:"function",function:{name:"structured_output",description:"提交本次子任务的结构化结果。这是你的产出方式,调用它之后即可结束。",parameters:t}},run:async n=>{const r=d(n,t);return r.length?"结果不符合要求,请修正后重新调用:\n"+r.join("\n"):(e.value=n,"已记录结构化结果。现在可以结束了。")}}}(p,h):null,S=[{role:"system",content:b},{role:"user",content:p?s+"\n\n完成后必须调用structured_output工具提交结构化结果(这是你唯一的产出方式,不要只把JSON写在正文里)。":s}],x=c({yolo:!1,ask:null}),v=makeSubagentTool({config:f,cwd:e,extraTools:n,depth:o+1,onSub:l}),j=[...n,...v?[v]:[],...y?[y]:[]],w=u||String(s).slice(0,60),$=makeSubEvents(l,w,"agent"),A=(t,e)=>{if("function"==typeof l)try{l({scope:w,kind:"agent",type:t,...e})}catch{}},k=Date.now();A("start",{text:String(s).replace(/\s+/g," ").slice(0,200),agent:a?.name||""});const I=await withSubagentSlot(t,o,()=>r({config:f,messages:S,permissions:x,cwd:e,extraTools:j,depth:o+1,maxIters:t.subagentMaxIters||40,io:{write:()=>{}},events:$,signal:m}),m);m?.throwIfAborted();const _=(t,e)=>A("end",{ok:t,ms:Date.now()-k,text:String(e||"").replace(/\s+/g," ").slice(0,200)});if(I?.error)return _(!1,I.error),p?{ok:!1,error:`子智能体执行失败:${I.error}`}:`子智能体执行失败:${I.error}`;if(p)return void 0===h.value?(_(!1,"未提交结构化结果"),{ok:!1,error:"子智能体没有提交结构化结果(未调用structured_output)。"}):(_(!0,JSON.stringify(h.value)),{ok:!0,value:h.value});const T=[...S].reverse().find(t=>"assistant"===t.role&&t.content),q=T?T.content:"(子智能体未产出结论)";return _(!!T,q),q}export function makeWorkflowDispatcher({config:t,cwd:e,extraTools:n=[],depth:r=0,onSub:o=null}){const s=loadAgents(e),i=n.filter(t=>t.readOnly);return async n=>{const a=n.agentType?s.find(t=>t.name===n.agentType):null;if(n.agentType&&!a)return{ok:!1,usage:!0,error:`agent() 指定的agentType「${n.agentType}」不存在。可用:${s.map(t=>t.name).join("、")||"(本项目未定义任何子智能体人格)"}`};f(e);const c=await h({config:t,cwd:e,readOnlyExtras:i,depth:r,objective:n.prompt,context:n.context,agentDef:a,onSub:o,label:n.label,structured:n.schema});if(n.schema)return c.ok?{ok:!0,result:c.value}:{ok:!1,error:c.error};const l=String(c);return l.startsWith("子智能体执行失败:")?{ok:!1,error:l}:{ok:!0,result:l}}}export function makeSubagentTool({config:t,cwd:e,extraTools:n=[],depth:r=0,onSub:o=null}){if(r>=2)return null;const s=loadAgents(e),i={type:"function",function:{name:"dispatch_agent",description:"把一个明确、独立、自足的子任务委派给子智能体:它在隔离上下文中自主使用(只读)工具完成后只回传结论。适合可并行的调研/检索/分析类子任务,让主线程上下文保持精简。可多次调用以分解复杂任务。",parameters:{type:"object",properties:{objective:{type:"string",description:"要子智能体完成的单一、明确、自足的子任务"},context:{type:"string",description:"可选:完成该子任务所需的背景信息"},...s.length?{agent:{type:"string",enum:s.map(t=>t.name),description:"可选:使用某个已定义的子智能体人格 — "+s.map(t=>t.name+(t.description?`(${t.description.slice(0,40)})`:"")).join(";")}}:{}},required:["objective"]}}},a=n.filter(t=>t.readOnly),c=async(n,i,c=o)=>{i?.throwIfAborted(),f(e);const l=n?.agent?s.find(t=>t.name===n.agent):null,u=String(n?.objective||"").trim();return h({config:t,cwd:e,readOnlyExtras:a,depth:r,objective:u,context:n?.context,agentDef:l,onSub:c,signal:i,label:u.slice(0,60)})};return{readOnly:!0,spec:i,run:c,runWithEnv:(t,e,n)=>c(t,n,t=>o?.({...t,callId:e.toolCallId}))}}export function makeDispatchAgentsTool({config:t,cwd:e,extraTools:n=[],depth:r=0,onSub:o=null}){if(r>=2)return null;const s=loadAgents(e),i=n.filter(t=>t.readOnly),a={objective:{type:"string",description:"单一、明确、自足的子任务"},context:{type:"string",description:"可选:完成该子任务所需背景"},...s.length?{agent:{type:"string",enum:s.map(t=>t.name),description:"可选:指定子智能体人格"}}:{}},c=async(n,a,c=o)=>{a?.throwIfAborted();const l=Array.isArray(n?.tasks)?n.tasks:[],u=l.slice(0,12);if(!u.length)return"未提供tasks(需要一个子任务数组)";const d=l.length-u.length;f(e);const g=String(n?.shared_context||"").trim();if("function"==typeof c)try{c({kind:"agent",type:"batch",text:`并行 ${u.length} 个子任务`,tasks:u.map((t,e)=>({scope:`#${e+1} ${String(t?.objective||"").slice(0,50)}`,objective:String(t?.objective||"").replace(/\s+/g," ").slice(0,200),agent:t?.agent||""}))})}catch{}const p=(await async function(t,e,n){const r=new Array(t.length);let o=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(e,t.length))},async()=>{for(;o<t.length;){const e=o++;r[e]=await n(t[e],e)}})),r}(u,u.length,async(n,o)=>{const l=n?.agent?s.find(t=>t.name===n.agent):null,u=String(n?.objective||"").trim(),d=String(n?.context||"").trim(),p=g&&d?`${g}\n\n${d}`:g||d;try{const s=await h({config:t,cwd:e,readOnlyExtras:i,depth:r,objective:u,context:p,agentDef:l,onSub:c,signal:a,label:`#${o+1} ${u.slice(0,50)}`});return{objective:u.replace(/\s+/g," ").slice(0,80),agent:n?.agent,out:s}}catch(t){if(t&&"AbortError"===t.name)throw t;return{objective:u.replace(/\s+/g," ").slice(0,80),agent:n?.agent,out:`子任务执行异常:${t.message}`}}})).map((t,e)=>`【子任务 ${e+1}${t.agent?" · "+t.agent:""}】${t.objective}\n${t.out}`).join("\n\n---\n\n");return d>0?`${p}\n\n---\n\n(注意:本次提交了 ${l.length} 个子任务,超出上限 12,末尾 ${d} 个未执行,需要的话再发一批。)`:p};return{readOnly:!0,spec:{type:"function",function:{name:"dispatch_agents",description:"一次性把多个**独立、可并行**的子任务同时委派给多个子智能体(隔离上下文、只读工具),并行执行后聚合各自结论。适合把一个较大目标拆成 3-8 个可并行的调研/检索/分析子任务同时推进,显著减少串行等待(上限 12 个)。每个子任务需自足;多个子任务共用的背景放shared_context,不要在每个context里重复抄一遍。复杂任务优先用它一次性铺开,而不是反复单发dispatch_agent串行等待。",parameters:{type:"object",properties:{tasks:{type:"array",description:"并行子任务数组(建议 3-8 个,上限 12)",items:{type:"object",properties:a,required:["objective"]}},shared_context:{type:"string",description:"可选:所有子任务共用的背景(会附加到每个子任务,省去逐条重复)"}},required:["tasks"]}}},run:c,runWithEnv:(t,e,n)=>c(t,n,t=>o?.({...t,callId:e.toolCallId}))}}const y=new Map;function S(t){return{id:t.childId,label:t.label,parentSession:t.parentSessionId,depth:t.depth,status:t.running?"running":t.queue.length?"queued":"idle",job:t.job?.id||"",queued:t.queue.length,turns:t.turns,startedAt:t.startedAt,lastActiveAt:t.lastActiveAt}}function x(t,e){const n=y.get(String(t||""));return n?n.parentSessionId!==String(e||"")?{err:`无权操作子智能体 ${t}:它不是由当前会话派生的`}:{act:n}:{err:`未找到子智能体 ${t}(它可能已结束并被清理,或从未存在)`}}function v(t,e,n){t.queue.push({text:String(e),settle:n}),t.running||async function(t){if(t.running)return;t.running=!0,t.job=a.start({kind:"agent",label:`子智能体 ${t.childId}:${t.label}`,sessionId:t.parentSessionId,cwd:t.cwd,kill:()=>{try{t.abort?.abort()}catch{}},meta:{subagentId:t.childId}});let e=!1,n="",o=0;try{for(;t.queue.length;){const i=t.queue.shift();let a;t.session.messages.push({role:"user",content:i.text}),t.abort=new AbortController,t.turns++,t.lastActiveAt=Date.now();try{a=await withSubagentSlot(t.config,t.depth,()=>r({config:t.config,messages:t.session.messages,permissions:t.permissions,cwd:t.cwd,extraTools:t.extras,depth:t.depth,maxIters:t.config.subagentMaxIters||40,io:{write:()=>{}},events:t.events,signal:t.abort.signal,compaction:t.session.compaction}))}catch(t){a=t&&"AbortError"===t.name?{aborted:!0}:{error:t.message}}t.abort=null,t.lastActiveAt=Date.now();const c=[...t.session.messages].reverse().find(t=>"assistant"===t.role&&t.content);t.lastResult=a?.error?`执行失败:${a.error}`:c?.content||"(未产出结论)";try{t.job?.append(`\n【回合 ${t.turns} 结论】\n${t.lastResult}\n`)}catch{}try{s(t.session)}catch{}if(i.settle&&i.settle(a),o++,a?.error&&(n=a.error),a?.aborted){e=!0;break}}}finally{t.running=!1;const r=t.job;if(t.job=null,r){const s=t.lastResult?String(t.lastResult).replace(/\s+/g," ").slice(0,160):"";e?r.settle("killed",`被打断(${t.queue.length} 条消息仍在排队)`):n?r.settle("failed",n.slice(0,120)):r.settle("done",`${o} 个回合;${s}`)}}}(t).catch(()=>{})}function j({config:t,cwd:e,readOnlyExtras:n,depth:r,objective:a,context:l,agentDef:u,onSub:d,parentSession:g,deliverToParent:p}){const m=String(g?.id||""),f=r+1,h={...t,aux:!0};t.subagentModel&&t.subagentModel!==t.model&&(h.model=t.subagentModel);const x=String(a).replace(/\s+/g," ").slice(0,80),j=u?u.prompt.trim()+"\n\n工作目录:"+e+(l&&String(l).trim()?"\n\n## 背景\n"+l:""):function(t,e){let n="你是一个可持续对话的子智能体:派你来的智能体可能会在你工作过程中追加信息或新指令,你们共享同一个工作目录。要求:聚焦当前收到的指令;用可用的只读工具收集信息;发现会改变对方决策的结论时用report立刻回报,不要攒到最后;输出结构清晰、可直接复用的结论,不寒暄、不复述任务本身。\n工作目录:"+t+"\n";e&&String(e).trim()&&(n+="\n## 背景\n"+e+"\n");return n}(e,l),$=o({cwd:e,model:h.model,messages:[{role:"system",content:j}],parentSession:m,origin:"subagent",delegationDepth:f,label:x,user:g?.user||i(t)||""});$.title="(子智能体) "+x;try{s($)}catch{}h.sessionId=$.id;const A={childId:$.id,parentSessionId:m,label:x,depth:f,cwd:e,config:h,session:$,permissions:c({yolo:!1,ask:null}),queue:[],running:!1,abort:null,turns:0,startedAt:Date.now(),lastActiveAt:Date.now(),lastResult:"",baseEvents:makeSubEvents(d,x,"agent"),job:null,extras:[]},k=makeSubagentTool({config:h,cwd:e,extraTools:n,depth:f,onSub:d});return A.extras=[...n,...k?[k]:[],w(A,p)],A.events=function(t){const e=e=>{try{t.job?.append(e)}catch{}},n=t.baseEvents;return{onToolStart:(t,r)=>{e(`· ${b(t,r)}\n`),n?.onToolStart?.(t,r)},onToolResult:(t,r)=>{e(` → ${String(r).split("\n")[0].slice(0,200)}\n`),n?.onToolResult?.(t,r)},onInfo:t=>{e(` [信息] ${String(t).slice(0,200)}\n`),n?.onInfo?.(t)},onAssistantText:t=>{e(`\n${String(t)}\n`),n?.onAssistantText?.(t)}}}(A),y.set(A.childId,A),v(A,a),{childId:A.childId,view:S(A)}}function w(t,e){return{readOnly:!0,spec:{type:"function",function:{name:"report",description:"把内容回报给派你来的那个智能体。在你**完成前**就可以调用:发现了会改变对方下一步决策的结论时立刻报,不要攒到最后。对方与你共享工作目录,但看不到你的对话过程、工具输出和推理——所以「你把活干完了」本身不等于对方拿到了结果。回报不会结束你的回合,你可以继续做事。",parameters:{type:"object",properties:{output:{type:"string",description:"给对方的自足内容:写清结论,并指出相关的共享路径"}},required:["output"]}}},run:async n=>{const r=String(n?.output||"").trim();return r?"function"!=typeof e?"当前环境不支持向上回报(父智能体不在本进程)。":(e(`【子智能体回报 · ${t.label}(${t.childId})】\n${r}`),"已回报给派你来的智能体。你可以继续做事。"):"未提供output(要回报什么)。"}}}export function makeContinuableAgentTools({config:t,cwd:e,extraTools:n=[],depth:r=0,onSub:o=null,session:s=null,queueInject:i=null}){if(r>=2)return[];const a=loadAgents(e),c=n.filter(t=>t.readOnly),l=()=>String(s?.id||t.sessionId||""),u="function"==typeof i?t=>i(t):null,d=(t,e,n={},r=[])=>({type:"function",function:{name:t,description:e,parameters:{type:"object",properties:n,required:r}}});return[{readOnly:!0,spec:d("start_agent","启动一个**可持续对话**的子智能体:它在隔离上下文里自主工作,你可以随后用send_message追加信息或新指令,它也会用report主动向你回报阶段性结论。适合边界会变化、需要来回沟通的长任务。一次性、边界明确的子任务用dispatch_agent更省事(派出去只等一个结论)。本调用立即返回子智能体id,不等它做完——它有进展会主动告诉你,不要为了等它而空转。",{objective:{type:"string",description:"交给它的初始任务(明确、自足)"},context:{type:"string",description:"可选:完成该任务所需的背景信息"},...a.length?{agent:{type:"string",enum:a.map(t=>t.name),description:"可选:使用某个已定义的子智能体人格 — "+a.map(t=>t.name+(t.description?`(${t.description.slice(0,40)})`:"")).join(";")}}:{}},["objective"]),run:async n=>{const i=String(n?.objective||"").trim();if(!i)return"未提供objective(要它做什么)。";if(!l())return"当前没有可归属的会话,无法启动可续子智能体。";f(e);const d=n?.agent?a.find(t=>t.name===n.agent):null,{childId:g,view:p}=j({config:t,cwd:e,readOnlyExtras:c,depth:r,objective:i,context:n?.context,agentDef:d,onSub:o,parentSession:s||{id:l()},deliverToParent:u});return`已启动可续子智能体 ${g}(${p.label})。\n它正在后台工作:有进展会用report主动回报给你,每跑完一段也会有完成通知——不要为了等它而空转。\n用send_message("${g}", …) 追加信息,list_agents查看状态(含它当前作业的id,可用job_output看细节),interrupt_agent打断当前回合。`}},{readOnly:!0,spec:d("list_agents","列出你启动的可续子智能体及其状态(运行中 / 排队中 / 空闲)、已跑回合数与最近一次结论摘要。"),run:async()=>{const t=[...y.values()].filter(t=>t.parentSessionId===l());return t.length?"你的可续子智能体:\n"+t.map(t=>{const e=S(t),n={running:"运行中",queued:"有排队消息",idle:"空闲(可继续对话)"}[e.status],r=t.lastResult?"\n 最近结论:"+String(t.lastResult).replace(/\s+/g," ").slice(0,120):"",o=e.job?`\n 进行中的作业 ${e.job}(job_output可看它当前在做什么)`:"";return`· ${e.id} [${n}] 已跑 ${e.turns} 回合${e.queued?`,${e.queued} 条待处理`:""} ${e.label}${o}${r}`}).join("\n"):"你还没有启动过可续子智能体(用start_agent启动)。"}},{readOnly:!1,spec:d("send_message","给一个可续子智能体追加消息,继续同一段对话。它会成为该子智能体的下一个回合:如果它正在忙,消息会排队等当前回合结束——**因此这不能改变它正在做的事**。本调用只确认送达,不返回它的答复;它的结论会经report回到你这里。",{subagent_id:{type:"string",description:"start_agent返回的子智能体id"},message:{type:"string",description:"要交给它的新信息或新指令"}},["subagent_id","message"]),run:async t=>{const e=String(t?.subagent_id||"").trim(),n=String(t?.message||"").trim();if(!e||!n)return"需要subagent_id与message。";const{act:r,err:o}=x(e,l());return o||(v(r,n),r.running?`已送达 ${e}(它正在忙,这条会在当前回合结束后处理;排队 ${r.queue.length} 条)。`:`已送达 ${e},它已开始处理。`)}},{readOnly:!1,spec:d("interrupt_agent","打断一个可续子智能体**当前正在跑的那个回合**。已排队但还没开始的消息会原地保留,它本身仍然可用——之后再send_message就会继续。用于「方向错了,先停下」。",{subagent_id:{type:"string",description:"要打断的子智能体id"}},["subagent_id"]),run:async t=>{const e=String(t?.subagent_id||"").trim();if(!e)return"需要subagent_id。";const{act:n,err:r}=x(e,l());return r||(n.running&&n.abort?(n.abort.abort(),`已请求打断 ${e} 的当前回合(排队中的 ${n.queue.length} 条消息保留,它仍可继续对话)。`):`子智能体 ${e} 当前没有在跑的回合,无需打断。`)}}]}export function resetActivations(){for(const t of y.values())try{t.abort?.abort()}catch{}y.clear()}export function listActivations(t){const e=[...y.values()];return(void 0===t?e:e.filter(e=>e.parentSessionId===String(t||""))).map(S)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import r from"node:fs";import e from"node:path";import{sessionDir as t,safeSegment as n}from"./privatedir.js";export const SURFACE_AREA="surfaces";export const SURFACE_KINDS=["checklist","mapping"];export const MAX_ROWS=200;export const MAX_SURFACES=20;export const EDIT_OPS=["toggle","remove","reorder","setMapping","setText"];const i=r=>null!==r&&"object"==typeof r&&!Array.isArray(r),o=r=>"string"==typeof r?r:"";export function createSurface(r){if(!i(r))return{ok:!1,error:"spec必须是对象"};const e=o(r.id).trim();if(!e)return{ok:!1,error:"缺少id"};const t=o(r.kind);if(!SURFACE_KINDS.includes(t))return{ok:!1,error:`kind只能是 ${SURFACE_KINDS.join(" / ")}`};if("checklist"===t){const n=Array.isArray(r.items)?r.items:null;if(!n||!n.length)return{ok:!1,error:"checklist需要非空items"};if(n.length>200)return{ok:!1,error:"items超过 200 行"};const c=[];for(let r=0;r<n.length;r++){const e=n[r],t=o(i(e)?e.text:e).trim();if(!t)return{ok:!1,error:`第 ${r+1} 项缺text`};c.push({id:o(i(e)&&e.id)||`r${r+1}`,text:t,done:!(!i(e)||!e.done)})}const a=c.map(r=>r.id);return new Set(a).size!==a.length?{ok:!1,error:"items的id有重复"}:{ok:!0,surface:s(e,t,r.title,{rows:c})}}const n=Array.isArray(r.columns)?r.columns:null;if(!n||!n.length)return{ok:!1,error:"mapping需要非空columns"};if(n.length>200)return{ok:!1,error:"columns超过 200 行"};const c=(Array.isArray(r.targets)?r.targets:[]).map(o).filter(Boolean),a=[];for(let r=0;r<n.length;r++){const e=n[r],t=o(i(e)?e.source:e).trim();if(!t)return{ok:!1,error:`第 ${r+1} 列缺source`};const s=o(i(e)&&e.target).trim();if(s&&c.length&&!c.includes(s))return{ok:!1,error:`第 ${r+1} 列的预设目标「${s}」不在targets里`};a.push({source:t,target:s})}const u=a.map(r=>r.source);return new Set(u).size!==u.length?{ok:!1,error:"columns的source有重复"}:{ok:!0,surface:s(e,t,r.title,{pairs:a,targets:c})}}function s(r,e,t,n){return{id:r,kind:e,title:o(t)||"",rev:1,userEditing:!1,updatedAt:Date.now(),...n}}export function applyEdit(r,e){if(!i(r))return{ok:!1,error:"表面不存在"};if(!i(e))return{ok:!1,error:"编辑必须是对象"};const t=o(e.op);if(!EDIT_OPS.includes(t))return{ok:!1,error:`不支持的操作 ${JSON.stringify(t)}(只允许 ${EDIT_OPS.join(" / ")})`};if(null!=e.rev&&Number(e.rev)!==r.rev)return{ok:!1,error:`版本过期(你看到的是rev ${e.rev},当前rev ${r.rev}):请重新读取后再改`};const n={...r,rev:r.rev+1,updatedAt:Date.now()};if("checklist"===r.kind){const i=r.rows.map(r=>({...r}));if("toggle"===t){const r=i.find(r=>r.id===o(e.id));return r?(r.done=null==e.done?!r.done:!!e.done,n.rows=i,{ok:!0,surface:n}):{ok:!1,error:`没有这一项:${e.id}`}}if("remove"===t){const r=i.findIndex(r=>r.id===o(e.id));return r<0?{ok:!1,error:`没有这一项:${e.id}`}:(i.splice(r,1),n.rows=i,{ok:!0,surface:n})}if("setText"===t){const r=i.find(r=>r.id===o(e.id));if(!r)return{ok:!1,error:`没有这一项:${e.id}`};const t=o(e.text).trim();return t?(r.text=t,n.rows=i,{ok:!0,surface:n}):{ok:!1,error:"文本不能为空(要删这一项请用remove)"}}if("reorder"===t){const r=Array.isArray(e.order)?e.order.map(o):null;if(!r)return{ok:!1,error:"reorder需要order数组"};return i.map(r=>r.id).sort().join("\0")!==[...r].sort().join("\0")?{ok:!1,error:"order必须恰好是当前全部项的一个排列(增删请用对应操作)"}:(n.rows=r.map(r=>i.find(e=>e.id===r)),{ok:!0,surface:n})}return{ok:!1,error:`checklist不支持 ${t}`}}if("setMapping"!==t)return{ok:!1,error:`mapping只支持setMapping,收到 ${t}`};const s=o(e.source),c=r.pairs.map(r=>({...r})),a=c.find(r=>r.source===s);if(!a)return{ok:!1,error:`没有这一列:${s}`};const u=o(e.target).trim();return u&&r.targets?.length&&!r.targets.includes(u)?{ok:!1,error:`「${u}」不在可选目标里`}:(a.target=u,n.pairs=c,{ok:!0,surface:n})}export function setEditing(r,e){return i(r)?{ok:!0,surface:{...r,userEditing:!!e,updatedAt:Date.now()}}:{ok:!1,error:"表面不存在"}}export function modelCanWrite(r){return!!r&&!r.userEditing}export function surfaceToText(r){if(!i(r))return"(表面不存在)";const e=`表面 ${r.id}(${r.kind}${r.title?" · "+r.title:""}) rev=${r.rev}${r.userEditing?" · 用户正在编辑(你现在不能改它)":""}`;if("checklist"===r.kind){const t=r.rows.map(r=>` [${r.done?"x":" "}] ${r.id} ${r.text}`);return`${e}\n已勾选 ${r.rows.filter(r=>r.done).length}/${r.rows.length}\n${t.join("\n")}`}const t=r.pairs.map(r=>` ${r.source} → ${r.target||"(未映射)"}`);return`${e}\n未映射 ${r.pairs.filter(r=>!r.target).length}/${r.pairs.length}`+(r.targets?.length?`\n可选目标:${r.targets.join("、")}`:"")+`\n${t.join("\n")}`}export function surfacesDir(r){return t("surfaces",r)}export function readSurface(t,n){try{const i=e.join(surfacesDir(t),`${sanitizeId(n)}.json`);return JSON.parse(r.readFileSync(i,"utf8"))}catch{return null}}export function listSurfaces(e){try{return r.readdirSync(surfacesDir(e)).filter(r=>r.endsWith(".json")).map(r=>readSurface(e,r.slice(0,-5))).filter(Boolean).sort((r,e)=>(e.updatedAt||0)-(r.updatedAt||0))}catch{return[]}}export function writeSurface(t,n){const i=surfacesDir(t);return r.mkdirSync(i,{recursive:!0,mode:448}),r.writeFileSync(e.join(i,`${sanitizeId(n.id)}.json`),JSON.stringify(n,null,2)+"\n",{mode:384}),n}export function sanitizeId(r){return n(r,"surface",64)}export function makeSurfaceTools({sessionId:r,emit:e}){const t=t=>{writeSurface(r,t);try{e?.(t)}catch{}return t};return[{readOnly:!1,spec:{type:"function",function:{name:"render_surface",description:"渲染一个**用户可以直接改**的表面,并保持与你共享同一份状态。两类:\n· checklist —— 清单:用户可勾选/删行/改文字/拖动排序。「这些行里你要处理哪些」「确认一下待办」用它。\n· mapping —— 字段映射:用户可改每一列对到哪个目标字段。「核对一下表头对应关系」用它。\n与render_ui的区别:那个是**只读呈现**,这个用户能动手改,改完你能读到。\n**不要用它替代ask_ui**:一次性的单个选择用ask_ui,需要来回改的产物才用这个。",parameters:{type:"object",properties:{id:{type:"string",description:"表面id(同id会覆盖重建;用有意义的短名,如rows-to-process)"},kind:{type:"string",enum:SURFACE_KINDS,description:"checklist或mapping"},title:{type:"string",description:"标题(可空)"},items:{type:"array",description:"checklist用:[{id?,text,done?}],也可直接给字符串数组",items:{type:"object",additionalProperties:!0}},columns:{type:"array",description:"mapping用:[{source,target?}]",items:{type:"object",additionalProperties:!0}},targets:{type:"array",description:"mapping用:目标字段的可选值域(给了就只能选这些)",items:{type:"string"}}},required:["id","kind"]}}},run:async e=>{if(listSurfaces(r).length>=20&&!readSurface(r,e?.id))return"本会话的表面已达上限 20 个。复用已有的id,或先让用户处理完现有的。";const n=createSurface(e||{});return n.ok?(t(n.surface),`已渲染表面 ${n.surface.id}。用户现在可以直接改它;改完你用read_surface取最新状态(不要凭记忆假设它没变)。\n\n`+surfaceToText(n.surface)):`表面创建失败:${n.error}`}},{readOnly:!0,spec:{type:"function",function:{name:"read_surface",description:"读一个共享表面的**当前**状态。用户可能已经改过它——在依据表面内容做任何事之前(尤其是「按用户勾选的行去处理」),必须先读一次,不要用你上次渲染时的内容。不给id则列出本会话全部表面。",parameters:{type:"object",properties:{id:{type:"string",description:"表面id;省略则列出全部"}},required:[]}}},run:async e=>{const t=o(e?.id).trim();if(!t){const e=listSurfaces(r);return e.length?e.map(r=>surfaceToText(r)).join("\n\n"):"本会话还没有共享表面。"}const n=readSurface(r,t);return n?surfaceToText(n):`没有这个表面:${t}(用read_surface不带参数可列出全部)`}},{readOnly:!1,spec:{type:"function",function:{name:"update_surface",description:"改一个共享表面(用户在对话里说「把第三项勾掉」时用它,别让用户自己去点)。\n**用户正在编辑该表面时会被拒绝**——这时改口头建议,让用户自己点完再说。",parameters:{type:"object",properties:{id:{type:"string",description:"表面id"},op:{type:"string",enum:EDIT_OPS,description:"操作"},itemId:{type:"string",description:"toggle/remove/setText的目标项id"},done:{type:"boolean",description:"toggle时指定勾选与否;省略则取反"},text:{type:"string",description:"setText的新文本"},order:{type:"array",items:{type:"string"},description:"reorder的新顺序(必须是全部id的排列)"},source:{type:"string",description:"setMapping的源列名"},target:{type:"string",description:"setMapping的目标字段(空串=清空)"}},required:["id","op"]}}},run:async e=>{const n=readSurface(r,o(e?.id));if(!n)return`没有这个表面:${e?.id}`;if(!modelCanWrite(n))return`表面 ${n.id} 正在被用户编辑,你现在不能改它。改用文字说明你的建议,等用户改完再用read_surface看结果。`;const i=applyEdit(n,{...e,id:e?.itemId??e?.id});return i.ok?(t(i.surface),`已更新表面 ${i.surface.id}(rev ${i.surface.rev})。\n\n`+surfaceToText(i.surface)):`修改失败:${i.error}`}}]}export function applyHostEdit(r,e,t){const n=readSurface(r,e);if(!n)return{ok:!1,error:`没有这个表面:${e}`};if(t&&("beginEdit"===t.op||"endEdit"===t.op)){const e=setEditing(n,"beginEdit"===t.op);return e.ok&&writeSurface(r,e.surface),e}const i=applyEdit(n,t);return i.ok&&writeSurface(r,i.surface),i}
|
package/src/tapd.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{mapLimit as e,parseRange as t,weeklySettings as s,ymd as r}from"./worklog.js";const n={stories:{cn:"需求",envelope:"Story",path:"/stories",dateFields:["modified","created","completed","begin","due"],defaultDate:"modified",statusSystem:"story",people:["owner","creator","developer","te","participator"],fields:"id,name,status,priority,owner,creator,developer,created,modified,completed,begin,due,iteration_id,size,effort",title:e=>e.name},bugs:{cn:"缺陷",envelope:"Bug",path:"/bugs",dateFields:["resolved","created","modified","fixed","closed"],defaultDate:"resolved",statusSystem:"bug",people:["fixer","current_owner","reporter","te","de"],fields:"id,title,status,priority,severity,fixer,current_owner,reporter,created,modified,resolved,closed,iteration_id,bugtype",title:e=>e.title},tasks:{cn:"任务",envelope:"Task",path:"/tasks",dateFields:["completed","created","modified","begin","due"],defaultDate:"completed",statusSystem:"",people:["owner","creator"],fields:"id,name,status,priority,owner,creator,created,modified,completed,begin,due,story_id,iteration_id,effort,progress",title:e=>e.name},iterations:{cn:"迭代",envelope:"Iteration",path:"/iterations",dateFields:[],defaultDate:"",span:["startdate","enddate"],statusSystem:"",people:["creator"],fields:"id,name,startdate,enddate,status,creator,created,modified",title:e=>e.name},releases:{cn:"发布计划",envelope:"Release",path:"/releases",dateFields:[],defaultDate:"",span:["startdate","enddate"],statusSystem:"",people:["creator"],fields:"id,name,startdate,enddate,status,creator,created,modified,description",title:e=>e.name}};export function tapdCreds(e=process.cwd()){const t=e&&"object"==typeof e?e:s(e),r=t.tapd&&"object"==typeof t.tapd?t.tapd:{},n=String(process.env.TAPD_ACCESS_TOKEN||r.access_token||"").trim(),i=String(process.env.TAPD_API_USER||r.api_user||"").trim(),a=String(process.env.TAPD_API_PASSWORD||r.api_password||"").trim(),o=String(process.env.TAPD_BASE_URL||r.base_url||"https://api.tapd.cn").trim().replace(/\/+$/,""),c=String(process.env.TAPD_WORKSPACE_ID||"").split(/[,;\s]+/).filter(Boolean),p=Array.isArray(r.workspace_id)?r.workspace_id.map(String):r.workspace_id?String(r.workspace_id).split(/[,;\s]+/).filter(Boolean):[];return{token:n,user:i,pass:a,base:o,workspaces:(c.length?c:p).filter(e=>/^\d+$/.test(e)),nick:String(process.env.TAPD_NICKNAME||r.nickname||"").trim(),mode:n?"token":i&&a?"basic":"",configured:Boolean(n||i&&a)}}const i=["TAPD尚未配置。**推荐用个人访问令牌**(自助生成、可随时吊销,且自带身份——","配了它就不用再填空间ID和昵称,工具会自己问出来):"," ① 在TAPD里打开「个人设置 → 访问令牌(Personal Access Token)」生成一个;"," ② 配置:"," · 环境变量(推荐,令牌不落盘):export TAPD_ACCESS_TOKEN=<你的令牌>"," · 或设置文件 .huicai/weekly.json(项目级;用户级放 ~/.huicai/weekly.json):",' { "tapd": { "access_token": "<你的令牌>" } }'," —— 令牌落盘就有被备份/同步带走的风险,能用环境变量就别写进文件。","也支持公司API账号(Basic,需管理员在开放平台开通):"," TAPD_API_USER / TAPD_API_PASSWORD,或写进weekly.json的tapd.api_user / api_password。"," 用API账号时**要自己配workspace_id**(它问不出「我是谁」)。","私有部署把base_url / TAPD_BASE_URL指到自己的网关。"].join("\n");function a(e){return"token"===e.mode?"Bearer "+e.token:"Basic "+Buffer.from(`${e.user}:${e.pass}`,"utf8").toString("base64")}const o=new Map;async function c(e,t){const s=o.get(e);if(s&&s.until>Date.now())return s.value;const r=await t();return o.set(e,{value:r,until:Date.now()+36e5}),r}async function p(e,t,s={}){const r=new URL(e.base+t);for(const[e,t]of Object.entries(s))null!=t&&""!==t&&r.searchParams.set(e,String(t));let n;try{n=await fetch(r,{headers:{authorization:a(e),accept:"application/json"},signal:AbortSignal.timeout(3e4)})}catch(e){throw new Error(`连不上 ${r.origin}(${"TimeoutError"===e.name?"超时":e.message})——检查网络/代理,或私有部署的base_url是否正确`)}const i=await n.text();if(401===n.status||403===n.status)throw new Error(`凭据被拒(HTTP ${n.status})——`+("token"===e.mode?"个人访问令牌无效或已吊销,去「个人设置 → 访问令牌」重新生成":"API账号或口令不对(那是开放平台的API账号,不是登录密码)"));let o;try{o=JSON.parse(i)}catch{throw new Error(`返回的不是JSON(HTTP ${n.status}):${i.slice(0,200)}`)}if(o&&1!==Number(o.status))throw new Error(`接口报错(status=${o.status}):${o.info||JSON.stringify(o).slice(0,200)}`);return o?.data}export async function whoami(e){return c(`me:${e.base}:${e.mode}`,async()=>{const t=await p(e,"/users/info");return t&&"object"==typeof t?{nick:String(t.nick||t.name||"").trim(),email:String(t.user_email||"").trim(),companyId:String(t.current_company_id||"")}:null})}export async function myProjects(e){return c(`proj:${e.base}:${e.mode}`,async()=>{const t=await p(e,"/workspaces/user_participant_projects");return(Array.isArray(t)?t:[]).map(e=>{const t=e?.Workspace||e||{};return{id:String(t.id||""),name:String(t.name||""),status:String(t.status||"")}}).filter(e=>e.id)})}export async function statusMap(e,t,s){return s?c(`sm:${e.base}:${t}:${s}`,async()=>{try{const r=await p(e,"/workflows/status_map",{workspace_id:t,system:s});return r&&"object"==typeof r&&!Array.isArray(r)?r:{}}catch{return{}}}):{}}async function d(e,t,s,r){const i=n[t],a=[];for(let t=1;t<=10;t++){const n=await p(e,i.path,{workspace_id:s,limit:200,page:t,fields:i.fields,...r}),o=Array.isArray(n)?n:[];for(const e of o){const t=e&&"object"==typeof e?e[i.envelope]||e:null;t&&a.push(t)}if(o.length<200)break;10===t&&(a.truncated=!0)}return a}function l(e,t,s){if(!s.length)return!0;for(const r of t){const t=e[r];if(t)for(const e of String(t).split(/[;,、]/)){const t=e.trim();if(t&&s.some(e=>t===e))return!0}}return!1}export async function fetchKind(e,t,{since:s,until:i,dateField:a,names:o,workspaceId:c,status:p}){const u=n[t],m={};if(p&&(m.status=p),u.span){const n=await d(e,t,c,m);return{items:n.filter(e=>function(e,t,s,n){const i=String(e[t[0]]||"").slice(0,10),a=String(e[t[1]]||"").slice(0,10);if(!i&&!a)return!1;const o=a||i;return(i||a)<=r(n)&&o>=r(s)}(e,u.span,s,i)&&l(e,u.people,o)),total:n.length,truncated:Boolean(n.truncated)}}const f=a||u.defaultDate;m[f]=`${r(s)}~${r(i)}`;const g=await d(e,t,c,m);return{items:g.filter(e=>l(e,u.people,o)),total:g.length,truncated:Boolean(g.truncated),dateField:f}}const u=(e,t)=>e&&e[t]||t||"(空)";function m(e,t,s){const r=n[e];return t.map(e=>{const t=r.people.map(t=>e[t]?String(e[t]).replace(/^;|;$/g,"").replace(/;/g,"/"):"").filter(Boolean)[0]||"",n=[];e.status&&n.push(u(s,e.status)),e.priority&&n.push("优先级 "+e.priority),e.severity&&n.push("严重 "+e.severity),t&&n.push(t);const i=(a=e.resolved||e.completed||e.modified||e.enddate||e.created,String(a||"").slice(0,10));var a;return` #${e.id} ${r.title(e)||"(无标题)"} ${n.join(" · ")}${i?` ${i}`:""}`})}function f(e,t,s){const r=new Map;for(const n of e){const e="status"===t?u(s,n[t]):String(n[t]||"(空)");r.set(e,(r.get(e)||0)+1)}return[...r.entries()].sort((e,t)=>t[1]-e[1])}export function makeTapdTools({cwd:s=process.cwd()}={}){return[{readOnly:!0,spec:{type:"function",function:{name:"tapd_fetch",description:'从TAPD读取本周期内的**需求 / 缺陷 / 任务 / 迭代 / 发布计划**(只读,不写回TAPD)——写周报时用它把「交付了哪些需求、解了哪些缺陷、发了哪个版本」落到真实单号上。\nkind:stories需求 · bugs缺陷 · tasks任务 · iterations迭代 · releases发布计划 · projects我参与的项目列表(**第一次用先调它**,看清有哪些空间、顺便验证凭据通不通)。\n默认时间字段按「周报最常问的那句话」定:需求modified(本周动过的)、缺陷resolved(本周解掉的)、任务completed(本周完成的);要别的口径显式传date_field。迭代与发布计划按其起止区间与汇报周期**相交**判定。\nperson填你在TAPD的**中文昵称**;用个人访问令牌时可直接填 "me",工具会自己问出你是谁。不填则返回整个空间的条目(团队层面的数据这样取)。\n不指定workspace_id时,用令牌自动取回**你参与的全部项目**逐个查(用户不必配空间ID)。\n状态会翻成该空间自己的说法(接口里是status_2 这种自定义代号,直接写进周报等于没写)。\n未配置凭据时不会报错,而是返回一份配置指引——请原样转达给用户。\n**取不到就说取不到**:绝不允许凭空编造需求号、缺陷号或版本号。',parameters:{type:"object",properties:{kind:{type:"string",description:"stories | bugs | tasks | iterations | releases | projects"},workspace_id:{type:"string",description:"TAPD空间ID。不给则用配置里的,再没有就自动取「我参与的全部项目」。"},person:{type:"string",description:'中文昵称;填 "me" 用令牌自动识别的本人。不给则取全空间(团队口径)。'},since:{type:"string",description:"起始日期YYYY-MM-DD(含)"},until:{type:"string",description:"结束日期YYYY-MM-DD(含),默认今天"},days:{type:"number",description:"不给since时:往前N天(含今天),默认 7"},week:{type:"string",description:"this=本自然周 / last=上一自然周"},date_field:{type:"string",description:"按哪个时间字段切窗,见工具说明;不给用该类默认值"},status:{type:"string",description:"可选:按状态过滤(多个用 | 分隔,如resolved|closed)"},detail:{type:"boolean",description:"是否逐条列出,默认true;条目很多时可设false只看统计"}},required:["kind"]}}},run:async r=>{const a=String(r?.kind||"").trim();if("projects"!==a&&!n[a])return`kind应为 ${Object.keys(n).join(" / ")} / projects之一,收到「${r?.kind}」。`;const o=tapdCreds(s);if(!o.configured)return i;const c="token"===o.mode?"个人访问令牌":"公司API账号";let p,d=null;try{d=await whoami(o)}catch{}if("projects"===a){let e;try{e=await myProjects(o)}catch(e){return`【TAPD项目列表】读取失败 —— ${e.message}\n(认证方式:${c})`}const t=[`【TAPD我参与的项目】共 ${e.length} 个 认证:${c}`];d&&t.push(`当前身份:${d.nick}${d.email?`(${d.email})`:""}${d.companyId?` 公司 ${d.companyId}`:""}`);for(const s of e)t.push(` ${s.id} ${s.name}${s.status&&"normal"!==s.status?` [${s.status}]`:""}`);return t.push("把常用的几个写进 .huicai/weekly.json的tapd.workspace_id可以少查几个空间;不写就默认全查(周报要的本来就是「我参与的全部」)。"),t.join("\n")}try{p=t(r||{})}catch(e){return"时间段有误:"+e.message}let l=r?.workspace_id?[String(r.workspace_id)]:o.workspaces,u=r?.workspace_id?"本次调用指定":"配置tapd.workspace_id",g=new Map;if(!l.length)try{const e=await myProjects(o);l=e.map(e=>e.id),g=new Map(e.map(e=>[e.id,e.name])),u="令牌自动取回「我参与的项目」"}catch(e){return`没有指定workspace_id,自动取项目列表也失败了 —— ${e.message}\n请传workspace_id,或写进 .huicai/weekly.json的tapd.workspace_id / 环境变量TAPD_WORKSPACE_ID。`}if(!l.length)return"没有可查的TAPD空间:账号下一个参与的项目都没有,或凭据对应的用户不在任何空间里。";let y=String(r?.person||"").trim();if("me"===y&&(y=d&&d.nick||o.nick),r?.person&&!y)return'按人筛选需要你在TAPD里的**中文昵称**,但令牌问不出身份、设置里也没有nickname。\n传person="张三",或设环境变量TAPD_NICKNAME / 在 .huicai/weekly.json里写tapd.nickname。';const h=y?y.split(/[,、;]/).map(e=>e.trim()).filter(Boolean):[],w=n[a],$=[];$.push(`【TAPD ${w.cn}】${p.label}(${p.source}) 认证:${c}`),d&&$.push(`当前身份:${d.nick}${d.email?`(${d.email})`:""}`),$.push(`空间:${l.length} 个(${u}) 范围:${h.length?`仅「${h.join("/")}」`:"全空间(未按人筛,团队口径)"}`);const k=await e(l,5,async e=>{try{const t=await fetchKind(o,a,{since:p.since,until:p.until,dateField:r?.date_field,names:h,workspaceId:e,status:r?.status});return{ws:e,r:t,smap:t.items.length?await statusMap(o,e,w.statusSystem):{}}}catch(t){return{ws:e,error:t.message}}});let _=[],A=0,S={};const D=[];for(const{ws:e,r:t,smap:s,error:n}of k){const i=g.get(e)?`${e} ${g.get(e)}`:e;n?(A++,$.push(`■ 空间 ${i}:读取失败 —— ${n}`)):t.items.length?(S=s&&Object.keys(s).length?s:S,$.push(""),$.push(`■ 空间 ${i}:命中 ${t.items.length} 条`+(h.length?`(该空间本周期共 ${t.total} 条,按人筛后剩这些)`:"")+(t.dateField?` 时间口径:${t.dateField}`:" 口径:起止区间与汇报周期相交")+(t.truncated?" ⚠ 已达翻页上限 2000 条,可能没取全,请收窄时间段或加status过滤":"")),$.push(" 按状态:"+f(t.items,"status",s).map(([e,t])=>`${e} ${t}`).join(" · ")),!1!==r?.detail&&$.push(...m(a,t.items,s)),_=_.concat(t.items)):D.push(i)}return $.push(""),D.length&&$.push(`(${D.length} 个空间本周期无命中:${D.slice(0,8).join("、")}${D.length>8?" …":""})`),A===l.length?($.push("【结果】所有空间都没读到数据——上面每条都写明了原因,请照着排查后重试,**不要**在没有数据的情况下往周报里填需求号。"),$.join("\n")):($.push(`【合计】${w.cn} ${_.length} 条`+(_.length?" 状态分布:"+f(_,"status",S).map(([e,t])=>`${e} ${t}`).join(" · "):"")),_.length||$.push(`本周期内没有命中任何条目。先别下结论——按顺序核对:① 时间口径(现在按 ${r?.date_field||w.defaultDate||"区间相交"} 算,换一个字段试试);② 昵称是否与TAPD里显示的完全一致;③ 空间是否是本项目的(kind:"projects" 可以列出来看)。核对完仍为空,就如实写「本周期该系统内无记录」。`),$.push('【口径提醒】状态已按各空间自定义的说法翻译;条目标题往往就是可以直接引用的工作项名称。需求「动过」不等于「交付了」——要写交付,用date_field:"completed" 另查一遍。'),$.join("\n"))}}]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"node:readline";import{PassThrough as t}from"node:stream";import{stripVTControlCharacters as n}from"node:util";import{initialSelection as r,selectionKey as o,insertSelectionText as s}from"./selection.js";const i=e=>n(String(e??"")).replace(/[\x00-\x1f\x7f-\x9f]/g," "),a=new Intl.Segmenter(void 0,{granularity:"grapheme"});function l(e){let t=0;for(const{segment:n}of a.segment(e)){const e=n.replace(/[\p{Mark}\p{Default_Ignorable_Code_Point}]/gu,"");if(!e)continue;const r=e.codePointAt(0);t+=/\p{Emoji_Presentation}|\uFE0F|\u20E3/u.test(n)||r>=4352&&(r<=4447||9001===r||9002===r||r>=11904&&r<=42191||r>=44032&&r<=55203||r>=63744&&r<=64255||r>=65040&&r<=65049||r>=65072&&r<=65135||r>=65280&&r<=65376||r>=65504&&r<=65510||r>=131072&&r<=262141)?2:1}return t}export function terminalSelect(c,{rl:u,input:m=process.stdin,output:p=process.stdout,signal:f,onInterrupt:d}={}){return!m.isTTY||!p.isTTY||f?.aborted||u?.closed?Promise.resolve(null):new Promise(g=>{let h,w=r(c),x=0,b=!1,v=!1,y="";const M=m.isRaw,k=m.listeners("keypress");for(const e of k)m.off("keypress",e);const A=new t;e.emitKeypressEvents(A);const $=()=>w.kind||"segments"===w.layout?w.options:w.options.filter(e=>[e.label,e.value,e.description].join(" ").toLowerCase().includes(w.query.toLowerCase())),T=()=>{x&&(e.moveCursor(p,0,-x),e.cursorTo(p,0),e.clearScreenDown(p))},j=()=>{T();const e=Math.max(8,(p.columns||80)-1),t=$(),n=Math.max(0,Math.min(w.index,t.length-1)),r=Math.min(4,Math.max(1,(p.rows||24)-5)),o=Math.max(0,Math.min(n-Math.floor(r/2),t.length-r)),s=[`╭─ ${w.title}${w.editing?"":` ${t.length?n+1:0}/${t.length}`}`];if(w.editing)s.push("│ › "+[...a.segment(w.answerText.slice(0,w.cursor))].slice(-Math.floor(e/2)).map(e=>e.segment).join("")+"▏"+w.answerText.slice(w.cursor));else if("segments"===w.layout){let r=n,o=n+1;const i=(e,t)=>`${t===n?"❯ ":""}${e.label}${e.value===w.current?" ✓":""}`;for(;r>0&&l(t.slice(r-1,o).map((e,t)=>i(e,r-1+t)).join(" │ "))<e-8;)r--;for(;o<t.length&&l(t.slice(r,o+1).map((e,t)=>i(e,r+t)).join(" │ "))<e-8;)o++;s.push("│ "+(r?"‹ ":"")+t.slice(r,o).map((e,t)=>i(e,r+t)).join(" │ ")+(o<t.length?" ›":""))}else t.length?s.push(...t.slice(o,o+r).map((e,t)=>`│ ${o+t===n?"❯":" "} ${w.multiple&&!e.custom?w.selected.includes(e.value)?"☑ ":"☐ ":""}${e.label}${w.kind||e.value!==w.current?"":" ✓"}`)):s.push("│ 没有匹配项;退格清除搜索");!w.editing&&t[n]?.description&&(p.rows||24)>=9&&s.push("│ "+t[n].description),s.push(w.editing?"╰─ Enter 提交 · Esc 返回":`╰─ ${"segments"===w.layout?"←→":"↑↓"} 选择${w.multiple?" · 空格 勾选":""} · Enter 确认 · Esc 取消`),p.write(s.map(t=>function(e,t){let n="";for(const{segment:r}of a.segment(i(e))){if(l(n+r)>t)break;n+=r}return n}(t,e)).join("\n")+"\n"),x=s.length},E=e=>{if(!b){b=!0,clearTimeout(h),m.off("data",L),p.off("resize",j),u?.off("close",C),f?.removeEventListener("abort",C),A.removeAllListeners("keypress"),A.destroy(),T(),p.write("[?2004l[?25h");for(const e of k)m.on("keypress",e);m.setRawMode?.(!!M),g(e)}},C=()=>E(null);A.on("keypress",(e="",t={})=>{if(b)return;if(t.ctrl&&"c"===t.name)return E(null),void d?.();const n={...t,return:"return"===t.name,escape:"escape"===t.name,upArrow:"up"===t.name,downArrow:"down"===t.name,leftArrow:"left"===t.name,rightArrow:"right"===t.name,pageUp:"pageup"===t.name,pageDown:"pagedown"===t.name,tab:"tab"===t.name,backspace:"backspace"===t.name,delete:"delete"===t.name};if(w.kind){const r=o(w,t.ctrl?t.name:e,n);if("result"in r)return void E(r.result);w=r.menu}else{if(n.escape)return void E(null);const r=$();if(n.return)return void(r[w.index]&&E(r[w.index].value));if(n.upArrow||n.downArrow||n.pageUp||n.pageDown||n.tab||"segments"===w.layout&&(n.leftArrow||n.rightArrow)){const e=n.upArrow||n.pageUp||n.leftArrow||n.tab&&t.shift?-1:1;w.index=Math.max(0,Math.min(r.length-1,w.index+e*(n.pageUp||n.pageDown?4:1)))}else t.ctrl||t.meta||n.leftArrow||n.rightArrow||(w.query=n.backspace||n.delete?[...w.query].slice(0,-1).join(""):w.query+i(e),w.index="segments"===w.layout?Math.max(0,r.findIndex(e=>[e.label,e.value].join(" ").toLowerCase().includes(w.query.toLowerCase()))):0)}j()});const L=e=>{for(clearTimeout(h),y+=e.toString();y&&!b;){const e=v?"[201~":"[200~",t=y.indexOf(e);if(!(t>=0)){let t=0;for(let n=Math.min(e.length-1,y.length);n>0;n--)if(e.startsWith(y.slice(-n))){t=n;break}const n=t?y.slice(0,-t):y;y=t?y.slice(-t):"",v?w.editing&&(w=s(w,n),j()):A.write(n),!v&&y&&(h=setTimeout(()=>{const e=y;y="",b||A.write(e)},60));break}{const n=y.slice(0,t);y=y.slice(t+e.length),v?w.editing&&(w=s(w,n)):A.write(n),v=!v,v||b||j()}}};c.context&&p.write(n(String(c.context)).replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g,"")+"\n"),m.setRawMode?.(!0),p.write("[?2004h[?25l"),m.on("data",L),p.on("resize",j),u?.once("close",C),f?.addEventListener("abort",C,{once:!0}),j()})}
|
package/src/terminal.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"node:fs";import t from"node:os";import n from"node:path";import{fileURLToPath as r}from"node:url";import{confineArgv as i}from"./sandbox.js";const s=2e5;export const DEFAULT_WAIT_MS=3e4;export const MAX_WAIT_MS=3e5;const o=2e4,a=new Set(["SIGINT","SIGTERM","SIGQUIT","SIGTSTP","SIGHUP"]),c={SIGINT:"",SIGQUIT:"",SIGTSTP:""},l=new Map;let u,d=0,p=null;export function pickShell(){return"win32"===process.platform?[process.env.COMSPEC,"powershell.exe","cmd.exe"].find(Boolean)||"cmd.exe":[process.env.SHELL,"/bin/zsh","/bin/bash","/usr/bin/bash","/bin/sh"].find(t=>{try{return!!t&&e.existsSync(t)}catch{return!1}})||"/bin/sh"}export function ensureSpawnHelperExecutable(t){try{const i=n.dirname(r(t)),s=`${process.platform}-${process.arch}`;for(const t of[`../prebuilds/${s}/spawn-helper`,"../build/Release/spawn-helper"]){const r=n.resolve(i,t);if(!e.existsSync(r))continue;const s=511&e.statSync(r).mode;if(73&s)return;return void e.chmodSync(r,493|s)}}catch{}}async function f(){if(void 0!==u)return u;try{u=await import("node-pty"),"win32"!==process.platform&&ensureSpawnHelperExecutable(import.meta.resolve("node-pty"))}catch{u=null}return u}export function ptyLaunchArgv(e,t,n){const r=i(e,t,n);return{argv:r.argv,confined:!!r.sandbox,note:r.note}}async function m({cwd:n,cols:r,rows:i,config:s}){const o=await f();if(!o)return null;let a=n;try{a&&e.statSync(a).isDirectory()||(a=t.homedir())}catch{a=t.homedir()}const c="win32"===process.platform,l=pickShell(),u=c?[...new Set([l,"powershell.exe","cmd.exe"])].map(e=>[e,[]]):[[l,["-l"]],["/bin/sh",[]]];let d=null,p="",m=!1;for(const[e,t]of u)try{const n=ptyLaunchArgv([e,...t],s,a);m=n.confined,I(),d=o.spawn(n.argv[0],n.argv.slice(1),{name:"xterm-256color",cwd:a,cols:r,rows:i,env:{...process.env,TERM:"xterm-256color"}});try{d._socket?.unref?.()}catch{}break}catch(e){p=e?.message||String(e)}if(!d)throw new Error(`终端启动失败(shell=${l},目录=${a}):${p||"posix_spawnp failed"}`);return{pid:d.pid,shell:l,cwd:a,confined:m,startupSettle:8e3,write:e=>d.write(e),resize:(e,t)=>{try{d.resize(e,t)}catch{}},kill:e=>{try{d.kill(e)}catch{}},onData:e=>d.onData(e),onExit:e=>d.onExit(({exitCode:t})=>e(t))}}export function setBackend(e){p="function"==typeof e?e:null}export async function isAvailable(){return!!p||null!==await f()}function w(e){return{id:e.id,name:e.name,cwd:e.cwd,shell:e.shell,pid:e.pid,status:e.status,exitCode:e.exitCode,confined:!!e.confined,sessionId:e.sessionId,startedAt:e.startedAt,bytes:e.bytes}}function h(e,t){return e&&e.sessionId===String(t||"")}export async function open({sessionId:e,cwd:t,name:n,cols:r=120,rows:i=30,config:o}={}){const a=p||m;let c;try{c=await a({cwd:t,cols:r,rows:i,config:o})}catch(e){return{ok:!1,error:e.message}}if(!c)return{ok:!1,error:"终端组件不可用(node-pty未安装或ABI不匹配)。"};const u={id:"term-"+ ++d,name:String(n||"").slice(0,40),sessionId:String(e||""),cwd:c.cwd||t||process.cwd(),shell:c.shell||"",pid:c.pid,handle:c,buf:"",bytes:0,cursor:0,status:"live",exitCode:null,confined:!!c.confined,startedAt:Date.now(),lastDataAt:Date.now(),sawOutput:!1,startupSettled:!1,startupWaitMs:Number(c.startupSettle)||0,gotAnyData:!1,sinceWrite:null,pendingEcho:"",waiters:new Set};return c.onData(e=>{!function(e,t){const n=String(t);if(e.buf+=n,e.bytes+=n.length,e.buf.length>s){const t=e.buf.length-s;e.buf=e.buf.slice(t),e.cursor=Math.max(0,e.cursor-t)}e.lastDataAt=Date.now(),e.sawOutput=!0,e.gotAnyData=!0,null!==e.sinceWrite&&(e.sinceWrite+=n)}(u,e)}),c.onExit(e=>{u.status="exited",u.exitCode=e;for(const e of[...u.waiters])try{e("exited")}catch{}}),l.set(u.id,u),{ok:!0,view:w(u)}}function g(e,t){return new Promise(n=>{let r=!1;const i=t=>{r||(r=!0,clearTimeout(s),clearInterval(o),e.waiters.delete(i),n(t))},s=setTimeout(()=>i("timeout"),t),o=setInterval(()=>{if("exited"===e.status)return i("exited");const t=Date.now()-e.lastDataAt;return!e.sawOutput||t<400?void 0:function(e){if(null===e.sinceWrite)return!0;const t=cleanForModel(e.sinceWrite),n=cleanForModel(e.pendingEcho).trim();return(n?t.replace(n,""):t).trim().length>0}(e)?i("quiet"):void(t>=2e3&&i("quiet"))},50);e.waiters.add(i)})}const y=String.fromCharCode(27);export function cleanForModel(e){const t=String(e||""),n=[];let r="",i=0;const s=e=>{r=i<r.length?r.slice(0,i)+e+r.slice(i+1):r.padEnd(i," ")+e,i++};for(let e=0;e<t.length;e++){const o=t[e];if(o===y){const n=t[e+1];if("["===n){let n=e+2;for(;n<t.length&&/[0-9;?]/.test(t[n]);)n++;for(;n<t.length&&/[ -/]/.test(t[n]);)n++;const s=t[n],o=parseInt(t.slice(e+2,n).replace(/[^0-9]/g,""),10)||1;if("D"===s)i=Math.max(0,i-o);else if("C"===s)i+=o;else if("G"===s)i=Math.max(0,o-1);else if("K"===s){const s=parseInt(t.slice(e+2,n)||"0",10);0===s?r=r.slice(0,i):1===s?r=" ".repeat(i)+r.slice(i):(r="",i=0)}e=n;continue}if("]"===n){let n=e+2;for(;n<t.length&&""!==t[n]&&(t[n]!==y||"\\"!==t[n+1]);)n++;e=t[n]===y?n+1:n;continue}if("("===n||")"===n||"#"===n||"%"===n||"*"===n||"+"===n){e+=2;continue}e+=1;continue}"\n"!==o?"\r"!==o?"\b"!==o?""!==o&&s(o):i=Math.max(0,i-1):i=0:(n.push(r),r="",i=0)}return n.push(r),n.map(e=>e.replace(/\s+$/,"")).join("\n")}function x(e,{all:t=!1}={}){const n=t?0:Math.min(e.cursor,e.buf.length);let r=cleanForModel(e.buf.slice(n));return t||(e.cursor=e.buf.length),r.length>o&&(r=`…(已省略前 ${r.length-o} 字符,只显示最新部分)…\n`+r.slice(-2e4)),r}export async function send(e,{sessionId:t,text:n,submit:r=!0,wait:i=!0,timeoutMs:s}={}){const o=l.get(String(e||""));if(!h(o,t))return{ok:!1,error:`未找到终端 ${e}(或它不属于当前会话)`};if("live"!==o.status)return{ok:!1,error:`终端 ${e} 已退出(退出码 ${o.exitCode}),请用terminal_open新开一个。`};if(!1!==i&&(await async function(e){if(e.startupSettled||!e.startupWaitMs)return;const t=Date.now()+e.startupWaitMs;for(;"live"===e.status&&Date.now()<t&&(!e.gotAnyData||Date.now()-e.lastDataAt<250);)await new Promise(e=>setTimeout(e,25));e.startupSettled=!0}(o),"live"!==o.status))return{ok:!1,error:`终端 ${e} 在准备就绪前就结束了(退出码 ${o.exitCode}),请用terminal_open重开。`};o.sawOutput=!1,o.sinceWrite="",o.pendingEcho=String(n??"");try{o.handle.write(String(n??"")+(r?"\r":""))}catch(e){return{ok:!1,error:"写入终端失败:"+e.message}}if(!i)return{ok:!0,why:"nowait",text:"",view:w(o)};const a=Math.min(Math.max(Number(s)||3e4,500),3e5),c=await g(o,a);return o.sinceWrite=null,{ok:!0,why:c,text:x(o),view:w(o)}}export function read(e,{sessionId:t,all:n=!1}={}){const r=l.get(String(e||""));return h(r,t)?{ok:!0,text:x(r,{all:n}),view:w(r)}:{ok:!1,error:`未找到终端 ${e}(或它不属于当前会话)`}}export function signal(e,{sessionId:t,signal:n}={}){const r=l.get(String(e||""));if(!h(r,t))return{ok:!1,error:`未找到终端 ${e}(或它不属于当前会话)`};const i=String(n||"").toUpperCase();if("SIGKILL"===i)return{ok:!1,error:"不能对终端本身发SIGKILL(那会留下孤儿进程)。要结束它请用terminal_close;要中断当前命令用SIGINT。"};if(!a.has(i))return{ok:!1,error:`不支持的信号 ${n}。可用:${[...a].join(" / ")}`};if("live"!==r.status)return{ok:!1,error:`终端 ${e} 已退出。`};const s=c[i];if(!s)return{ok:!1,error:`${i} 会终结整个终端(连同里面攒下的目录、变量、还开着的REPL),而不是打断当前命令。要打断正在跑的命令用SIGINT;确实要结束这个终端请用terminal_close。`};try{r.handle.write(s)}catch(e){return{ok:!1,error:"递送信号失败:"+e.message}}return{ok:!0,view:w(r)}}export function close(e,{sessionId:t}={}){const n=l.get(String(e||""));if(!h(n,t))return{ok:!1,error:`未找到终端 ${e}(或它不属于当前会话)`};try{n.handle.kill("SIGKILL")}catch{}n.status="exited";for(const e of[...n.waiters])try{e("exited")}catch{}return l.delete(n.id),{ok:!0}}export function list(e){return[...l.values()].filter(t=>h(t,e)).map(w)}let S=!1;function I(){S||(S=!0,process.once("exit",()=>{for(const e of[...l.values()])try{e.handle.kill("SIGKILL")}catch{}l.clear()}))}export function closeAllTerminals(){for(const e of[...l.values()])v(e);l.clear()}function v(e){try{e.handle.kill("SIGKILL")}catch{}e.status="exited";for(const t of[...e.waiters||[]])try{t("exited")}catch{}}export function dropSessionTerminals(e){const t=String(e||"");for(const e of[...l.values()])e.sessionId===t&&(v(e),l.delete(e.id))}export function resetTerminals(){for(const e of[...l.values()])v(e);l.clear(),d=0}const b=(e,t,n={},r=[])=>({type:"function",function:{name:e,description:t,parameters:{type:"object",properties:n,required:r}}});export async function makeTerminalTools({sessionId:e,cwd:t,config:n}={}){if(!await isAvailable())return[];const r="function"==typeof e?()=>String(e()||""):()=>String(e||"");return[{readOnly:!1,spec:b("terminal_open","开一个**状态保持**的终端会话:cwd、环境变量、shell函数在多次调用之间都保留。用于交互式调试(gdb/pdb)、REPL(python/node/psql)、ssh、需要逐步喂输入的向导,以及「先source环境再连续跑多条命令」。一次性命令仍用run_bash更省事。",{name:{type:"string",description:'可选:给它起个名字便于区分(如 "调试"、"dev-server")'},cwd:{type:"string",description:"可选:初始工作目录,默认当前项目目录"}}),run:async e=>{const i=await open({sessionId:r(),cwd:e?.cwd||t,name:e?.name,config:n});if(!i.ok)return"无法开启终端:"+i.error;const s=i.view.confined?"(沙箱已生效:只能写工作区与临时目录)":"";return`已开启终端 ${i.view.id}(shell=${i.view.shell},目录=${i.view.cwd})${s}。\n用terminal_send发命令,terminal_read读输出,terminal_signal发Ctrl-C,terminal_close关闭。`}},{readOnly:!1,spec:b("terminal_send","向持久终端发送一行文本(默认按回车提交),等它输出安静下来后返回这段新增输出。需要输入控制字符或喂REPL的半截输入时设submit=false。对一直刷屏不停的命令(如tail -f、dev server),到硬超时就返回,命令仍在跑——之后用terminal_read继续读。",{terminal_id:{type:"string",description:"terminal_open返回的终端id"},text:{type:"string",description:"要发送的文本"},submit:{type:"boolean",description:"是否追加回车提交(默认true)"},wait:{type:"boolean",description:"是否等输出安静后再返回(默认true;false表示发完立刻返回)"},timeout_ms:{type:"number",description:"最长等待毫秒,默认 30000,上限 300000"}},["terminal_id","text"]),timeoutMs:33e4,run:async e=>{const t=await send(e?.terminal_id,{sessionId:r(),text:e?.text,submit:!1!==e?.submit,wait:!1!==e?.wait,timeoutMs:e?.timeout_ms});if(!t.ok)return t.error;if("nowait"===t.why)return`已发送到 ${e.terminal_id}(未等待)。用terminal_read读输出。`;const n="timeout"===t.why?"\n(已到等待上限,命令**仍在运行**;继续用terminal_read读后续输出,或用terminal_signal发SIGINT中断它)":"exited"===t.why?`\n(终端已退出,退出码 ${t.view.exitCode})`:"";return(t.text||"(无输出)")+n}},{readOnly:!0,spec:b("terminal_read","读取持久终端的输出,不发送任何输入。默认只返回上次读取之后的新增内容;all=true读全部保留内容。",{terminal_id:{type:"string",description:"终端id"},all:{type:"boolean",description:"是否返回全部保留输出而非仅新增(默认否)"}},["terminal_id"]),run:async e=>{const t=read(e?.terminal_id,{sessionId:r(),all:!!e?.all});return t.ok?t.text||("live"===t.view.status?"(暂无新增输出)":`(终端已退出,退出码 ${t.view.exitCode})`):t.error}},{readOnly:!1,spec:b("terminal_signal","向终端当前前台进程递送信号。最常用SIGINT(等价Ctrl-C)中断跑飞的命令。注意:不能对终端本身发SIGKILL,要结束整个终端请用terminal_close。",{terminal_id:{type:"string",description:"终端id"},signal:{type:"string",enum:[...a],description:"要递送的信号"}},["terminal_id","signal"]),run:async e=>{const t=signal(e?.terminal_id,{sessionId:r(),signal:e?.signal});return t.ok?`已向 ${e.terminal_id} 递送 ${String(e.signal).toUpperCase()}。`:t.error}},{readOnly:!1,spec:b("terminal_close","关闭一个持久终端并结束它的进程。",{terminal_id:{type:"string",description:"终端id"}},["terminal_id"]),run:async e=>{const t=close(e?.terminal_id,{sessionId:r()});return t.ok?`已关闭终端 ${e.terminal_id}。`:t.error}},{readOnly:!0,spec:b("terminal_list","列出本会话开着的持久终端及其状态、工作目录。"),run:async()=>{const e=list(r());return e.length?"本会话的持久终端:\n"+e.map(e=>"· "+(e=>`${e.id}${e.name?"("+e.name+")":""} [${"live"===e.status?"存活":"已退出"}]${e.confined?"[沙箱]":""} ${e.cwd}`)(e)).join("\n"):"当前会话没有打开的持久终端(用terminal_open开一个)。"}}]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"node:fs";import t from"node:path";import s from"node:os";import{captureCommand as n}from"./autofix.js";const r=[{id:"maven",detect:s=>e.existsSync(t.join(s,"pom.xml")),command:e=>"mvn -q -B test"+(e?` -Dtest=${o(e)}`:""),reports:["**/target/surefire-reports","**/target/failsafe-reports"],format:"junit-xml"},{id:"gradle",detect:s=>e.existsSync(t.join(s,"build.gradle"))||e.existsSync(t.join(s,"build.gradle.kts")),command:e=>("win32"===process.platform?"gradlew.bat":"./gradlew")+" test"+(e?` --tests ${o(e)}`:""),reports:["**/build/test-results/test"],format:"junit-xml"},{id:"pytest",detect:s=>e.existsSync(t.join(s,"pytest.ini"))||e.existsSync(t.join(s,"tox.ini"))||e.existsSync(t.join(s,"conftest.py"))||e.existsSync(t.join(s,"pyproject.toml"))&&/\[tool\.pytest/.test(i(t.join(s,"pyproject.toml")))||a(s,e=>/^test_.*\.py$|_test\.py$/.test(e)),command:(e,t)=>`pytest -q --junit-xml=${o(t)}`+(e?` -k ${o(e)}`:""),format:"junit-xml",ownXml:!0},{id:"node",detect:s=>{const n=function(t){try{return JSON.parse(e.readFileSync(t,"utf8"))}catch{return null}}(t.join(s,"package.json"));return!!(n&&n.scripts&&n.scripts.test)},command:e=>"npm test --silent"+(e?` -- --test-name-pattern=${o(e)}`:""),format:"tap"}];function i(t){try{return e.readFileSync(t,"utf8")}catch{return""}}function a(s,n,r=2){try{for(const i of e.readdirSync(s,{withFileTypes:!0}))if(!i.name.startsWith(".")&&"node_modules"!==i.name){if(i.isFile()&&n(i.name))return!0;if(i.isDirectory()&&r>0&&a(t.join(s,i.name),n,r-1))return!0}}catch{}return!1}function o(e){const t=String(e);return/^[\w./:@=-]+$/.test(t)?t:`'${t.replace(/'/g,"'\\''")}'`}export function detectRunner(e,t){return t?r.find(e=>e.id===t)||null:r.find(t=>{try{return t.detect(e)}catch{return!1}})||null}const c=/<testcase\b([^>]*?)(\/>|>([\s\S]*?)<\/testcase>)/g,l=/(\w[\w:.-]*)\s*=\s*"([^"]*)"/g,p=/<(failure|error)\b([^>]*?)(?:\/>|>([\s\S]*?)<\/\1>)/;function u(e){const t={};let s;for(l.lastIndex=0;s=l.exec(e);)t[s[1]]=d(s[2]);return t}function d(e){return String(e).replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'").replace(/ /g,"\n").replace(/	/g,"\t").replace(/&/g,"&")}export function parseJUnitXml(e,t){let s;for(c.lastIndex=0;s=c.exec(e);){const e=u(s[1]),n=s[3]||"";if(t.total+=1,t.timeMs+=Math.round(1e3*Number(e.time||0)),/<skipped\b/.test(n)){t.skipped+=1;continue}const r=p.exec(n);if(!r){t.passed+=1;continue}const i=u(r[2]||"");t.failed+=1,t.failures.push({suite:e.classname||e.class||"",name:e.name||"(未命名用例)",kind:"error"===r[1]?"报错":"断言失败",message:(i.message||"").trim(),type:i.type||"",stack:d(r[3]||"").trim()})}return t}function m(s,n=[],r=6){let i=[];try{i=e.readdirSync(s,{withFileTypes:!0})}catch{return n}for(const e of i){const i=t.join(s,e.name);if(e.isDirectory()){if("node_modules"===e.name||e.name.startsWith("."))continue;r>0&&m(i,n,r-1)}else(/^TEST-.*\.xml$/.test(e.name)||/\.xml$/.test(e.name))&&n.push(i)}return n}function f(s,n=4){const r=[],i=(s,n)=>{let a=[];try{a=e.readdirSync(s,{withFileTypes:!0})}catch{return}for(const e of a){if(!e.isDirectory()||e.name.startsWith(".")||"node_modules"===e.name)continue;const a=t.join(s,e.name);"surefire-reports"===e.name||"failsafe-reports"===e.name||"test-results"===e.name?r.push(a):n>0&&i(a,n-1)}};return i(s,n),r}const y=/^[✔✓]\s+(.+?)\s*\(\d[\d.]*\s*ms\)\s*$/,x=/^[✖✗×]\s+(.+?)\s*\(\d[\d.]*\s*ms\)\s*$/;export function parseNodeSpec(e,t){const s=String(e||""),n=s.split("\n"),r=e=>{const t=new RegExp(`^\\s*[ℹi]\\s+${e}\\s+(\\d+)\\s*$`,"m").exec(s);return t?Number(t[1]):null},i=r("pass"),a=r("fail"),o=r("skipped");if(null!==i||null!==a)t.passed+=i||0,t.failed+=a||0,t.skipped+=o||0,t.total+=(i||0)+(a||0)+(o||0);else for(const e of n){const s=e.trim();y.test(s)?(t.total+=1,t.passed+=1):x.test(s)&&(t.total+=1,t.failed+=1)}const c=n.findIndex(e=>/failing tests:/.test(e));if(c<0)return t;let l=null;const p=new Set;for(const e of n.slice(c+1)){const s=e.trim(),n=x.exec(s);if(n){if(p.has(n[1])){l=null;continue}p.add(n[1]),l={suite:"",name:n[1],kind:"断言失败",message:"",type:"",stack:""},t.failures.push(l);continue}if(!l)continue;const r=/^actual:\s*(.+?),?$/.exec(s),i=/^expected:\s*(.+?),?$/.exec(s);r?l.actual=r[1]:i?l.expected=i[1]:/Error|AssertionError/.test(s)&&!l.message?l.message=s:(/^at\s+/.test(s)||/^test at /.test(s))&&(l.stack+=(l.stack?"\n":"")+s)}return t}export function parseTap(e,t){const s=String(e||"").split("\n");let n=null;for(const e of s){const s=e.replace(/^\s*/,""),r=/^ok\s+\d+\s*-?\s*(.*)$/.exec(s),i=/^not ok\s+\d+\s*-?\s*(.*)$/.exec(s);if(r)t.total+=1,/#\s*SKIP/i.test(r[1])?t.skipped+=1:t.passed+=1,n=null;else if(i)t.total+=1,t.failed+=1,n={suite:"",name:i[1].trim(),kind:"断言失败",message:"",type:"",stack:""},t.failures.push(n);else if(n){/^(?:error|expected|actual|operator):\s*(.*)$/i.exec(s)&&!n.message?n.message=s.trim():/^at\s+/.test(s)&&(n.stack+=(n.stack?"\n":"")+s.trim())}const a=/^#\s*(pass|fail|skipped)\s+(\d+)$/.exec(s);a&&0===t.total&&("pass"===a[1]&&(t.passed=Number(a[2])),"fail"===a[1]&&(t.failed=Number(a[2])),"skipped"===a[1]&&(t.skipped=Number(a[2])),t.total=t.passed+t.failed+t.skipped)}return t}const g=new RegExp(["at\\s+(java|javax|jdk|sun|com\\.sun|kotlin)\\.","org\\.(junit|mockito|hamcrest|assertj|testng|springframework|apache\\.maven|gradle)","surefire|failsafe|\\breflect\\b|jdk\\.internal","pytest|_pytest|site-packages|unittest[/\\\\.]","node:internal|node_modules|internal/test_runner","System\\.(Runtime|Threading|Reflection)|xunit|nunit|Microsoft\\.VisualStudio\\.TestPlatform"].join("|"),"i"),$=/([Tt]ests?\.(java|py|cs)|_test\.py|test_\w+\.py|\.test\.[cm]?[jt]s|\.spec\.[cm]?[jt]s)/;export function locateSourceFrame(e){let t=null;for(const s of String(e||"").split("\n")){const e=s.trim();if(!e||g.test(e))continue;let n=/\(([\w./$-]+\.(?:java|kt)):(\d+)\)/.exec(e);if(n||(n=/File\s+"([^"]+\.py)",\s+line\s+(\d+)/.exec(e)),n||(n=/^([\w./$-]+\.py):(\d+):/.exec(e)),n||(n=/\(?([\w./$-]+\.[cm]?[jt]sx?):(\d+)(?::\d+)?\)?$/.exec(e)),!n)continue;const r={file:n[1],line:Number(n[2])};if(!$.test(n[1]))return{...r,isTest:!1};t||(t={...r,isTest:!0})}return t}export function extractAssertion(e){const t=String(e||"").split("\n")[0];let s=/expected:?\s*<?(.+?)>?\s+but\s+(?:was|got):?\s*<?(.+?)>?\s*$/is.exec(t);const n=e=>String(e).trim().replace(/\s+/g," ").replace(/[.,;]$/,"").slice(0,160);return s?{expected:n(s[1]),actual:n(s[2])}:(s=/Expected:\s*(.+?)[\r\n]+\s*(?:Actual|but was):\s*(.+?)$/is.exec(t),s?{expected:n(s[1]),actual:n(s[2])}:(s=/\bassert\s+(.+?)\s*==\s*(.+?)\s*$/is.exec(t),s?{expected:n(s[2]),actual:n(s[1])}:null))}export async function runTests(r,a={}){const o=detectRunner(r,a.framework);if(!o)return{error:"没有探测到测试框架。支持:Maven(pom.xml) / Gradle(build.gradle) / pytest(pytest.ini·conftest.py·test_*.py) / Node(package.json的test脚本)。\n可以用framework参数强制指定,或直接用run_bash跑你自己的命令。"};const c=o.ownXml?t.join(s.tmpdir(),`huicai-tests-${process.pid}-${Date.now()}.xml`):"",l=o.ownXml?[]:f(r),p=new Map;for(const t of l)for(const s of m(t))try{p.set(s,e.statSync(s).mtimeMs)}catch{}const u=o.command(a.filter||"",c),d=Date.now(),{code:y,output:x}=await n(r,u,Number(a.timeoutMs)||6e5),g=Date.now()-d,$={total:0,passed:0,failed:0,skipped:0,timeMs:0,failures:[]};let h="";if("junit-xml"===o.format){const t=o.ownXml?e.existsSync(c)?[c]:[]:f(r).flatMap(e=>m(e)).filter(t=>{try{return!p.has(t)||e.statSync(t).mtimeMs>p.get(t)}catch{return!1}});for(const e of t)parseJUnitXml(i(e),$);if(h=t.length?`${t.length} 份JUnit XML报告`:"",o.ownXml)try{e.unlinkSync(c)}catch{}}else"tap"===o.format&&(parseTap(x,$),0===$.total?(parseNodeSpec(x,$),h=$.total?"node:test输出":""):h="TAP输出");const k=$.total>0;return{runner:o.id,command:u,code:y,wallMs:g,stats:{...$,failures:void 0},failures:$.failures,parsedFrom:h,compiledOk:k,raw:x}}export function renderTestReport(e,s="."){if(e.error)return e.error;const n=e.stats,r=`【测试结果】${e.runner} · ${(e.wallMs/1e3).toFixed(1)}s`;if(!e.compiledOk){const t=String(e.raw||"").split("\n").filter(Boolean).slice(-40).join("\n");return`${r}\n✗ **一个用例都没跑起来**(退出码 ${e.code})——通常是编译失败、依赖缺失或用例收集报错。\n命令:${e.command}\n\n原始输出(末 40 行):\n${t}\n\n先把它跑起来再谈测试结果:编译错误看上面第一个ERROR的文件与行号。`}const i=[`${r} · 解析自${e.parsedFrom}`,`通过 ${n.passed} · 失败 ${n.failed} · 跳过 ${n.skipped} · 共 ${n.total}`];if(0===n.failed)return i.push("✅ 全部通过。"),n.skipped&&i.push(`ℹ 有 ${n.skipped} 个用例被跳过——确认是有意跳过,而不是被skip标记忘了去掉。`),i.join("\n");i.push("");const a=e.failures.slice(0,12);return a.forEach((e,r)=>{const a=e.suite?`${e.suite}.${e.name}`:e.name;i.push(`── 失败 ${r+1}/${n.failed}:${a}`);const o=void 0!==e.expected||void 0!==e.actual?{expected:String(e.expected??"(未给出)"),actual:String(e.actual??"(未给出)")}:extractAssertion(e.message);o?(i.push(` 期望:${o.expected}`),i.push(` 实际:${o.actual}`)):e.message?i.push(` ${e.kind}:${e.message.split("\n")[0].slice(0,200)}`):i.push(` ${e.kind}(无消息)`);const c=locateSourceFrame(e.stack)||locateSourceFrame(e.message);if(c){const e=t.isAbsolute(c.file)?t.relative(s,c.file):c.file;i.push(c.isTest?` → 用例位置:${e}:${c.line}(堆栈里没有业务代码帧)`:` → 被测代码:${e}:${c.line}`)}}),e.failures.length>a.length&&i.push(`…… 另有 ${e.failures.length-a.length} 个失败用例未展开(用filter缩小范围逐个看)。`),i.push(""),i.push("先读「期望vs实际」——两者的差异通常直接指出是业务逻辑错了还是用例的预期写错了;再看「被测代码」那一行(它是堆栈里第一个既非测试、也非框架的位置)。"),i.join("\n")}export function makeTestTools({cwd:s,config:n={}}){return[{readOnly:!1,spec:{type:"function",function:{name:"run_tests",description:"跑单元测试并**把结果解析成结构化摘要**——改完代码验证、排查失败用例时用它,不要用run_bash跑测试。\n两者的差别在于你拿到什么:run_bash给你几千行原始输出(Maven的下载与编译日志、pytest堆栈里十几层框架调用),要你自己从噪声里找哪个用例失败、为什么;本工具直接给出「通过/失败/跳过的数量 + 每个失败用例的期望值与实际值 + **堆栈里第一个非测试非框架的位置**(也就是你的哪一行业务代码出的问题)」。\n自动探测框架:Maven(pom.xml) · Gradle(build.gradle) · pytest(pytest.ini/conftest.py/test_*.py) · Node(package.json的test脚本)。Maven/Gradle/pytest都走JUnit XML报告,结果精确到用例级。\n用filter只跑相关用例(Maven是 -Dtest、Gradle是 --tests、pytest是 -k、Node是用例名模式)——定位问题时比全量跑快得多。\n**一个用例都没跑起来时会如实说明**(编译失败/依赖缺失/收集报错),并给出原始输出末尾,不会伪装成「0 个用例、全部通过」。",parameters:{type:"object",properties:{path:{type:"string",description:"项目目录(相对工作目录),默认当前目录"},filter:{type:"string",description:"只跑匹配的用例。Maven:`OrderServiceTest` 或 `OrderServiceTest#某方法`;Gradle:`*.OrderServiceTest`;pytest:`-k` 表达式如 `settle and not slow`;Node:用例名片段"},framework:{type:"string",description:"强制指定框架(maven / gradle / pytest / node)。不给则自动探测"},timeout:{type:"number",description:"超时毫秒,默认 600000(10 分钟)"}},required:[]}}},run:async n=>{const r=t.resolve(s,String(n?.path||".").trim()||".");if(r!==s&&!r.startsWith(s+t.sep))return"路径越出工作目录:"+n.path;if(!e.existsSync(r))return"路径不存在:"+n.path;try{return renderTestReport(await runTests(r,{filter:n?.filter,framework:n?.framework,timeoutMs:n?.timeout}),r)}catch(e){return"测试未能运行:"+(e.message||String(e))}}}]}
|
package/src/todo.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const t=new Set(["pending","in_progress","completed"]);export const TODO_TOOL_NAME="todo_write";export function makeTodoTool({state:e,emit:o}){return{readOnly:!0,spec:{type:"function",function:{name:"todo_write",description:"维护当前任务的步骤清单,帮助用户看到进度。多步骤/非琐碎任务开始前先列出计划,每完成一步就更新状态(同一时刻至多一项
|
|
1
|
+
const t=new Set(["pending","in_progress","completed"]);export const TODO_TOOL_NAME="todo_write";export function makeTodoTool({state:e,emit:o}){return{readOnly:!0,spec:{type:"function",function:{name:"todo_write",description:"维护当前任务的步骤清单,帮助用户看到进度。多步骤/非琐碎任务开始前先列出计划,每完成一步就更新状态(同一时刻至多一项in_progress)。每次调用需提交完整清单(全量覆盖)。单步小事无需使用。",parameters:{type:"object",properties:{todos:{type:"array",description:"完整的任务清单(全量覆盖旧清单)",items:{type:"object",properties:{content:{type:"string",description:"任务内容(简短祈使句)"},status:{type:"string",enum:["pending","in_progress","completed"],description:"任务状态"}},required:["content","status"]}}},required:["todos"]}}},run:async n=>{const r=Array.isArray(n?.todos)?n.todos:null;if(!r)return"todo_write需要todos数组";const s=r.map(e=>({content:String(e?.content??"").trim(),status:t.has(e?.status)?e.status:"pending"})).filter(t=>t.content).slice(0,50);e.list=s;try{o?.(s)}catch{}const i=s.filter(t=>"completed"===t.status).length;return`已更新任务清单:共 ${s.length} 项,完成 ${i} 项。`}}}export function formatTodos(t){return(t||[]).map(t=>`${"completed"===t.status?"☑":"in_progress"===t.status?"◐":"☐"} ${t.content}`).join("\n")}
|
package/src/toolpipe.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"node:fs";import r from"node:path";import{runTool as t,toolTimeoutMs as n,toolTimeoutText as o}from"./tools.js";import{runHooks as s}from"./hooks.js";import{isFileWriteTool as i}from"./permissions.js";import{capOrSpill as a,MAX_TOOL_RESULT_CHARS as l}from"./spill.js";import{toolResultCharBudget as c}from"./context-window.js";import{queryOf as u}from"./crush.js";import{guardOverwrite as f,noteAfterCall as d}from"./fs-observe.js";export function looksBinary(e){return!!e&&e.subarray(0,8e3).includes(0)}export function isBinaryFile(r){let t;try{t=e.openSync(r,"r");const n=Buffer.alloc(8e3),o=e.readSync(t,n,0,n.length,0);return looksBinary(n.subarray(0,o))}catch{return!1}finally{if(void 0!==t)try{e.closeSync(t)}catch{}}}export async function withToolDeadline(e,r,t,n){if(!(r>0))return e(n);const s=new AbortController,i=()=>s.abort();n&&(n.aborted?s.abort():n.addEventListener("abort",i,{once:!0}));let a=null;const l=new Promise(e=>{a=setTimeout(()=>{s.abort(),e({timedOut:!0})},r)});try{const n=await Promise.race([e(s.signal).then(e=>({value:e}),e=>({err:e})),l]);if(n.timedOut)return o(t,r);if(n.err)throw n.err;return n.value}finally{a&&clearTimeout(a),n?.removeEventListener?.("abort",i)}}const m={name:"validate",before(e){e.argsError&&(e.result=e.argsError,e.settled=!0)}},g={name:"authorize",async before(e,r){const t=await r.permissions.request(e.name,e.args,r.cwd,e.readOnly||e.deferToIde);t.policyRule&&(e.policyRule=t.policyRule),t.allow||(e.denied=t.reason||"未授权,未执行。")}};function p(e,r){return{cwd:r.cwd,toolName:e.name,args:e.args,callId:e.callId||"",sessionId:r.config?.sessionId||"",model:r.config?.model||"",turn:r.turn??""}}const h={name:"hooks",skipReadOnly:!0,before(e,r){if(!r.hooksActive)return;const t=s("PreToolUse",{...p(e,r)},r.hooks);for(const e of t.outputs)r.info("钩子·PreToolUse:"+r.shortLine(e.stdout,120));t.block?e.denied="PreToolUse钩子阻断:"+t.reason:t.modifiedArgs&&(e.args={...e.args,...t.modifiedArgs},r.info("钩子·PreToolUse已修改工具参数"))},after(e,r){if(!r.hooksActive)return;const t=s("PostToolUse",{...p(e,r),result:String(e.result)},r.hooks);if(t.outputs.length){for(const e of t.outputs)r.info("钩子·PostToolUse:"+r.shortLine(e.stdout,120));e.result=String(e.result)+"\n\n[PostToolUse钩子输出]\n"+t.outputs.map(e=>e.stdout).join("\n")}}},y={name:"journal",skipReadOnly:!0,before(t,n){if(!i(t.name)||!t.args?.path)return;const o=String(t.args.path),s=r.resolve(n.cwd,o);let a=null;try{a=e.readFileSync(s)}catch{}const l=looksBinary(a);if(t.meta.before=l?null:a?a.toString("utf8"):"",!n.fileJournal)return;const c=!!a||e.existsSync(s);a&&a.length>2097152?n.fileJournal.push({path:o,tooLarge:!0}):l?n.fileJournal.push({path:o,before:a.toString("base64"),encoding:"base64",existed:c}):n.fileJournal.push({path:o,before:t.meta.before,existed:c})}},w={name:"inbound-scan",async after(e,r){if(!1===r.config?.inboundScan)return;const t=function(e){const r=e.extra;if(r?.mcpServer)return r.mcpOrigin?`${r.mcpServer}@${r.mcpOrigin}`:`MCP:${r.mcpServer}`;if(r?.networkSource)return String(r.networkSource);if(b.has(e.name)){const r=e.args?.url||e.args?.uri||"";let t="";try{t=r?new URL(String(r)).host:""}catch{t=""}return t||e.name}return null}(e);if(!t)return;const n=String(e.result??"");if(!n.trim())return;const{scanUntrustedText:o,fenceUntrusted:s}=await import("./skill-scan.js"),i=o(n,{source:t,label:e.name});if(i.ok){if(e.meta.inboundScan={source:t,verdict:i.verdict,counts:i.counts},i.counts.critical){const o=i.findings.filter(e=>"critical"===e.severity).slice(0,3).map(e=>e.message).join(";");return r.info(`已给来自 ${t} 的结果加不可信围栏:命中 ${i.counts.critical} 处注入话术(${o})`),void(e.result=s(n,t))}i.counts.warn&&r.config?.debug&&r.info(`来自 ${t} 的结果含 ${i.counts.warn} 处可疑表述(未拦截,仅记录)`)}}};const b=new Set(["fetch_url"]),S={name:"observe",before(e,r){if(!1===r.config?.readBeforeWrite)return;const t=f({sessionId:r.config?.sessionId||"",cwd:r.cwd,name:e.name,args:e.args});t&&(e.denied=t)},after(e,r){const t=/^工具执行失败|^读取失败|不存在/.test(String(e.result??""));d({sessionId:r.config?.sessionId||"",cwd:r.cwd,name:e.name,args:e.args,failed:t})}};export const DEFAULT_STAGES=[m,S,g,h,y,w];export async function runToolCall(e,r){e.meta=e.meta||{},e.denied=null,e.settled=!1;const o=(r.stages||DEFAULT_STAGES).filter(r=>!(r.skipReadOnly&&e.readOnly)),s=[];for(const t of o){if(t.before){if(await t.before(e,r),null!=e.denied)return e;if(e.settled)return e}s.push(t)}r.onStart?.(e),e.result=await async function(e,r){const{config:o,cwd:s,signal:i}=r;let f;try{f=await withToolDeadline(n=>e.extra?"function"==typeof e.extra.runWithEnv?e.extra.runWithEnv(e.args,{...r,toolCallId:e.callId},n):e.extra.run(e.args):t(e.name,e.args,s,o,n),n(e.name,e.extra),e.name,i)}catch(e){f=`工具执行失败:${e.message}`}return e.nested?String(f):a(String(f),{sessionId:o.sessionId||"",toolName:e.name,callId:e.callId,enabled:!1!==o.spill,maxChars:c(o,l),query:u(e.name,e.args),crush:!1!==o.crush})}(e,r);for(let t=s.length-1;t>=0;t--)s[t].after&&await s[t].after(e,r);return e}export function makeDispatch(e){return async(r,t)=>{const n=e.resolveTool?e.resolveTool(r):null;if(!n)return{ok:!1,denied:`未知工具:${r}`};const o=await runToolCall({name:r,args:t||{},readOnly:!!n.readOnly,extra:n.extra,callId:`nested-${r}-${Date.now().toString(36)}`,nested:!0},e);return null!=o.denied?{ok:!1,denied:o.denied}:{ok:!0,result:String(o.result??"")}}}
|
package/src/tools.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as e from"./executors.js";import{searchCode as t}from"./codeindex.js";import{AUTOMATION_TOOL_SPECS as
|
|
1
|
+
import*as e from"./executors.js";import{searchCode as t}from"./codeindex.js";import{searchHistory as r}from"./sessionindex.js";import{AUTOMATION_TOOL_SPECS as n,AUTOMATION_EXECUTORS as i}from"./automation.js";import{runPython as o}from"./pyruntime.js";const s=(e,t)=>({type:"object",properties:e,required:t||[]}),a=e=>({type:"string",description:e}),p=e=>({type:"integer",description:e}),c=e=>({type:"boolean",description:e});export const TOOL_SPECS=[{type:"function",function:{name:"read_file",description:"读取文本文件内容,每行带「行号→」前缀(行号仅供定位,不属于文件内容,编辑时不要包含它)。默认最多 2000 行,可用offset(起始行,1基)/limit(行数) 分段读取大文件。",parameters:s({path:a("相对项目根的文件路径"),offset:p("可选:起始行(1基)"),limit:p("可选:读取行数")},["path"])}},{type:"function",function:{name:"list_files",description:"列出目录下的文件树(跳过 .git/node_modules等)。可选path指定子目录(默认项目根)。",parameters:s({path:a("可选:子目录路径,默认当前项目根")},[])}},{type:"function",function:{name:"glob",description:'按glob模式查找文件(支持 ** 与 *),如 "**/*.ts"、"src/**/*.java"。',parameters:s({pattern:a("glob模式")},["pattern"])}},{type:"function",function:{name:"search_code",description:"按「概念/功能」检索最相关的代码块(BM25 相关性排序,比grep更懂「哪段代码在做这件事」)。适合「登录逻辑在哪」「哪里处理支付回调」这类找相关实现;要找精确字符串仍用grep。返回 文件:行范围 + 代码块。",parameters:s({query:a("要找的功能/概念(自然语言或关键词组合,多写几个相关词更准)"),k:p("可选:返回块数,默认 8,上限 20")},["query"])}},{type:"function",function:{name:"grep",description:"在文件内容中按正则搜索(底层用ripgrep,大型仓库快、自动尊重 .gitignore、不受单文件大小限制→大文件也能命中)。返回 匹配文件:行号: 该行。可选path限定目录、glob限定文件名模式、ignore_case忽略大小写、files_only只列出命中文件、max_results上限。",parameters:s({pattern:a("正则表达式"),path:a("可选:搜索目录,默认项目根"),glob:a("可选:文件名glob过滤"),ignore_case:c("可选:忽略大小写(默认否)"),files_only:c("可选:只返回命中文件列表,不含匹配行(默认否)"),max_results:p("可选:最多返回条数(默认 200,上限 500)")},["pattern"])}},{type:"function",function:{name:"search_history",description:"在**以往的对话历史**里按内容检索(BM25 相关性,中英皆可)。用于「我们之前是怎么解决这个问题的」「上次那个报错在哪次对话里」「以前定过什么方案」——当前对话看不到的历史结论,用它找回来,避免重复踩坑或重复讨论。返回匹配的会话标题、时间、项目与命中片段。注意:它检索的是历史对话,不是代码(找代码用search_code/grep)。",parameters:s({query:a("要在历史对话里找什么(自然语言或关键词组合)"),k:p("可选:返回会话数,默认 8,上限 20"),this_project:c("可选:只在当前项目目录的会话里找(默认跨项目全找)"),include_unowned:c('可选:连"登录之前留下的、不带用户标记的旧会话"一起检索。默认只看当前登录用户的会话;当返回提示有旧会话被挡住时,带上它重试')},["query"])}},{type:"function",function:{name:"read_document",description:"读取PDF/Word/Excel/PPT/RTF等文档的纯文本内容(经平台解析,read_file读不了这类二进制文档)。大型文档分段通读:段末标注「全文共N字符/本段区间」,继续调用并传offset读取后续,直到「已到结尾」。对话附件若标注「已截断」,用本工具读取被省略的部分。",parameters:s({path:a("相对项目根的文档路径"),offset:p("可选:窗口起始字符位置(0 基,默认 0,从头顺序读取)"),max_chars:p("可选:本段最大字符数(默认 16000)")},["path"])}},{type:"function",function:{name:"write_file",description:"把文本写入文件(覆盖式,自动建父目录)。仅限当前项目目录内。仅用于新建文件或全量重写;小改动优先edit_file。生成超长文件(长文档/PPT/大代码文件)时分段产出:先写首段,后续每段设append=true追加到文件末尾,避免单次内容过长被截断。",parameters:s({path:a("相对项目根的文件路径"),content:a("本次写入的内容(append=true时为追加的续段)"),append:c("可选:true=追加到文件末尾(分段产出大文件);默认覆盖写")},["path","content"])}},{type:"function",function:{name:"edit_file",description:"精确替换文件中的片段:old_string必须是文件原文(不含read_file的行号前缀)且在文件中唯一(除非replace_all=true)。仅限项目目录内。",parameters:s({path:a("相对项目根的文件路径"),old_string:a("被替换的原文(需在文件中唯一,除非replace_all;不要包含行号前缀)"),new_string:a("替换为的新内容"),replace_all:c("是否替换全部匹配(默认否)")},["path","old_string","new_string"])}},{type:"function",function:{name:"run_bash",description:"在项目目录下执行一条shell命令并返回输出(stdout+stderr+退出码)。用于构建、测试、git、脚手架等。默认超时 120s(可用timeout调整)。避免用它读文件/搜索(用read_file/grep更高效)。长命令(构建/全量测试/大批量下载等)设run_in_background=true:立即返回作业id而不占住这一轮,之后用job_output读输出、job_kill终止;作业跑完会主动通知你,不要为了等它而空转轮询。注意:以当前用户的完整权限运行、无沙箱隔离(可访问项目目录之外),删除/覆盖/联网等破坏性或外发操作须先说明意图再执行。",parameters:s({command:a("要执行的shell命令"),timeout:p("可选:超时毫秒(仅前台执行有效;后台作业不设超时)"),run_in_background:c("可选:放到后台执行,立即返回作业id(默认否)")},["command"])}},{type:"function",function:{name:"run_python",description:"在内置托管Python(含pandas/openpyxl/python-docx/python-pptx/pypdf/matplotlib/tabulate/requests)中执行代码,用于数据分析与Excel/Word/PPT/PDF文档处理(读写原始文档优先用它,勿依赖read_document的纯文本摘要做精确分析)。首次调用会自动准备运行时(可能需数分钟,进度随结果返回)。结果必须print输出;图表用matplotlib savefig落盘(Agg无窗口后端)。缺库时用packages按需安装。",parameters:s({code:a("内联Python代码(与script二选一)"),script:a("脚本路径(相对项目根,与code二选一)"),args:{type:"array",items:{type:"string"},description:"可选:传给脚本的命令行参数"},packages:{type:"array",items:{type:"string"},description:"可选:额外PyPI包(如reportlab、xlsxwriter),自动按需安装"},timeout:p("可选:超时毫秒,默认 120000,上限 600000")},[])}},{type:"function",function:{name:"fetch_url",description:"抓取一个http/https网页或接口(GET),HTML自动转为纯文本(按页面字符集解码,附标题)。用于查在线文档、API说明、Issue、公开数据页等。需要顺着页面继续跳转/翻页采集时传links=true 获取「锚文本 → 绝对URL」清单。结果可能很长,尽量带着明确目的使用。",parameters:s({url:a("完整URL(http/https)"),timeout:p("可选:超时毫秒,默认 30000"),links:c("可选:附带页面链接清单(继续跳转/翻页采集用),默认不附")},["url"])}},...n].filter(e=>!("1"===process.env.HUICAI_PYTHON_DISABLED&&"run_python"===e.function.name));export const READONLY_TOOLS=new Set(["read_file","list_files","glob","grep","search_code","search_history","read_document"]);export const DEFAULT_TOOL_TIMEOUT_MS=3e5;export const MCP_TOOL_TIMEOUT_MS=12e4;const l={run_bash:66e4,run_python:66e4,fetch_url:15e4,read_document:3e5,render_diagram:3e5,security_scan:6e5,semgrep:6e5,gitleaks:6e5,govulncheck:6e5,install_packages:9e5,setup_environment:9e5,install_plugin:9e5,auto_execute:9e5,lsp:12e4,terminal_send:33e4,terminal_open:6e4,terminal_read:3e4,terminal_signal:3e4,terminal_close:3e4,terminal_list:3e4,job_output:33e4,job_list:3e4,job_kill:3e4,consult_platform:6e5,consult_expert:6e5,consult_experts:9e5,ask_advisor:6e5,wait_for_unlock:0,dispatch_agent:0,dispatch_agents:0,dispatch_member:0,workflow:0};export function toolTimeoutMs(e,t){const r=Number(t?.timeoutMs);if(Number.isFinite(r)&&r>=0)return Math.floor(r);const n=l[e];return Number.isFinite(n)?n:String(e||"").startsWith("mcp__")?12e4:3e5}export function toolTimeoutText(e,t){return`【工具超时】${e} 超过 ${Math.round(t/1e3)} 秒未返回,已中止本次调用。请不要原样重试:缩小输入范围(如加limit / 更精确的路径或匹配模式 / 拆成多次小调用),或改用其他工具达成同一目的;若该工具依赖的外部服务不可用,请如实告知用户并继续其余工作。`}const u={read_file:e.readFile,read_document:e.readDocument,list_files:e.listFiles,glob:e.glob,grep:e.grep,search_code:t,search_history:r,write_file:e.writeFile,edit_file:e.editFile,run_bash:e.runBash,run_python:o,fetch_url:e.fetchUrl,...i};export async function runTool(e,t,r,n,i){const o=u[e];if(!o)return`未知工具:${e}`;try{return await o(r,t||{},n,i)}catch(e){return`工具执行失败:${e.message}`}}
|
package/src/toon.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const n=[",","\t","|"],t=n=>null!==n&&"object"==typeof n&&!Array.isArray(n),e=n=>null===n||["string","number","boolean"].includes(typeof n);export function tableShapeOf(n,{minRows:r=3}={}){if(!Array.isArray(n)||n.length<r)return null;if(!t(n[0]))return null;const l=Object.keys(n[0]);if(!l.length)return null;const o=new Set(l);for(const r of n){if(!t(r))return null;const n=Object.keys(r);if(n.length!==l.length)return null;for(const t of n)if(!o.has(t))return null;for(const n of l)if(!e(r[n]))return null}return l}function r(n){let t="";for(let e=0;e<n.length;e++){if("\\"!==n[e]){t+=n[e];continue}const r=n[++e];t+="n"===r?"\n":"r"===r?"\r":"\\"===r?"\\":'"'===r?'"':"\\"+(r??"")}return t}function l(n){const t=n;return t.length>=2&&'"'===t[0]&&'"'===t[t.length-1]?r(t.slice(1,-1)):"null"===t?null:"true"===t||"false"!==t&&(/^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(t)?Number(t):r(t))}const o={",":"comma","\t":"tab","|":"pipe"},u={comma:",",tab:"\t",pipe:"|"};export function encode(t,e={}){const r=tableShapeOf(t,e);if(!r)return null;const l=String(e.name||"rows").replace(/[^\w.-]/g,"_"),u=function(t,e){for(const r of n){let n=!1;for(const l of t){for(const t of e){const e=l[t];if("string"==typeof e&&e.includes(r)){n=!0;break}}if(n)break}if(!n)return r}return","}(t,r),i=`${l}[${t.length}]{${r.join(",")}}`+(","===u?"":`#${o[u]}`)+":",c=t.map(n=>r.map(t=>function(n,t){if(null===n)return"null";if("number"==typeof n)return Number.isFinite(n)?String(n):"null";if("boolean"==typeof n)return String(n);const e=function(n){return n.replace(/\\/g,"\\\\").replace(/\r/g,"\\r").replace(/\n/g,"\\n")}(String(n));return function(n,t){return""===n||n!==n.trim()||"true"===n||"false"===n||"null"===n||!!/^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(n)||!(!n.includes(t)&&!n.includes('"'))}(e,t)?'"'+e.replace(/"/g,'\\"')+'"':e}(n[t],u)).join(u));return i+"\n"+c.join("\n")}function i(n,t){const e=[];let r="",l=!1;for(let o=0;o<n.length;o++){const u=n[o];if(l){if("\\"===u&&o+1<n.length){r+=u+n[++o];continue}'"'===u&&(l=!1),r+=u}else'"'!==u||""!==r?u!==t?r+=u:(e.push(r),r=""):(l=!0,r+=u)}return e.push(r),e}const c=/^([\w.-]+)\[(\d+)]\{([^}]*)}(?:#(comma|tab|pipe))?:$/;export function decode(n){const t=String(n??"").split("\n"),e=c.exec((t[0]||"").trim());if(!e)return null;const[,r,o,s,f]=e,a=s.split(",").map(n=>n.trim()).filter(Boolean);if(!a.length)return null;const g=u[f||"comma"],p=Number(o),h=[];for(let n=1;n<t.length&&h.length<p;n++){const e=t[n];if(!e.trim())continue;const r=i(e,g);if(r.length!==a.length)return null;const o={};a.forEach((n,t)=>{o[n]=l(r[t])}),h.push(o)}return h.length!==p?null:{name:r,rows:h}}export const TOON_HINT="(以上为TOON紧凑表格:首行 `名称[行数]{列名,…}` 声明表头,其后每行按同样列序只给值;值为空写null,带引号的是字符串原文。它与逐行重复字段名的JSON等价,只是省token。)";export function compactJsonText(n,t={}){const e=String(n??""),r=e.trim();if(!r.startsWith("[")||!r.endsWith("]"))return e;if(r.length<400)return e;let l;try{l=JSON.parse(r)}catch{return e}const o=tryEncode(l,{name:t.name||"items",json:r});return o?o.text:e}export function tryEncode(n,t={}){const e=encode(n,t);if(!e)return null;let r;try{r=null!=t.json?String(t.json):JSON.stringify(n,null,2)}catch{return null}return e.length>=r.length?null:{text:e+"\n"+TOON_HINT,saved:r.length-e.length,baseline:r.length}}
|
package/src/transcript.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{streamChat as t}from"./api.js";import{DEFAULT_MODEL as e}from"./config.js";const n=/^(agent|workflow|datahuman|squad)-/;export function textOf(t){return"string"==typeof t?t:Array.isArray(t)?t.map(t=>"text"===t?.type?t.text:"image_url"===t?.type?"[图片]":"").join("\n"):""}export function toMarkdown(t){const e=t||{},n=["# HuiCai
|
|
1
|
+
import{streamChat as t}from"./api.js";import{DEFAULT_MODEL as e}from"./config.js";const n=/^(agent|workflow|datahuman|squad)-/;export function textOf(t){return"string"==typeof t?t:Array.isArray(t)?t.map(t=>"text"===t?.type?t.text:"image_url"===t?.type?"[图片]":"").join("\n"):""}export function toMarkdown(t){const e=t||{},n=["# HuiCai对话记录","",`- 会话:${e.id||""}`,`- 模型:${e.model||""}`,`- 导出时间:${(new Date).toLocaleString()}`,"","---",""];for(const t of e.messages||[])if("system"!==t.role)if("user"===t.role)n.push("### 🧑 用户","",textOf(t.content).trim()||"(空)","");else if("assistant"===t.role){const e=textOf(t.content).trim();e&&n.push("### 🤖 助手","",e,"");for(const e of t.tool_calls||[]){const t=String(e.function?.arguments||"").replace(/\s+/g," ").slice(0,200);n.push(`> 🔧 \`${e.function?.name}(${t})\``,"")}}else if("tool"===t.role){const e=String(t.content||"").slice(0,1500);n.push("> 工具结果:","","```",e,"```","")}return n.join("\n")}export async function distillMemory(o,s,r=""){const i=n.test(String(s.model||""))?s.compactModel||e:s.model,l=(o||[]).filter(t=>"system"!==t.role).map(t=>`[${t.role}] ${textOf(t.content).replace(/\s+/g," ").slice(0,600)}`).join("\n").slice(0,16e3);if(!l.trim())return[];const c=(r?"【已有记忆(勿重复)】\n"+r+"\n\n":"")+"【本次记录】\n"+l,a=((await t({baseUrl:s.baseUrl,apiKey:s.apiKey,model:i,client:s.client,aux:!0,messages:[{role:"system",content:"你是项目知识提炼器。从下面这段对话/执行记录中,提炼出**值得跨会话长期记住**的稳定事实/约定/结论/踩坑(如构建与测试命令、架构决定、代码约定、易错点、外部依赖要点)。要求:每条一行,以「- 」开头,简洁具体;只输出真正长期有用的,不要一次性细节、不要大段代码;若与「已有记忆」重复则跳过;最多 5 条;没有则输出「(无)」。"},{role:"user",content:c}]})).content||"").trim();return!a||/^[((]无[))]$/.test(a)?[]:a.split("\n").map(t=>t.replace(/^[-*]\s*/,"").trim()).filter(t=>t&&!/^[((]无[))]$/.test(t)).slice(0,5)}export async function recapConversation(o,s){const r=n.test(String(s.model||""))?s.compactModel||e:s.model,i=(o||[]).filter(t=>"system"!==t.role).map(t=>`[${t.role}] ${textOf(t.content).replace(/\s+/g," ").slice(0,500)}`).join("\n").slice(0,12e3);if(!i.trim())return"(对话尚无内容)";return((await t({baseUrl:s.baseUrl,apiKey:s.apiKey,model:r,client:s.client,aux:!0,messages:[{role:"system",content:"用一句话(不超过40个汉字)概括这段对话到目前为止在做什么、进展如何。只输出这一句话,不要引号、不要多余说明。"},{role:"user",content:i}]})).content||"").trim().split("\n")[0]||"(无法生成小结)"}
|
package/src/tui/app.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import r,{useState as t,useRef as e,useReducer as n,useEffect as o,useMemo as l}from"react";import{execSync as c}from"node:child_process";import{render as s,Box as u,Text as i,Static as a,useInput as m,useApp as p}from"ink";import{runTurn as g}from"../agent.js";import{fetchUsage as f}from"../api.js";import{toolLabel as d}from"../render.js";import{renderMarkdown as h,looksLikeMarkdown as x}from"../markdown.js";import{renderWelcome as w}from"../banner.js";import{createSession as y,saveSession as b}from"../session.js";import{ensureSessionTitle as k}from"../title.js";import{createCheckpoints as v}from"../checkpoints.js";import{buildExtraTools as $}from"../repl.js";import{buildSystemPrompt as M}from"../context.js";import{closeMcp as T}from"../mcp.js";import{runSlash as E,listCommands as j}from"../commands.js";import{usageStatus as P,fmtReset as A}from"../usage.js";import{expandAtMentions as S,buildUserContent as D,runBangCommand as I,loadHistory as L,appendHistory as C}from"../input.js";import{listWorkspaceFiles as U}from"../executors.js";import{loadAllowRules as _,loadDenyRules as W,matchAllowRules as O,suggestRule as R,addProjectAllowRule as F,riskWarning as K}from"../settings.js";import{PLAN_DENY_REASON as Y,SANDBOX_DENY_REASON as q,sandboxBlocked as B,AUTOMATION_TOOLS as G,automationActionLabel as J,automationDetail as N,PERMISSION_MODES as Q,MODE_ICON as X,MODE_LABEL as z}from"../permissions.js";import{runVerifyLoop as H}from"../autofix.js";import{runAutoFlow as V}from"../autorun.js";const Z=r.createElement,rr=10;function tr(r,t=12){const e=String(r).split("\n");return e.length<=t?r:e.slice(0,t).join("\n")+`\n…(共 ${e.length} 行)`}function er(r){return(r=Number(r)||0)>=1e3?(r/1e3).toFixed(1)+"k":String(r)}const nr=["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];export function App({config:r,cwd:s,session:w,extraTools:lr,mcp:cr,yolo:sr,initialPrompt:ur,version:ir,onExit:ar}){const mr=p(),[pr,gr]=t(s),fr=e(lr),dr=e([]),hr=e(0),xr=e(""),wr=e(w),yr=e(null),br=e(null),kr=e(new Set),vr=e(_(pr)),$r=e(W(pr)),Mr=e(!1),Tr=e(!1),Er=e(!1),jr=e(v()),Pr=e(!!sr),Ar=e(!1),Sr=e("idle"),Dr=e([]),Ir=e({list:[]}),Lr=e(L(pr)),Cr=e(null),Ur=e(""),[,_r]=n(r=>r+1,0),[Wr,Or]=n(r=>r+1,0),[Rr,Fr]=t(0),[Kr,Yr]=t("idle"),[qr,Br]=t({text:"",cursor:0}),[Gr,Jr]=t(null),[Nr,Qr]=t({prompt:0,completion:0}),[Xr,zr]=t(0),[Hr,Vr]=t([]),[Zr,rt]=t(0),[tt,et]=t(sr?"auto":"manual"),nt=()=>Mr.current?"plan":Tr.current?"sandbox":Pr.current?"auto":Er.current?"acceptEdits":"manual",[ot,lt]=t(""),ct=e(null),[st,ut]=t(0),[it,at]=t(null),mt=e(null),[pt,gt]=t(0),[ft,dt]=t(!1),ht=l(()=>j(pr),[pr]),[xt,wt]=t(0),[yt,bt]=t(!1),kt=l(()=>{try{return U(pr)}catch{return[]}},[pr]),[vt,$t]=t(!1),[Mt,Tt]=t(0),Et=e(0),jt=(r,t)=>{dr.current.push({id:hr.current++,kind:r,text:t}),Or()},Pt=()=>{if(xr.current){const t=xr.current,e=!1!==r.markdown&&x(t);jt("assistant",e?h(t):t),xr.current=""}},At=r=>{Sr.current=r?"busy":"idle",Yr(Sr.current)};o(()=>{if("busy"!==Kr)return;const r=setInterval(()=>ut(r=>(r+1)%nr.length),80);return()=>clearInterval(r)},[Kr]);const St=async()=>{if(r.apiKey)try{const t=await f(r.baseUrl,r.apiKey);mt.current=t,at(t)}catch{}};o(()=>{St();const r=setInterval(()=>{St()},18e4);return()=>clearInterval(r)},[]),o(()=>{if(!r.statusLine)return void lt("");const t=()=>{try{const t=c(r.statusLine,{cwd:pr,timeout:3e3,stdio:["ignore","pipe","ignore"]}).toString().split("\n")[0].slice(0,80);lt(t)}catch{lt("")}};t();const e=setInterval(t,5e3);return()=>clearInterval(e)},[r.statusLine]),o(()=>()=>{ct.current&&clearInterval(ct.current)},[]);const Dt=e(""),It={onDelta:r=>{Dt.current&&(Dt.current=""),xr.current+=r,_r()},onReasoning:r=>{Dt.current=(Dt.current+r).slice(-400),_r()},onToolStart:(r,t)=>{Dt.current="",Pt(),jt("tool",d(r,t))},onToolResult:(r,t)=>jt("toolresult",function(r,t=200){const e=String(r).split("\n")[0];return e.length>t?e.slice(0,t)+"…":e}(t)),onFileDiff:r=>jt("diff",r),onSkip:(r,t)=>jt("skip",d(r,t)+" — 已跳过"),onInfo:r=>{Pt(),jt("info",r)},onTodo:r=>Vr(r),onDecision:r=>{Pt();const t=(r.alternatives||[]).length?` · ${r.alternatives.length} 备选`:"",e="draft"===r.status?"(待确认草稿)":"";jt("info",`◆ 已记录决策:${r.title}${t}${e}`)},onTurnEnd:()=>{Dt.current="",Pt()}},Lt={request:(r,t,e,n)=>{if($r.current.length&&O($r.current,r,t))return Promise.resolve({allow:!1,reason:"该操作被拒绝规则阻止(/permissions 查看或移除)。"});if(Mr.current&&!n)return Promise.resolve({allow:!1,reason:Y});if(Tr.current&&B(r))return Promise.resolve({allow:!1,reason:q});if(n||Pr.current||kr.current.has(r)||O(vr.current,r,t))return Promise.resolve({allow:!0});if(Er.current&&("write_file"===r||"edit_file"===r))return Promise.resolve({allow:!0});const o=G.has(r);return new Promise(e=>{br.current={name:r,args:t,resolve:e,consent:o},Jr({label:o?J(r,t):d(r,t),detail:o?N(r,t):"",name:r,args:t,consent:o,risk:K(r,t)})})},isYolo:()=>Pr.current,setYolo:r=>{Pr.current=!!r,et(nt())},setPlanMode:r=>{Mr.current=!!r,et(nt())},isPlanMode:()=>Mr.current,setSandbox:r=>{Tr.current=!!r,et(nt())},isSandbox:()=>Tr.current,setAcceptEdits:r=>{Er.current=!!r,et(nt())},isAcceptEdits:()=>Er.current,setMode:r=>{Pr.current="auto"===r,Er.current="acceptEdits"===r,Mr.current="plan"===r,Tr.current="sandbox"===r,et(r)},getMode:()=>nt(),reloadRules:()=>{vr.current=_(pr),$r.current=W(pr)}},Ct=()=>{const r=Dr.current.shift();return rt(Dr.current.length),null==r?null:r},Ut=()=>{k(wr.current,r).then(r=>{r&&b(wr.current)}).catch(()=>{})},_t=async t=>{const e=P(mt.current);if(e&&"exhausted"===e.level)return void jt("info",`✗ Token 用量窗口(${e.label})已用尽,暂无法继续。${A(e.resetAtEpochMs)}`);const{text:n,images:o}=await S(t,pr,r),l=jr.current.snapshot({session:wr.current,label:t});wr.current.messages.push({role:"user",content:D(n,o)}),Ut(),At(!0),yr.current=new AbortController,wr.current.compaction||(wr.current.compaction={});const c=await g({config:r,messages:wr.current.messages,permissions:Lt,cwd:pr,extraTools:fr.current,events:It,signal:yr.current.signal,todoState:Ir.current,compaction:wr.current.compaction,pullQueued:Ct,fileJournal:l});return yr.current=null,Pt(),c?.usage&&Qr(r=>({prompt:r.prompt+c.usage.prompt,completion:r.completion+c.usage.completion})),c?.lastPromptTokens&&(zr(c.lastPromptTokens),wr.current.lastPromptTokens=c.lastPromptTokens),b(wr.current),Ut(),(c?.aborted||c?.error)&&Dr.current.length&&(Dr.current=[],rt(0),jt("info","已清空排队消息")),At(!1),St(),c},Wt=()=>{ar?.(),mr.exit()},Ot=async t=>{const e=t.trim();if(Br({text:"",cursor:0}),Cr.current=null,e){if(C(pr,e),Lr.current[Lr.current.length-1]!==e&&Lr.current.push(e),e.startsWith("/"))return(async t=>{const e={version:ir,config:r,cwd:pr,usage:Nr,permissions:Lt,extraTools:fr.current,session:wr.current,checkpoints:jr.current,print:r=>jt("info",String(r))},n=await E(t,e);if("exit"===n.action)return Wt();if("clear"===n.action&&(wr.current=y({cwd:pr,model:r.model,messages:[wr.current.messages[0]]}),Ir.current.list=[],jr.current.list.length=0,Vr([]),zr(0),dr.current=[],xr.current="",Fr(r=>r+1),jt("info","已开启新会话(旧会话仍可 --resume 恢复)")),"refresh"===n.action&&(Ir.current.list=[],Vr([]),zr(0)),"autofix"===n.action&&await H({command:n.command,maxIters:n.maxIters,cwd:pr,runTurnOnce:r=>_t(r),emit:r=>jt("info",r)}),"autorun"===n.action&&await V({goal:n.goal,config:r,permissions:Lt,cwd:pr,runTurnOnce:r=>_t(r),emit:r=>jt("info",r)}),"cd"===n.action){vr.current=_(n.cwd),$r.current=W(n.cwd),T(cr?.clients);const t=await $(r,n.cwd);fr.current=t.extraTools,cr&&(cr.clients=t.clients),wr.current=y({cwd:n.cwd,model:r.model,messages:[{role:"system",content:M(n.cwd,{memory:!1!==r.memoryEnabled})}]}),r.sessionId=wr.current.id,Ir.current.list=[],jr.current.list.length=0,Vr([]),zr(0),dr.current=[],xr.current="",Fr(r=>r+1),gr(n.cwd),jt("info","已切换到 "+n.cwd+"(新会话,旧会话已保存)")}"loop"===n.action&&(ct.current&&(clearInterval(ct.current),ct.current=null),n.stop?jt("info","已停止循环。"):(jt("info",`已开始循环:每 ${n.seconds}s 运行「${n.prompt}」(/loop off 停止)`),ct.current=setInterval(()=>{"busy"!==Sr.current&&(jt("user",n.prompt),_t(n.prompt))},1e3*n.seconds))),n.prompt&&("busy"===Sr.current?(Dr.current.push(n.prompt),rt(Dr.current.length)):(jt("user",n.prompt),await _t(n.prompt)))})(e);if(e.startsWith("!")&&e.length>1)return(async r=>{if("busy"===Sr.current)return void jt("info","正在处理上一条,! 命令请稍候再试");jt("user","! "+r);const{output:t,record:e}=await I(r,pr);jt("toolresult",tr(t)),wr.current.messages.push({role:"user",content:e}),b(wr.current)})(e.slice(1).trim());if("busy"===Sr.current)return jt("user",e),Dr.current.push(e),void rt(Dr.current.length);jt("user",e),await _t(e)}};o(()=>{!Ar.current&&ur&&(Ar.current=!0,Ot(ur))},[]);const Rt=/^\/(\S*)$/.exec(qr.text),Ft=Rt?Rt[1]:null,Kt=null==Ft?[]:ht.filter(r=>r.name.toLowerCase().startsWith(Ft.toLowerCase())),Yt=Kt.length>0&&!ft,qt=Kt.length<=rr?0:Math.max(0,Math.min(pt-Math.floor(5),Kt.length-rr)),Bt=Kt.slice(qt,qt+rr);o(()=>{gt(0),dt(!1)},[Ft]);const Gt=null==Ft?/(?:^|\s)@([^\s@]*)$/.exec(qr.text):null,Jt=Gt?Gt[1]:null,Nt=null==Jt?[]:(()=>{const r=Jt.toLowerCase(),t=[];for(const e of kt){const n=e.toLowerCase(),o=n.split("/").pop();let l=-1;if(r?o.startsWith(r)?l=3:o.includes(r)?l=2:n.includes(r)&&(l=1):l=0,l>=0&&t.push({f:e,rank:l}),!r&&t.length>=50)break}return t.sort((r,t)=>t.rank-r.rank||r.f.length-t.f.length||r.f.localeCompare(t.f)),t.slice(0,50).map(r=>r.f)})(),Qt=Nt.length>0&&!yt,Xt=Nt.length<=rr?0:Math.max(0,Math.min(xt-Math.floor(5),Nt.length-rr)),zt=Nt.slice(Xt,Xt+rr);o(()=>{wt(0),bt(!1)},[Jt]);const Ht=l(()=>{const r=new Set,t=[];for(let e=Lr.current.length-1;e>=0&&t.length<50;e--){const n=Lr.current[e];n&&!r.has(n)&&(r.add(n),t.push(n))}return t},[vt]),Vt=Ht.length<=rr?0:Math.max(0,Math.min(Mt-Math.floor(5),Ht.length-rr)),Zt=Ht.slice(Vt,Vt+rr);m((t,e)=>{if(br.current){const r=br.current,n=t=>{br.current=null,Jr(null),r.resolve(t)};if("y"===t||"1"===t)return n({allow:!0});if("a"===t||"2"===t)return kr.current.add(r.name),n({allow:!0});if("3"===t)return n({allow:!1,reason:"用户选择了「否」,该操作未执行。"});if("A"===t){kr.current.add(r.name);const t=R(r.name,r.args);try{F(pr,t),vr.current=_(pr),jt("info",`已写入允许规则:${t} → .huicai/settings.json`)}catch(r){jt("info",`规则写入失败(本会话仍生效):${r.message}`)}return n({allow:!0})}return"n"===t||e.escape||e.return?n({allow:!1,reason:"用户拒绝了该操作。"}):void 0}if(vt){if(!Ht.length)return void $t(!1);if(e.upArrow)return Tt(r=>(r-1+Ht.length)%Ht.length);if(e.downArrow)return Tt(r=>(r+1)%Ht.length);if(e.pageUp)return Tt(r=>Math.max(0,r-rr));if(e.pageDown)return Tt(r=>Math.min(Ht.length-1,r+rr));if(e.return){const r=Ht[Math.min(Mt,Ht.length-1)]||"";return $t(!1),Cr.current=null,Br({text:r,cursor:r.length})}return e.escape?$t(!1):void 0}if(e.tab&&e.shift){const t=Q[(Q.indexOf(Lt.getMode())+1)%Q.length];return Lt.setMode(n=t),r.planMode="plan"===n,r.sandbox="sandbox"===n,void jt("info",`对话模式 → ${X[n]} ${z[n]}`+("auto"===n?"(已开放所有权限,任何命令免审批)":""))}var n;if(Yt){const r=()=>Kt[Math.min(pt,Kt.length-1)];if(e.upArrow)return gt(r=>(r-1+Kt.length)%Kt.length);if(e.downArrow)return gt(r=>(r+1)%Kt.length);if(e.pageUp)return gt(r=>Math.max(0,r-rr));if(e.pageDown)return gt(r=>Math.min(Kt.length-1,r+rr));if(e.tab){const t=r().name;return Br({text:"/"+t+" ",cursor:t.length+2})}if(e.return)return void Ot("/"+r().name);if(e.escape)return dt(!0)}if(Qt){const r=()=>Nt[Math.min(xt,Nt.length-1)];if(e.upArrow)return wt(r=>(r-1+Nt.length)%Nt.length);if(e.downArrow)return wt(r=>(r+1)%Nt.length);if(e.pageUp)return wt(r=>Math.max(0,r-rr));if(e.pageDown)return wt(r=>Math.min(Nt.length-1,r+rr));if(e.tab||e.return){const t=r(),e=qr.text.length-(Jt?Jt.length:0)-1,n=qr.text.slice(0,e+1)+t+" ";return Br({text:n,cursor:n.length})}if(e.escape)return bt(!0)}if(e.escape){if("busy"===Sr.current&&yr.current)return yr.current.abort(),void jt("info","已请求中断(Esc)");const r=Date.now();return r-Et.current<600&&Lr.current.length?(Et.current=0,Tt(0),void $t(!0)):void(Et.current=r)}if(e.ctrl&&"c"===t)"busy"===Sr.current&&yr.current?(yr.current.abort(),jt("info","已请求中断")):Wt();else if(e.return)Ot(qr.text);else{if(e.upArrow)return(()=>{const r=Lr.current;if(!r.length)return;null==Cr.current?(Ur.current=qr.text,Cr.current=r.length-1):Cr.current>0&&Cr.current--;const t=r[Cr.current]??"";Br({text:t,cursor:t.length})})();if(e.downArrow)return(()=>{if(null==Cr.current)return;const r=Lr.current;if(Cr.current<r.length-1){Cr.current++;const t=r[Cr.current];Br({text:t,cursor:t.length})}else Cr.current=null,Br({text:Ur.current,cursor:Ur.current.length})})();if(e.leftArrow)return Br(r=>({...r,cursor:Math.max(0,r.cursor-1)}));if(e.rightArrow)return Br(r=>({...r,cursor:Math.min(r.text.length,r.cursor+1)}));if(e.ctrl&&"a"===t)return Br(r=>({...r,cursor:0}));if(e.ctrl&&"e"===t)return Br(r=>({...r,cursor:r.text.length}));if(e.ctrl&&"u"===t)return Br(r=>({text:r.text.slice(r.cursor),cursor:0}));if(e.ctrl&&"k"===t)return Br(r=>({text:r.text.slice(0,r.cursor),cursor:r.cursor}));if(e.ctrl&&"w"===t)return Br(r=>{const t=r.text.slice(0,r.cursor).replace(/\S+\s*$/,"");return{text:t+r.text.slice(r.cursor),cursor:t.length}});if(e.backspace||e.delete)return Cr.current=null,Br(r=>r.cursor>0?{text:r.text.slice(0,r.cursor-1)+r.text.slice(r.cursor),cursor:r.cursor-1}:r);if(t&&!e.ctrl&&!e.meta){Cr.current=null;const r=t.replace(/\r\n?/g,"\n");Br(t=>({text:t.text.slice(0,t.cursor)+r+t.text.slice(t.cursor),cursor:t.cursor+r.length}))}}});const re=l(()=>dr.current.slice(),[Wr]),te=pr.split("/").filter(Boolean).pop()||pr,ee=qr.text.slice(0,qr.cursor),ne=qr.text[qr.cursor]??" ",oe=qr.text.slice(qr.cursor+1),le=Math.max(8,process.stdout.columns||80),ce=P(it),se=ce?"exhausted"===ce.level?`⛔ ${ce.label}用量已用尽${A(ce.resetAtEpochMs)?" · "+A(ce.resetAtEpochMs):""}`:`⚠ ${ce.label}用量 ${Math.round(100*ce.pct)}%(${er(ce.used)}/${er(ce.limit)})`:"";return Z(u,{flexDirection:"column"},Z(a,{key:Rr,items:re},r=>Z(or,{key:r.id,item:r})),Dt.current?Z(u,null,Z(i,{color:"gray",italic:!0},"✦ 思考 "+Dt.current.replace(/\n/g," ").slice(-160))):null,xr.current?Z(u,{marginTop:0},Z(i,null,xr.current)):null,Hr.length?Z(u,{flexDirection:"column",paddingLeft:2,marginTop:1},...Hr.map((r,t)=>Z(i,{key:t,color:"completed"===r.status?"green":"in_progress"===r.status?"cyan":"gray",dimColor:"completed"===r.status,bold:"in_progress"===r.status},("completed"===r.status?"☑ ":"in_progress"===r.status?"◐ ":"☐ ")+r.content))):null,"busy"!==Kr||xr.current?null:Z(u,null,Z(i,{color:"cyan"},nr[st]+" 思考中…"),Z(i,{color:"gray"},"(Esc 中断 · 输入回车可排队补充)")),Gr?Z(u,{flexDirection:"column",borderStyle:"round",borderColor:"yellow",paddingX:1,marginTop:1},Z(i,{color:"yellow",bold:!0},Gr.consent?"● 是否继续?"+Gr.label:"● 需要授权:"+Gr.label),Gr.risk?Z(i,{color:"red",bold:!0},"⚠ "+Gr.risk):null,Gr.consent&&Gr.detail?Z(i,{color:"gray"},tr(Gr.detail,6)):null,"run_bash"===Gr.name?Z(i,{color:"gray"},"$ "+(Gr.args.command||"")):null,"fetch_url"===Gr.name?Z(i,{color:"gray"},"GET "+(Gr.args.url||"")):null,Z(u,{marginTop:1}),...Gr.consent?[Z(u,{key:"c1"},Z(i,{color:"green",bold:!0}," [1] "),Z(i,null,"是,继续")),Z(u,{key:"c2"},Z(i,{color:"cyan",bold:!0}," [2] "),Z(i,null,"是,并在本次对话中后续相同操作不再询问")),Z(u,{key:"c3"},Z(i,{color:"gray",bold:!0}," [3] "),Z(i,{color:"gray"},"否(或按 Esc / 回车)"))]:[Z(u,{key:"a1"},Z(i,{color:"green",bold:!0}," [y] "),Z(i,null,"允许一次")),Z(u,{key:"a2"},Z(i,{color:"cyan",bold:!0}," [a] "),Z(i,null,"本次对话都允许")),Z(u,{key:"a3"},Z(i,{color:"blue",bold:!0}," [A] "),Z(i,null,"永久允许(写入项目设置)")),Z(u,{key:"a4"},Z(i,{color:"gray",bold:!0}," [n] "),Z(i,{color:"gray"},"拒绝(或按 Esc / 回车)"))]):null,Yt?Z(u,{flexDirection:"column",marginTop:1},Z(i,{color:"gray"},Kt.length>rr?` 斜杠命令 ${qt+1}-${qt+Bt.length}/${Kt.length} · ↑↓ 选择 · PgUp/PgDn 翻页 · Tab 补全 · ⏎ 执行 · Esc 关闭`:" 斜杠命令 · ↑↓ 选择 · Tab 补全 · ⏎ 执行 · Esc 关闭"),...Bt.map((r,t)=>{const e=qt+t;return Z(u,{key:r.name},Z(i,{color:"cyan",bold:e===pt},(e===pt?" ❯ ":" ")+"/"+r.name+(r.args?" "+r.args:"")),Z(i,{color:"gray"}," "+String(r.desc||"").replace(/\s+/g," ").slice(0,46)))})):null,Qt?Z(u,{flexDirection:"column",marginTop:1},Z(i,{color:"gray"},Nt.length>rr?` @ 引用文件 ${Xt+1}-${Xt+zt.length}/${Nt.length} · ↑↓ 选择 · PgUp/PgDn 翻页 · Tab/⏎ 插入 · Esc 关闭`:" @ 引用文件 · ↑↓ 选择 · Tab/⏎ 插入 · Esc 关闭"),...zt.map((r,t)=>{const e=Xt+t;return Z(u,{key:r},Z(i,{color:"cyan",bold:e===xt},(e===xt?" ❯ ":" ")+"@"+r))})):null,vt&&Ht.length?Z(u,{flexDirection:"column",marginTop:1},Z(i,{color:"gray"},Ht.length>rr?` 历史记录 ${Vt+1}-${Vt+Zt.length}/${Ht.length} · ↑↓ 选择 · PgUp/PgDn 翻页 · ⏎ 填入 · Esc 关闭`:" 历史记录 · ↑↓ 选择 · ⏎ 填入 · Esc 关闭"),...Zt.map((r,t)=>{const e=Vt+t;return Z(u,{key:e},Z(i,{color:"cyan",bold:e===Mt},(e===Mt?" ❯ ":" ")+r.replace(/\s+/g," ").slice(0,le-6)))})):null,Z(u,{marginTop:Yt||Qt||vt?0:1},Z(i,{color:"blue"},"─".repeat(le))),Z(u,null,Z(i,{color:"busy"===Kr?"yellow":"cyan"},"busy"===Kr?"⧗ ":"› "),Z(i,null,ee),Z(i,{inverse:!0},ne),Z(i,null,oe)),Z(i,{color:"blue"},"─".repeat(le)),Z(u,{justifyContent:"space-between"},Z(i,{color:"gray"},(ot?ot+" · ":"")+`${te} · ${r.model} · ${X[tt]} ${z[tt]}`+(Zr?` · 排队 ${Zr}`:"")+" · ⇧Tab 模式 · /help"),se?Z(i,{color:"exhausted"===ce.level?"red":"yellow",bold:!0},se):Z(i,null,"")))}function or({item:r}){const{kind:t,text:e}=r;if("user"===t)return Z(u,{marginTop:1},Z(i,{color:"cyan",bold:!0},"› "),Z(i,{bold:!0},e));if("assistant"===t)return Z(u,null,Z(i,null,e));if("diff"===t)return Z(u,{marginLeft:2},Z(i,null,e));if("tool"===t)return Z(i,{color:"cyan"}," ⏺ "+e);if("toolresult"===t)return Z(i,{color:"gray"}," "+e);if("skip"===t)return Z(i,{color:"gray"}," ⊘ "+e);if("info"===t){const r=/✗|失败|错误|error/i.test(e);return Z(i,{color:r?"red":"yellow"},(r?" ✗ ":" · ")+e.replace(/^✗\s*/,""))}return Z(i,null,e)}export async function runInk({config:r,cwd:t,session:e,yolo:n,initialPrompt:o,version:l}){process.stdout.write(w({model:r.model,cwd:t,version:l,platform:/^(agent|workflow|datahuman)-/.test(String(r.model||"")),yolo:n,consult:r.consult?r.consultLabel||r.consult:null,ink:!0})+"\n");const{extraTools:c,clients:u}=await $(r,t),i={clients:u},{waitUntilExit:a}=s(Z(App,{config:r,cwd:t,session:e,extraTools:c,mcp:i,yolo:n,initialPrompt:o,version:l,onExit:null}));await a(),T(i.clients)}
|
|
1
|
+
import r,{useState as e,useRef as t,useReducer as n,useEffect as o,useMemo as s}from"react";import{syncPlanUser as c}from"../plan-limits.js";import{exec as u}from"node:child_process";import{render as i,Box as l,Text as a,useInput as m,usePaste as f,useApp as d,useStdout as p,useStdin as g}from"ink";import{runTurn as h}from"../agent.js";import{fetchUsage as x}from"../api.js";import{toolLabel as w}from"../render.js";import{renderMarkdown as v,looksLikeMarkdown as b}from"../markdown.js";import{createSession as k,saveSession as y}from"../session.js";import{ensureSessionTitle as j}from"../title.js";import{createCheckpoints as I}from"../checkpoints.js";import{buildExtraTools as T}from"../repl.js";import{buildSystemPrompt as M}from"../context.js";import{closeMcp as $}from"../mcp.js";import{runSlash as A,listCommands as S}from"../commands.js";import{matchCommands as P}from"../command-ui.js";import{menuOptions as U}from"./command-menu.js";import{questionSelection as E,approvalSelection as R,initialSelection as D,selectionKey as q,insertSelectionText as L}from"../selection.js";import{parseAskAnswer as C}from"../ask-ui.js";import{usageStatus as O,fmtReset as G}from"../usage.js";import{handleAccountError as W}from"../account.js";import{pricingUrl as N}from"../errkind.js";import{expandAtMentions as Y,buildUserContent as F,loadHistory as H,appendHistory as Q}from"../input.js";import{listWorkspaceFiles as z,runBash as K}from"../executors.js";import{loadAllowRules as V,loadDenyRules as _,matchAllowRules as B,suggestRule as J,addProjectAllowRule as X,riskWarning as Z}from"../settings.js";import{PLAN_DENY_REASON as rr,SANDBOX_DENY_REASON as er,sandboxBlocked as tr,isFileWriteTool as nr,AUTOMATION_TOOLS as or,automationActionLabel as sr,automationDetail as cr,PERMISSION_MODES as ur,MODE_ICON as ir,MODE_LABEL as lr}from"../permissions.js";import{runVerifyLoop as ar}from"../autofix.js";import{runAutoFlow as mr}from"../autorun.js";import{acquireWakeLock as fr,releaseWakeLock as dr}from"../presence.js";import{notifyTurnDone as pr}from"../notify.js";import{closeAllTerminals as gr,dropSessionTerminals as hr}from"../terminal.js";import{shutdownLsp as xr}from"../lsp.js";import{ensureVerified as wr}from"../sandbox.js";import{runGoalRounds as vr,getGoal as br,setRoundKind as kr}from"../goalround.js";import{Workbench as yr,moveCursor as jr}from"./workbench.js";import{WelcomeBanner as Ir}from"./welcome.js";import{enableMouseTracking as Tr,mouseReport as Mr}from"./scroll.js";const $r=r.createElement;function Ar(r){return(r=Number(r)||0)>=1e3?(r/1e3).toFixed(1)+"k":String(r)}export function App({config:r,cwd:c,session:i,extraTools:l,mcp:a,yolo:gr,initialPrompt:xr,version:wr,onExit:Ir,exitState:Sr={},injectQueue:Pr=[],bridge:Ur={}}){const Er=d(),{stdout:Rr}=p(),{stdin:Dr}=g();o(()=>Dr.isTTY?Tr(Rr):void 0,[Dr,Rr]);const[qr,Lr]=e({columns:Rr.columns||80,rows:Rr.rows||24});o(()=>{const r=()=>Lr({columns:Rr.columns||80,rows:Rr.rows||24});return Rr.on("resize",r),()=>Rr.off("resize",r)},[Rr]);const[Cr,Or]=e({offset:0,revision:0}),Gr=r=>{Wr.current.offset=r,Or(e=>({offset:r,revision:e.revision+1}))},Wr=t({max:0,page:10,offset:0}),Nr=r=>Gr(Math.max(0,Math.min(Wr.current.max,Wr.current.offset+r))),Yr=t(null),Fr=t(!1),Hr=t(null),[Qr,zr]=e(null),Kr=t([]),[Vr,_r]=e(""),Br=t(0),[Jr,Xr]=e(c),Zr=t(l),re=t(null);re.current||(re.current=i.messages.filter(r=>["user","assistant"].includes(r.role)&&r.content).map((r,e)=>({id:e,kind:r.role,text:"string"==typeof r.content?r.content:r.content.filter(r=>"text"===r.type).map(r=>r.text).join("\n")})));const ee=t(re.current.length),te=t(""),ne=t(i),oe=t(null),se=t(new Set),ce=t(V(Jr)),ue=t(_(Jr)),ie=t(!1),le=t(!1),ae=t(!1),me=t(I()),fe=t(!!gr),de=t(!1),pe=t("idle"),ge=t([]),he=t({list:[]}),xe=t(H(Jr)),we=t(null),ve=t(""),[,be]=n(r=>r+1,0),[ke,ye]=n(r=>r+1,0),[je,Ie]=e("idle"),[Te,Me]=e({text:"",cursor:0}),$e=t({prompt:0,completion:0}),[Ae,Se]=e({prompt:0,completion:0}),[Pe,Ue]=e(0),[Ee,Re]=e([]),[De,qe]=e("hidden"),[Le,Ce]=e(0),[Oe,Ge]=e(gr?"auto":"manual"),We=()=>ie.current?"plan":le.current?"sandbox":fe.current?"auto":ae.current?"acceptEdits":"manual",[Ne,Ye]=e(""),Fe=t(null),[He,Qe]=e(null),ze=t(null),[Ke,Ve]=e(0),[_e,Be]=e(!1),Je=s(()=>S(Jr),[Jr,je]),[Xe,Ze]=e(0),[rt,et]=e(!1),tt=s(()=>{try{return z(Jr)}catch{return[]}},[Jr,je]),[nt,ot]=e(!1),[st,ct]=e(0),ut=t(0),it=(r,e)=>{re.current.push({id:ee.current++,kind:r,text:e}),ye()},lt=()=>{if(te.current){const e=te.current,t=!1!==r.markdown&&b(e);it("assistant",t?v(e):e),te.current=""}},at=r=>{pe.current=r?"busy":"idle",Ie(pe.current)};Ur.signalRef||={current:null},Ur.sessionOf=()=>ne.current;const mt=(r=null)=>{const e=Hr.current,t=Kr.current.shift()||null;Hr.current=t,zr(t),e?.resolve(r)},ft=r=>new Promise(e=>{if(Yr.current?.signal.aborted||Fr.current)return e(null);const t={...D(r),resolve:e};Hr.current?Kr.current.push(t):(Hr.current=t,zr(t),Gr(0))});Ur.askUi=async r=>{const e=await ft(E(r));return"date_range"===r.kind&&e?.value?C(r,e.value):e||{value:null}},Ur.ask=async r=>(await Ur.askUi({kind:"text",question:r}))?.value||"已取消";const dt=()=>{const r=[Hr.current,...Kr.current.splice(0)];Hr.current=null,zr(null);for(const e of r)e?.resolve(null)},pt=()=>{qe("hidden"),Yr.current?.abort(),oe.current?.abort(),dt()};o(()=>()=>{Yr.current?.abort(),Hr.current?.resolve(null),Hr.current=null;for(const r of Kr.current.splice(0))r.resolve(null);Ur.askUi=null,Ur.ask=null},[]),o(()=>{if("busy"!==je)return;const r=setInterval(be,250);return()=>clearInterval(r)},[je]);const gt=async()=>{if(r.apiKey)try{const e=await x(r.baseUrl,r.apiKey);ze.current=e,Qe(e)}catch{}};o(()=>{gt();const r=setInterval(()=>{gt()},18e4);return()=>clearInterval(r)},[]),o(()=>{if(!r.statusLine)return void Ye("");let e=!1,t=null;const n=()=>{t||(t=u(r.statusLine,{cwd:Jr,timeout:3e3,maxBuffer:8192},(r,n)=>{t=null,e||Ye(r?"":n.split("\n")[0].slice(0,80))}))};n();const o=setInterval(n,5e3);return()=>{e=!0,clearInterval(o),t?.kill()}},[r.statusLine,Jr]),o(()=>()=>{Fe.current&&clearInterval(Fe.current)},[]);const ht=t(""),xt={onDelta:r=>{ht.current&&(ht.current=""),te.current+=r,be()},onReasoning:r=>{ht.current=(ht.current+r).slice(-400),be()},onToolStart:(r,e)=>{ht.current="",lt(),_r(w(r,e)),it("tool",w(r,e))},onToolResult:(r,e)=>{_r("整理结果"),it("toolresult",String(e))},onFileDiff:r=>it("diff",r),onSkip:(r,e)=>it("skip",w(r,e)+" — 已跳过"),onInfo:r=>{lt(),it("info",r)},onTodo:r=>{Re(r),Yr.current?.signal.aborted||qe(e=>r.length?"hidden"===e?"expanded":e:"hidden")},onDecision:r=>{lt();const e=(r.alternatives||[]).length?` · ${r.alternatives.length} 备选`:"",t="draft"===r.status?"(待确认草稿)":"";it("info",`◆ 已记录决策:${r.title}${e}${t}`)},onTurnEnd:()=>{qe("hidden"),ht.current="",lt()}},wt={request:(r,e,t,n)=>{if(Yr.current?.signal.aborted)return Promise.resolve({allow:!1,reason:"操作已中断。"});if(ue.current.length&&B(ue.current,r,e))return Promise.resolve({allow:!1,reason:"该操作被拒绝规则阻止(/permissions查看或移除)。"});if(ie.current&&!n)return Promise.resolve({allow:!1,reason:rr});if(le.current&&tr(r))return Promise.resolve({allow:!1,reason:er});if(n||fe.current||se.current.has(r)||B(ce.current,r,e))return Promise.resolve({allow:!0});if(ae.current&&nr(r))return Promise.resolve({allow:!0});const o=or.has(r);return ft(R({name:r,args:e,consent:o,label:o?sr(r,e):w(r,e),detail:o?cr(r,e):"",risk:Z(r,e)})).then(t=>{if(!t||"deny"===t||Yr.current?.signal.aborted)return{allow:!1,reason:"用户未批准此操作。"};if("session"!==t&&"project"!==t||se.current.add(r),"project"===t){const t=J(r,e);try{X(Jr,t),ce.current=V(Jr),it("info",`已写入允许规则:${t} → .huicai/settings.json`)}catch(r){it("info",`规则写入失败(本会话仍生效):${r.message}`)}}return{allow:!0}})},isYolo:()=>fe.current,setYolo:r=>{fe.current=!!r,Ge(We())},setPlanMode:r=>{ie.current=!!r,Ge(We())},isPlanMode:()=>ie.current,setSandbox:r=>{le.current=!!r,Ge(We())},isSandbox:()=>le.current,setAcceptEdits:r=>{ae.current=!!r,Ge(We())},isAcceptEdits:()=>ae.current,setMode:r=>{fe.current="auto"===r,ae.current="acceptEdits"===r,ie.current="plan"===r,le.current="sandbox"===r,Ge(r)},getMode:()=>We(),reloadRules:()=>{ce.current=V(Jr),ue.current=_(Jr)}},vt=()=>{if(Pr.length)return Pr.shift();const r=ge.current.shift();return Ce(ge.current.length),null==r?null:r},bt=()=>{j(ne.current,r).then(r=>{r&&y(ne.current)}).catch(()=>{})},kt=async e=>{qe("hidden");const t=O(ze.current);if(t&&"exhausted"===t.level){const e=N(r.baseUrl);return it("info",`✗ Token用量窗口(${t.label})已用尽,暂无法继续。${G(t.resetAtEpochMs)}`+(e?`\n 升级会员提高上限 → ${e}`:"")),{error:"Token用量已用尽"}}if(Yr.current?.signal.aborted)return{aborted:!0};_r("准备上下文");const{text:n,images:o}=await Y(e,Jr,r);if(Yr.current?.signal.aborted)return{aborted:!0};const s=me.current.snapshot({session:ne.current,label:e});ne.current.messages.push({role:"user",content:F(n,o)}),bt(),at(!0),oe.current=Yr.current||new AbortController,Ur.signalRef.current=oe.current.signal,ne.current.compaction||(ne.current.compaction={});const c="auto"===We()&&!1!==r.wakeLockInAuto?fr({reason:"自动模式任务执行中"}):null,u=Date.now();let i;try{i=await h({config:r,messages:ne.current.messages,permissions:wt,cwd:Jr,extraTools:Zr.current,events:xt,signal:oe.current.signal,todoState:he.current,compaction:ne.current.compaction,pullQueued:vt,fileJournal:s})}finally{qe("hidden"),c?.ok&&dr()}if(i?.aborted||i?.error||pr({durationMs:Date.now()-u,cwd:Jr,summary:String(e).replace(/\s+/g," ").slice(0,80),enabled:!1!==r.notifications}).catch(()=>{}),lt(),i?.usage&&($e.current={prompt:$e.current.prompt+(i.usage.prompt||0),completion:$e.current.completion+(i.usage.completion||0)},Se({...$e.current})),i?.lastPromptTokens&&(Ue(i.lastPromptTokens),ne.current.lastPromptTokens=i.lastPromptTokens),y(ne.current),bt(),(i?.aborted||i?.error)&&ge.current.length&&(ge.current=[],Ce(0),it("info","已清空排队消息")),i?.error){let e="";const t=W(i,r,r=>{e+=r});if(e.trim()&&it("info",e.trim()),"auth"===t)return Sr.relogin=!0,yt(),i}return gt(),i},yt=()=>{Fr.current=!0,pt(),Yr.current||(Ir?.(),Er.exit())},jt=async e=>{kr(ne.current.id,"human");const t=await kt(e);return t?.aborted||t?.error||Fr.current||Yr.current?.signal.aborted||br(ne.current)&&(await vr({session:ne.current,doTurn:kt,stats:()=>({usedTokens:$e.current.prompt+$e.current.completion,budgetTokens:Number(r.budgetTokens)||0}),onInfo:r=>it("info",r),aborted:()=>Fr.current||!!Yr.current?.signal.aborted}),y(ne.current)),t},It=async e=>{const t=e.trim();if(t){if(Yr.current)return/^\/(exit|quit)$/.test(t)?yt():t.startsWith("/")||t.startsWith("!")?void it("info","任务执行中,请先按 Esc 中断,再执行此命令。"):(Me({text:"",cursor:0}),it("user",t),ge.current.push(t),void Ce(ge.current.length));Me({text:"",cursor:0}),Gr(0),we.current=null,Q(Jr,t),xe.current.at(-1)!==t&&xe.current.push(t),Yr.current=new AbortController,oe.current=Yr.current,Ur.signalRef.current=Yr.current.signal,Br.current=Date.now(),qe("hidden"),at(!0);try{for(t.startsWith("/")?await(async e=>{const t=br(ne.current)?.rev,n={version:wr,config:r,cwd:Jr,select:r=>ft({...r,command:e.trim()}),signal:Yr.current?.signal,resumeSession:e=>{Fe.current&&(clearInterval(Fe.current),Fe.current=null),hr(ne.current.id),ne.current=e,r.sessionId=e.id,e.model&&(r.model=e.model),Pr.length=0,se.current.clear(),he.current.list=[],me.current.list.length=0,Re([]),Ue(e.lastPromptTokens||0),re.current=e.messages.filter(r=>["user","assistant"].includes(r.role)&&r.content).map(r=>({id:ee.current++,kind:r.role,text:"string"==typeof r.content?r.content:r.content.filter(r=>"text"===r.type).map(r=>r.text).join("\n")})),te.current="",Gr(0),ye()},usage:$e.current,permissions:wt,extraTools:Zr.current,session:ne.current,checkpoints:me.current,print:r=>it("info",String(r))},o=await A(e,n);if("exit"===o.action)return yt();if("clear"===o.action&&(y(ne.current),Fe.current&&(clearInterval(Fe.current),Fe.current=null),hr(ne.current.id),ne.current=k({cwd:Jr,model:r.model,messages:[ne.current.messages[0]]}),r.sessionId=ne.current.id,Pr.length=0,se.current.clear(),he.current.list=[],me.current.list.length=0,Re([]),Ue(0),re.current=[],te.current="",it("info","已开启新会话(旧会话仍可 --resume恢复)")),"refresh"===o.action&&(he.current.list=[],Re([]),Ue(0)),"autofix"===o.action&&await ar({command:o.command,maxIters:o.maxIters,signal:Yr.current?.signal,cwd:Jr,runTurnOnce:r=>kt(r),emit:r=>it("info",r)}),"autorun"===o.action&&await mr({goal:o.goal,config:r,permissions:wt,cwd:Jr,runTurnOnce:async r=>{const e=await kt(r);if(e?.aborted||e?.error||Yr.current?.signal.aborted)throw new Error("自动执行已停止");return e},emit:r=>it("info",r)}),"cd"===o.action){Fe.current&&(clearInterval(Fe.current),Fe.current=null);const e=await T(r,o.cwd,{queueInject:r=>Pr.push(r),ask:r=>Ur.ask(r),askUi:r=>Ur.askUi(r),sessionOf:()=>ne.current,signalRef:Ur.signalRef});ce.current=V(o.cwd),ue.current=_(o.cwd),hr(ne.current.id),$(a?.clients),Zr.current=e.extraTools,a&&(a.clients=e.clients),ne.current=k({cwd:o.cwd,model:r.model,messages:[{role:"system",content:M(o.cwd,{memory:!1!==r.memoryEnabled,config:r})}]}),r.sessionId=ne.current.id,Pr.length=0,se.current.clear(),xe.current=H(o.cwd),we.current=null,he.current.list=[],me.current.list.length=0,Re([]),Ue(0),re.current=[],te.current="",Xr(o.cwd),it("info","已切换到 "+o.cwd+"(新会话,旧会话已保存)")}if("loop"===o.action&&(Fe.current&&(clearInterval(Fe.current),Fe.current=null),o.stop?it("info","已停止循环。"):(it("info",`已开始循环:每 ${o.seconds}s运行「${o.prompt}」(/loop off停止)`),Fe.current=setInterval(()=>{"busy"!==pe.current&&It(o.prompt)},1e3*o.seconds))),o.prompt)it("user",o.prompt),await jt(o.prompt);else if(/^\/goal\s+/.test(e)&&!/^\/goal\s+(?:status|pause|clear|limit)(?:\s|$)/.test(e)&&"active"===br(ne.current)?.phase&&br(ne.current)?.rev!==t){const r="继续完成用户明确设定的目标:"+br(ne.current).objective;it("user",e),await jt(r)}})(t):t.startsWith("!")&&t.length>1?await(async e=>{it("user","! "+e),_r("$ "+e);const t=await K(Jr,{command:e},r,Yr.current?.signal);it("toolresult",t),ne.current.messages.push({role:"user",content:`我在终端直接执行了命令:$ ${e}\n${t.slice(0,8e3)}`}),y(ne.current)})(t.slice(1).trim()):(it("user",t),await jt(t));ge.current.length&&!Yr.current.signal.aborted&&!Fr.current;){const r=ge.current.shift();Ce(ge.current.length),await jt(r)}}catch(r){lt(),it("info",Yr.current?.signal.aborted?"操作已中断":"执行失败:"+r.message)}finally{dt(),ge.current=[],Ce(0),Yr.current=null,oe.current=null,Ur.signalRef.current=null,at(!1),qe("hidden"),_r(""),Fr.current&&(Ir?.(),Er.exit())}}};o(()=>{!de.current&&xr&&(de.current=!0,It(xr))},[]);const Tt=Qr?null:/^\/(\S*)$/.exec(Te.text),Mt=Tt?Tt[1]:null,$t=null==Mt?[]:P(Je,Mt),At=$t.length>0&&!_e;o(()=>{Ve(0),Be(!1)},[Mt]);const St=Qr||null!=Mt?null:/(?:^|\s)@([^\s@]*)$/.exec(Te.text),Pt=St?St[1]:null,Ut=null==Pt?[]:(()=>{const r=Pt.toLowerCase(),e=[];for(const t of tt){const n=t.toLowerCase(),o=n.split("/").pop();let s=-1;if(r?o.startsWith(r)?s=3:o.includes(r)?s=2:n.includes(r)&&(s=1):s=0,s>=0&&e.push({f:t,rank:s}),!r&&e.length>=50)break}return e.sort((r,e)=>e.rank-r.rank||r.f.length-e.f.length||r.f.localeCompare(e.f)),e.slice(0,50).map(r=>r.f)})(),Et=Ut.length>0&&!rt;o(()=>{Ze(0),et(!1)},[Pt]);const Rt=s(()=>{const r=new Set,e=[];for(let t=xe.current.length-1;t>=0&&e.length<50;t--){const n=xe.current[t];n&&!r.has(n)&&(r.add(n),e.push(n))}return e},[nt]);f(r=>{if(Hr.current){if(Hr.current.editing){const e=L(Hr.current,r);Hr.current=e,zr(e)}return}const e=r.replace(/\r\n?/g,"\n");we.current=null,Me(r=>({text:r.text.slice(0,r.cursor)+e+r.text.slice(r.cursor),cursor:r.cursor+e.length}))}),m((e,t)=>{const n=Mr(e);if(n)if(n.delta&&Hr.current){const r=Hr.current,e={...r,index:Math.max(0,Math.min(U(r).length-1,r.index-Math.sign(n.delta)))};Hr.current=e,zr(e)}else n.delta&&Nr(n.delta);else if(t.ctrl&&"c"===e)Yr.current?(pt(),it("info","已请求中断")):yt();else{if(t.ctrl&&t.end)return Gr(0);if(t.ctrl&&t.home)return Gr(Wr.current.max);if(t.ctrl&&(t.upArrow||t.downArrow))return Nr(t.upArrow?1:-1);if(!t.ctrl||"t"!==e||Hr.current){if(Hr.current){const r=Hr.current;if(r.kind){const n=q(r,e,t);return"result"in n?mt(n.result):(Hr.current=n.menu,void zr(n.menu))}const n=U(r),o=e=>{const t={...r,...e};Hr.current=t,zr(t)};if(t.escape)return mt();if(t.return)return n[r.index]?mt(n[r.index].value):void 0;if(t.upArrow||t.downArrow||t.pageUp||t.pageDown||"segments"===r.layout&&(t.leftArrow||t.rightArrow||t.tab)){const e=t.pageUp||t.pageDown?Math.max(1,Wr.current.menuPage||4):1,s=t.upArrow||t.pageUp||t.leftArrow||t.tab&&t.shift?-e:e;return o({index:Math.max(0,Math.min(n.length-1,r.index+s)),..."segments"===r.layout?{query:""}:{}})}const s=e=>{const t="segments"===r.layout?n.findIndex(r=>[r.label,r.value].join(" ").toLowerCase().includes(e.toLowerCase())):0;o({query:e,index:Math.max(0,t)})};return t.ctrl&&"u"===e?s(""):t.backspace||t.delete?s(r.query.slice(0,jr(r.query,r.query.length,-1))):!e||t.ctrl||t.meta||t.tab?void 0:s(r.query+e.replace(/[\x00-\x1f\x7f]/g,""))}if((t.pageUp||t.pageDown)&&!At&&!Et&&!nt)return Nr((t.pageUp?1:-1)*Math.max(1,Wr.current.page-1));if(t.ctrl&&"p"===e)return Me({text:"/",cursor:1}),void Be(!1);if(nt){if(!Rt.length)return void ot(!1);if(t.upArrow)return ct(r=>(r-1+Rt.length)%Rt.length);if(t.downArrow)return ct(r=>(r+1)%Rt.length);if(t.pageUp)return ct(r=>Math.max(0,r-4));if(t.pageDown)return ct(r=>Math.min(Rt.length-1,r+4));if(t.return){const r=Rt[Math.min(st,Rt.length-1)]||"";return ot(!1),we.current=null,Me({text:r,cursor:r.length})}return t.escape?ot(!1):void 0}if(t.tab&&t.shift&&!Yr.current){const e=ur[(ur.indexOf(wt.getMode())+1)%ur.length];return wt.setMode(o=e),r.planMode="plan"===o,r.sandbox="sandbox"===o,void it("info",`对话模式 → ${ir[o]} ${lr[o]}`+("auto"===o?"(已开放所有权限,任何命令免审批)":""))}var o;if(At){const r=()=>$t[Math.min(Ke,$t.length-1)];if(t.upArrow)return Ve(r=>(r-1+$t.length)%$t.length);if(t.downArrow)return Ve(r=>(r+1)%$t.length);if(t.pageUp)return Ve(r=>Math.max(0,r-4));if(t.pageDown)return Ve(r=>Math.min($t.length-1,r+4));if(t.tab){const e=r().name;return Me({text:"/"+e+" ",cursor:e.length+2})}if(t.return){const e=r();return!e.menu&&e.args?.startsWith("<")?Me({text:"/"+e.name+" ",cursor:e.name.length+2}):void It("/"+e.name)}if(t.escape)return Be(!0)}if(Et){const r=()=>Ut[Math.min(Xe,Ut.length-1)];if(t.upArrow)return Ze(r=>(r-1+Ut.length)%Ut.length);if(t.downArrow)return Ze(r=>(r+1)%Ut.length);if(t.pageUp)return Ze(r=>Math.max(0,r-4));if(t.pageDown)return Ze(r=>Math.min(Ut.length-1,r+4));if(t.tab||t.return){const e=r(),t=Te.text.length-(Pt?Pt.length:0)-1,n=Te.text.slice(0,t+1)+e+" ";return Me({text:n,cursor:n.length})}if(t.escape)return et(!0)}if(t.escape){if("busy"===pe.current&&oe.current)return pt(),void it("info","已请求中断(Esc)");const r=Date.now();return r-ut.current<600&&xe.current.length?(ut.current=0,ct(0),void ot(!0)):void(ut.current=r)}if(t.ctrl&&"c"===e)"busy"===pe.current&&oe.current?(pt(),it("info","已请求中断")):yt();else{if(t.return&&(t.shift||t.meta)||t.ctrl&&"j"===e)return Me(r=>({text:r.text.slice(0,r.cursor)+"\n"+r.text.slice(r.cursor),cursor:r.cursor+1}));if(t.return)It(Te.text);else{if(t.upArrow)return(()=>{const r=xe.current;if(!r.length)return;null==we.current?(ve.current=Te.text,we.current=r.length-1):we.current>0&&we.current--;const e=r[we.current]??"";Me({text:e,cursor:e.length})})();if(t.downArrow)return(()=>{if(null==we.current)return;const r=xe.current;if(we.current<r.length-1){we.current++;const e=r[we.current];Me({text:e,cursor:e.length})}else we.current=null,Me({text:ve.current,cursor:ve.current.length})})();if(t.leftArrow)return Me(r=>({...r,cursor:jr(r.text,r.cursor,-1)}));if(t.rightArrow)return Me(r=>({...r,cursor:jr(r.text,r.cursor,1)}));if(t.ctrl&&"a"===e)return Me(r=>({...r,cursor:0}));if(t.ctrl&&"e"===e)return Me(r=>({...r,cursor:r.text.length}));if(t.ctrl&&"u"===e)return Me(r=>({text:r.text.slice(r.cursor),cursor:0}));if(t.ctrl&&"k"===e)return Me(r=>({text:r.text.slice(0,r.cursor),cursor:r.cursor}));if(t.ctrl&&"w"===e)return Me(r=>{const e=r.text.slice(0,r.cursor).replace(/\S+\s*$/,"");return{text:e+r.text.slice(r.cursor),cursor:e.length}});if(t.backspace||t.delete)return we.current=null,Me(r=>{const e=t.backspace?jr(r.text,r.cursor,-1):r.cursor,n=t.backspace?r.cursor:jr(r.text,r.cursor,1);return{text:r.text.slice(0,e)+r.text.slice(n),cursor:e}});if(e&&!t.ctrl&&!t.meta){we.current=null;const r=e.replace(/\r\n?/g,"\n");Me(e=>({text:e.text.slice(0,e.cursor)+r+e.text.slice(e.cursor),cursor:e.cursor+r.length}))}}}}else he.current.list.length&&qe(r=>"expanded"===r?"busy"===pe.current?"collapsed":"hidden":"expanded")}});const Dt=s(()=>re.current.slice(),[ke]),qt=O(He),Lt=qt?"exhausted"===qt.level?`⛔ ${qt.label}用量已用尽${G(qt.resetAtEpochMs)?" · "+G(qt.resetAtEpochMs):""}`:`⚠ ${qt.label}用量 ${Math.round(100*qt.pct)}%(${Ar(qt.used)}/${Ar(qt.limit)})`:"";return $r(yr,{columns:qr.columns,rows:qr.rows,version:wr,cwd:Jr,model:r.model,theme:r.theme,accent:r.accentColor,mode:`${ir[Oe]} ${lr[Oe]}`,logItems:Dt,live:te.current,liveId:ee.current,thinking:ht.current,todos:Ee,taskView:De,busy:"busy"===je,activity:Ne||Vr,elapsed:Math.floor((Date.now()-Br.current)/1e3),pending:null,question:null,menu:Qr,input:Te,candidates:nt?Rt:At?$t.map(r=>"/"+r.name+(r.args?" "+r.args:"")+" ["+r.category+"] "+r.desc):Et?Ut.map(r=>"@"+r):[],selected:nt?st:At?Ke:Xe,candidateTitle:nt?"输入历史":At?"命令与技能":"引用文件",offset:Cr.offset,scrollRevision:Cr.revision,scrollRef:Wr,usage:{context:Ar(Pe),total:Ar(Ae.prompt+Ae.completion)},queueLen:Le,warning:Lt,toolCount:Zr.current.length})}export async function runInk({config:r,cwd:e,session:t,yolo:n,initialPrompt:o,version:s}){wr().catch(()=>{}),c(r).catch(()=>{}),r.sessionId=t.id;const u=[],m={signalRef:{current:null},sessionOf:()=>t},f={clients:[]},d={};let p,g=!1;const h=()=>{g=!0,p?.unmount()};try{p=i($r(l,{flexDirection:"column"},$r(Ir,{columns:process.stdout.columns,rows:process.stdout.rows,version:s}),$r(a,{dimColor:!0},"正在加载技能与工具…")),{alternateScreen:!0,interactive:!(!process.stdout.isTTY||!process.stdin.isTTY),exitOnCtrlC:!1,maxFps:20}),process.once("SIGTERM",h),process.once("SIGHUP",h),process.once("SIGINT",h);const c=await T(r,e,{queueInject:r=>u.push(r),sessionOf:()=>m.sessionOf(),signalRef:m.signalRef,ask:r=>m.ask?m.ask(r):Promise.resolve("终端尚未就绪"),askUi:r=>m.askUi?m.askUi(r):Promise.resolve({value:null}),runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});return f.clients=c.clients,g?d:(process.removeListener("SIGINT",h),p.rerender($r(App,{config:r,cwd:e,session:t,extraTools:c.extraTools,mcp:f,yolo:n,initialPrompt:o,version:s,exitState:d,injectQueue:u,bridge:m})),await p.waitUntilExit(),d)}finally{process.removeListener("SIGINT",h),process.removeListener("SIGTERM",h),process.removeListener("SIGHUP",h),p?.unmount(),$(f.clients),gr(),xr(),dr()}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"react";import{Box as t,Text as n}from"ink";import r from"strip-ansi";import o from"string-width";const i=e.createElement,a=e=>r(String(e||"")).replace(/[\x00-\x1f\x7f-\x9f]/g," "),l=new Intl.Segmenter(void 0,{granularity:"grapheme"});function s(e,t){let n="";for(const{segment:r}of l.segment(a(e))){if(o(n+r)>t)break;n+=r}return n}export const menuOptions=e=>e.kind||"segments"===e.layout?e.options:e.options.filter(t=>[t.label,t.value,t.description].join(" ").toLowerCase().includes((e.query||"").toLowerCase()));export function menuLayout(e,t,n=9){const r=Math.max(8,Math.min(t,88)),o="segments"===e.layout,i=menuOptions(e),a=Math.max(0,Math.min(e.index,i.length-1)),l=Math.max(3,Math.floor(n)),s=!o&&!!e.query&&l>=6,m=l>=(o?4:7),c=o||e.editing?1:Math.min(Math.max(1,i.length),4,l-2-Number(s)-Number(m));return{width:r,horizontal:o,options:i,index:a,count:c,start:Math.max(0,Math.min(a-Math.floor(c/2),i.length-c)),search:s,detail:m,height:2+c+Number(s)+Number(m)}}export function CommandMenu({menu:e,columns:r,maxHeight:m,theme:c,accent:u}){const h=menuLayout(e,r,m),{width:d,horizontal:g,options:p,index:f,count:x,start:$,search:E,detail:M,height:b}=h,v=p[f],w=(y=u||"cyan","mono"===c?void 0:y);var y;const k=(e,t={})=>i(n,{wrap:"truncate-end",...t},a(e)),C=`${p.length?f+1:0}/${p.length}`+(g?"":` ${$?"↑":""}${$+x<p.length?"↓":""}`),L=s(e.title,d-o(C)-7),N=`╭─ ${L} `+"─".repeat(Math.max(0,d-o(L+C)-6))+` ${C}`,q=g?"←→":"↑↓",S=e.editing?d<35?"Enter 提交 · Esc 返回":"输入补充意见 · Enter 提交 · Esc 返回":e.kind?d<35?`${q} Enter · Esc`:`${q} 选择${e.multiple?" · 空格 勾选":""} · Enter 确认 · Esc 取消`:d<28?`${q} Enter · Esc`:d<48?`${q} · Enter确认 · Esc取消`:`${q} 选择 · Enter 确认 · Esc 取消${g?"":" · 输入筛选"}`,j=g?function(e,t,n,r){const i=e.map((e,r)=>` ${r===t?"❯ ":""}${a(e.label)}${e.value===n?" ✓":""} `),l=Math.max(2,r-6);let m=t,c=t+1,u=o(i[t]||"");for(;;){let t=!1;if(m>0&&u+o(i[m-1])<=l&&(u+=o(i[--m]),t=!0),c<e.length&&u+o(i[c])<=l&&(u+=o(i[c++]),t=!0),!t)break}return{start:m,end:c,labels:i.slice(m,c).map(e=>s(e,l))}}(p,f,e.current,d):null;return i(t,{width:d,height:b,flexDirection:"column",flexShrink:0,overflow:"hidden"},k(N,{color:w,bold:!0}),...E?[k("│ 搜索 › "+e.query,{dimColor:!0})]:[],...e.editing?[k("│ › "+s([...l.segment(e.answerText.slice(0,e.cursor))].slice(-Math.max(1,Math.floor((d-8)/2))).map(e=>e.segment).join(""),d-8)+"▏"+e.answerText.slice(e.cursor))]:g?[i(n,{wrap:"truncate-end"},i(n,{color:w},"│ "),j.start>0?"‹ ":" ",...j.labels.map((e,t)=>i(n,{key:j.start+t,inverse:j.start+t===f,bold:j.start+t===f,color:j.start+t===f?w:void 0},e)),j.end<p.length?" ›":"")]:p.length?p.slice($,$+x).map((t,n)=>k(`│ ${$+n===f?"❯":" "} ${e.multiple&&!t.custom?e.selected.includes(t.value)?"☑ ":"☐ ":""}${t.label}${e.kind||t.value!==e.current?"":" ✓ 当前"}`,{key:t.value,color:$+n===f?w:void 0,bold:$+n===f})):[k("│ 没有匹配项;退格清除搜索",{dimColor:!0})],...M?[k("│ · "+(e.editing?"仅此处接受文字;Esc 返回选择视图":v?.description||(v?v.value===e.current?"当前正在使用此选项":"回车应用此选项":"Esc 返回")),{dimColor:!0})]:[],k("╰─ "+S,{dimColor:!0}))}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function formatElapsed(t=0){const e=Math.max(0,Math.floor(Number.isFinite(t)?t:0));if(e<60)return`${e}秒`;const o=Math.floor(e/60);return o<60?`${o}分 ${e%60}秒`:`${Math.floor(o/60)}时 ${o%60}分 ${e%60}秒`}export function taskRows(t=[],e="hidden",o=6){if(!t.length||"hidden"===e||o<1)return[];const s=t.filter(t=>"completed"===t.status).length,n=["in_progress","pending","completed"].flatMap(e=>t.filter(t=>t.status===e)),r="expanded"===e?n.slice(0,Math.min(5,o-1)):[],l=t.length-r.length;return[{text:`任务 ${s}/${t.length}${r.length&&l?` · 另 ${l} 项`:""} · Ctrl+T ${"expanded"===e?"收起":"展开"}`,dimColor:!0},...r.map(t=>({text:` ${"completed"===t.status?"✔":"in_progress"===t.status?"◼":"◻"} ${t.content.replace(/\s+/g," ")}`,color:"in_progress"===t.status?"cyan":void 0,bold:"in_progress"===t.status,dimColor:"completed"===t.status,strikethrough:"completed"===t.status}))]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function mouseReport(t){const e=/^(?:\x1b)?\[<(\d+);(\d+);(\d+)([Mm])$/.exec(t);if(!e)return null;const n=195&Number(e[1]);return{delta:"M"===e[4]?64===n?3:65===n?-3:0:0}}export function enableMouseTracking(t){if(!t.isTTY)return()=>{};const e=e=>{try{t.write(e)}catch{}};e("[?1000h[?1006h");let n=!0;const r=()=>{n&&(n=!1,e("[?1006l[?1000l"),process.removeListener("exit",r))};return process.once("exit",r),r}export function selectViewport(t,e,n,r){const o=Math.max(0,t.length-e);let a=Math.max(0,Math.min(o,n.offset));const s=r?.revision===n.revision&&r?.requested===n.offset;if(s&&r.reading){let n=-1;if(r.anchor)for(let e=0;e<t.length;e++){const o=t[e].anchor;o?.source===r.anchor.source&&o.line===r.anchor.line&&o.column<=r.anchor.column&&(n=e)}const s=n>=0?n:Math.min(r.start,o);a=Math.max(0,Math.min(o,t.length-e-s))}const i=o-a,c=Math.min(t.length,i+e),l=s?!!r?.reading&&a>0:a>0,h=n.contentSize??t.length,f=s&&l?r.baseline:h;return{lines:t.slice(i,c),offset:a,maxOffset:o,start:i,end:c,total:t.length,unread:l?Math.max(0,h-f):0,state:{revision:n.revision,requested:n.offset,reading:l,anchor:t[i]?.anchor,start:i,baseline:f}}}export function scrollbar(t,e){if(!t.maxOffset)return Array(e).fill(" ");const n=Math.max(1,Math.floor(e*e/t.total)),r=Math.round(t.start/t.maxOffset*(e-n));return Array.from({length:e},(t,e)=>e>=r&&e<r+n?"┃":"│")}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"react";import{Box as o,Text as r}from"ink";import t from"string-width";import n from"strip-ansi";export const BRAND="HuiCai AI";export function welcomeRows({columns:e=80,rows:o=24,version:r,compact:c=!1}={}){const i=Math.max(8,e),m=n(String(r||"")).replace(/[\x00-\x1f\x7f]/g,""),a=BRAND+(m?" v"+m:""),l=(e,o)=>{let r="";for(const n of e){if(t(r+n)>o)break;r+=n}return r};if(c||o<16||i<28)return[l(a+" · Welcome",i)];const p=i-2,s=l("─ "+a+" ",p),f=e=>{const o=l(e,p);return"│"+o+" ".repeat(p-t(o))+"│"};return["╭"+s+"─".repeat(p-t(s))+"╮",...o>=22&&i>=46?[f("")]:[],f(" ✦ Welcome to HuiCai AI"),...o>=22&&i>=46?[f(" Code · Skills · Automation")]:[],"╰"+"─".repeat(p)+"╯"]}export function WelcomeBanner(t){return e.createElement(o,{flexDirection:"column"},...welcomeRows(t).map((o,t)=>e.createElement(r,{key:t,color:"cyan",bold:!0},o)))}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{CommandMenu as e,menuLayout as t}from"./command-menu.js";import n,{useMemo as o,useRef as r}from"react";import{Box as l,Text as i}from"ink";import a from"strip-ansi";import s from"wrap-ansi";import c from"string-width";import{BRAND as m,welcomeRows as h}from"./welcome.js";import{selectViewport as p,scrollbar as u}from"./scroll.js";import{formatElapsed as d,taskRows as f}from"./progress.js";const g=n.createElement,x=new Intl.Segmenter(void 0,{granularity:"grapheme"});export const graphemes=e=>[...x.segment(String(e))];export function moveCursor(e,t,n){const o=[...graphemes(e).map(e=>e.index),e.length];return n<0?o.filter(e=>e<t).pop()??0:o.find(e=>e>t)??e.length}export function cleanText(e){return a(String(e??"")).replace(/\r\n?/g,"\n").replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g,"").replace(/\t/g," ")}export function styledText(e){return String(e??"").split(/(\x1b\[[0-9;]*m)/g).map(e=>/^\x1b\[[0-9;]*m$/.test(e)?e:cleanText(e)).join("")}export function linesOf(e,t){return s(styledText(e),Math.max(2,t),{hard:!0,trim:!1,wordWrap:!1}).split("\n")}export function viewport(e,t,n=0){const o=Math.max(0,e.length-t),r=Math.min(o,Math.max(0,n)),l=e.length-r;return{lines:e.slice(Math.max(0,l-t),l),offset:r,maxOffset:o}}export function inputWindow(e,t,n,o=3){const r=[[]];let l=0,i=0;for(const o of[...graphemes(e),{segment:" ",index:e.length}]){const e=o.index===t,a="\n"===o.segment?" ":cleanText(o.segment),s=c(a);l+s>n&&r.at(-1).length&&(r.push([]),l=0),e&&(i=r.length-1),r.at(-1).push({text:a,caret:e}),l+=s,"\n"===o.segment&&(r.push([]),l=0)}const a=Math.max(0,i-o+1);return r.slice(a,a+o)}const y={user:"cyan",tool:"cyan",toolresult:"gray",skip:"gray",info:"yellow",diff:"green"},k={user:"› ",tool:" ▶ ",toolresult:" ",skip:" ⊘ ",info:" · ",diff:" "};function w(e,t,n){return styledText(e.text).split("\n").flatMap((o,r)=>{let l=0;return linesOf(o,n).map(n=>{const o={text:n,color:e.color,bold:e.bold,anchor:{source:t,line:r,column:l}};return l+=a(n).length,o})})}export function Workbench({columns:n,rows:a,version:s,cwd:x,model:$,mode:v,theme:M,accent:b,logItems:C,live:E,liveId:T,thinking:S,todos:D,taskView:O="hidden",busy:P,activity:j,elapsed:q,pending:W,question:I,menu:U,input:H,candidates:R,selected:z,candidateTitle:A,offset:J=0,scrollRevision:L=0,scrollRef:N,usage:V,queueLen:B,warning:F,toolCount:G}){const K=Math.max(8,n),Q=K-1,X=Math.max(5,a-1),Y=inputWindow(H.text,H.cursor,K-2,X<14?1:3),Z=Math.min(R.length,Math.max(0,Math.min(4,X-12))),_=Math.max(0,Math.min(z-Math.floor(Z/2),R.length-Z)),ee=R.slice(_,_+Z),te=W?W.consent?"[1/y] 允许一次 [2/a] 本会话 [3/n/Esc] 拒绝":"[y] 一次 [a] 本会话 [A] 项目规则 [n/Esc] 拒绝":I?"请输入回答 · Enter 提交 · Esc 取消":K>=90?"滚轮 / PgUp/PgDn 翻阅 · Ctrl+Home 顶部 · Ctrl+End 最新 · Enter 发送 · Ctrl+P 命令":"滚轮 / PgUp/PgDn 翻阅 · Ctrl+End 最新",ne=h({columns:K,rows:a,version:s,compact:!!(C.length||P||W||I||ee.length)}),oe=X>=14?3:1,re=X>=14?1:0,le=!U?.kind&&X>=6?1:0,ie=Math.min(8,Math.max(3,X-oe-re-le-1)),ae=U?t(U,K,ie):null,se=6+ne.length+Y.length+(ee.length?ee.length+1:0),ce=U?[]:f(D,O,Math.max(0,X-(X<9?3:se)-2)),me=Math.max(1,U?X-oe-re-le-ae.height:(X<9?X-3:X-se)-ce.length),he=o(()=>(C.length?C:[{kind:"assistant",text:"开始你的工程任务\n\n直接描述需求,或用 @ 引用文件、! 执行命令。\n/init 项目约定 /inspect 代码体检 /verify 测试与修复\n/skills 技能 /mcp 扩展工具 /doctor capabilities 能力诊断\n/auto 规划与执行 /goal 目标管理 /help 全部命令\n\n计算机操作、浏览器与持久终端按本机配置和工具可用性调用。"}]).flatMap((e,t)=>w({text:(k[e.kind]||"")+e.text,color:y[e.kind],bold:"user"===e.kind},`message:${e.id??t}`,Q)),[C,Q]),pe=[];S&&pe.push({source:"thinking",color:"gray",text:"✦ "+S}),E&&pe.push({source:`message:${T??"live"}`,text:E}),W&&pe.push({source:"permission",color:"yellow",text:"需要授权:"+W.label+(W.risk?"\n⚠ "+W.risk:"")+"\n"+JSON.stringify(W.args,null,2)}),U?.kind&&pe.push({source:"selection",color:"approval"===U.kind?"yellow":"cyan",text:U.title+(U.context?"\n"+U.context:"")}),I&&pe.push({source:"question",color:"yellow",text:I});const ue=[...he,...pe.flatMap(e=>w(e,e.source,Q))],de=r(null),fe=p(ue,me,{offset:J,revision:L,contentSize:ue.filter(e=>0===e.anchor.column).length},de.current);de.current=fe.state,N.current={max:fe.maxOffset,page:me,offset:fe.offset,menuPage:ae?.count};const ge=e=>"mono"===M?void 0:"cyan"===e&&b||e,xe=(e,t={})=>g(i,{wrap:"truncate-end",...t,color:ge(t.color)},"mono"===M?cleanText(e):styledText(e)),ye=ce.map(({text:e,...t},n)=>{const o=cleanText(e);return xe(c(o)>K?linesOf(o,K-1)[0]+"…":o,{key:`task-${n}`,...t})}),ke=g(l,{height:me,flexShrink:0,overflow:"hidden"},g(l,{width:Q,flexDirection:"column"},...fe.lines.map((e,t)=>xe(e.text,{key:t,color:e.color,bold:e.bold}))),g(l,{width:1,flexDirection:"column"},...u(fe,me).map((e,t)=>g(i,{key:t,color:ge("┃"===e?"cyan":"gray")},e)))),we=`回看 ${fe.start+1}-${fe.end}/${fe.total} 行`+(fe.unread?` · 新增 ${fe.unread} 行`:""),$e=d(q),ve=linesOf(cleanText(j||"正在处理").replace(/\n/g," "),Math.max(2,K-c(`◐ · ${$e}`)))[0];return U?g(l,{flexDirection:"column",width:K,height:X,overflow:"hidden"},xe(m+(s?" v"+s:""),{color:"cyan",bold:!0}),...oe>1?[xe(`${$} · ${v}`,{dimColor:!0}),xe(x,{dimColor:!0})]:[],ke,...re?[xe((fe.offset?we:"approval"===U.kind?"● 等待授权":"question"===U.kind?"● 等待回答"+(U.multiple?" · 已选 "+U.selected.length+" 项":""):"● 选择设置")+(P?` · ${d(q)}`:""),{dimColor:!0})]:[],...le?[xe("› "+(U.command||""),{dimColor:!0})]:[],g(e,{menu:U,columns:K,maxHeight:ie,theme:M,accent:b})):X<9?g(l,{flexDirection:"column",width:K,height:X,overflow:"hidden"},xe(W?"等待授权 · PgUp 查看详情":I?"等待回答":P?`正在处理 · ${d(q)} · Esc 中断`:m+" · 就绪",{color:"cyan"}),ke,...ye,g(i,{wrap:"truncate-end"},"› ",...Y.flat().map((e,t)=>g(i,{key:t,inverse:e.caret},e.text))),xe(W?"[y]允许 [n/Esc]拒绝":I?"Enter 回答 · Esc 取消":fe.offset?we:"滚轮翻阅 · Ctrl+End 最新",{dimColor:!0})):g(l,{flexDirection:"column",width:K,height:X,overflow:"hidden"},...ne.map((e,t)=>xe(e,{key:`banner-${t}`,color:"cyan",bold:!0})),xe(`${$} · ${v} · 扩展工具 ${G}`,{dimColor:!0}),xe(`${x} · 上下文 ${V.context} · 本次 ${V.total} tokens`,{dimColor:!0}),ke,...ye,xe((fe.offset&&!P?we+" · ":"")+(W?"● 等待授权":I?"● 等待回答":P?fe.offset?`◐ ${$e} · ${we}`:`◐ ${ve} · ${$e}`:"● 就绪")+(B?` · 排队 ${B}`:"")+(F?" · "+F:""),{color:W||I||F?"yellow":"green"}),...ee.length?[xe(`${A} · ↑↓ 选择 · Tab 补全 · Esc 关闭`,{dimColor:!0}),...ee.map((e,t)=>xe(`${_+t===z?"❯":" "} ${e}`,{key:t,color:_+t===z?"cyan":"gray"}))]:[],xe("─".repeat(K),{color:"blue"}),...Y.map((e,t)=>g(l,{key:`input-${t}`,height:1,flexShrink:0},g(i,{color:ge("cyan")},t?" ":"› "),g(i,{wrap:"truncate-end"},...e.map((e,t)=>g(i,{key:t,inverse:e.caret},e.text))))),xe("─".repeat(K),{color:"blue"}),xe(te,{dimColor:!0}))}
|
package/src/ui-spec.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import t from"node:fs";import e from"node:path";export const CATALOG={Dashboard:{desc:"仪表盘容器。顶层用它把若干区块竖向排开。",props:{title:"string?",subtitle:"string?",children:"ref[]"},required:["children"]},Row:{desc:"横向排列若干子组件(指标卡并排时用)。",props:{children:"ref[]|each",gap:"number?"},required:["children"]},Column:{desc:"纵向排列若干子组件。",props:{children:"ref[]|each",gap:"number?"},required:["children"]},Card:{desc:"带标题的卡片容器,内放一个子组件。",props:{title:"string",subtitle:"string?",child:"ref"},required:["title","child"]},Metric:{desc:"单个关键指标:标签 + 数值 + 可选同比/环比。KPI用它。",props:{label:"string",value:"string",trend:"up|down|flat?",delta:"string?",hint:"string?"},required:["label","value"]},Table:{desc:"数据表格。columns给列定义,rows绑定到数据里的数组。",props:{columns:"column[]",rows:"path",caption:"string?"},required:["columns","rows"]},Chart:{desc:"图表。kind=bar/line/pie;series绑定到数据里的数组,x/y指字段名。",props:{kind:"bar|line|pie",series:"path",x:"string",y:"string",title:"string?"},required:["kind","series","x","y"]},Checklist:{desc:"检查清单。items绑定到数组,每项含text与done。",props:{items:"path",title:"string?"},required:["items"]},Timeline:{desc:"时间线 / 项目进度。items绑定到数组,每项含time、title、可选status/desc。",props:{items:"path",title:"string?"},required:["items"]},Text:{desc:"一段说明文字(支持简单Markdown行内语法)。",props:{text:"string",tone:"normal|muted|warn?"},required:["text"]},KeyValue:{desc:"键值对清单(文档摘要卡、参数一览用)。items绑定到数组,每项含k与v。",props:{items:"path",title:"string?"},required:["items"]}};const n=new Set(["Dashboard","Column","Row","Card","Table","Chart","Checklist","Timeline","KeyValue","Text","Metric"]);export function catalogText(){return"可用界面组件(只能用这些,不得自造):\n"+Object.entries(CATALOG).map(([t,e])=>{const n=Object.entries(e.props).map(([t,e])=>`${t}:${e}`).join(" ");return` ${t}(${e.desc})必填 ${e.required.join("/")};字段 ${n}`}).join("\n")}const r=t=>null!==t&&"object"==typeof t&&!Array.isArray(t),o=t=>r(t)&&"string"==typeof t.componentId&&"string"==typeof t.path;export function resolvePath(t,e){if(!e)return;let n=t;for(const t of String(e).replace(/^\//,"").split(".")){if(null==n)return;n=n[t]}return n}export function validateSpec(t){const e=[];if(!r(t))return{ok:!1,errors:["spec必须是对象"]};const i=Array.isArray(t.components)?t.components:null;if(!i||!i.length)return{ok:!1,errors:["components必须是非空数组"]};const s=new Map;for(const t of i)r(t)&&"string"==typeof t.id&&t.id?(s.has(t.id)&&e.push(`组件id重复:${t.id}`),s.set(t.id,t)):e.push("每个组件都要有字符串id");const a=s.get("root")||i[0];a||e.push("缺少根组件(id为root,或数组首项)");for(const t of s.values()){const n=CATALOG[t.component];if(n){for(const r of n.required)void 0!==t[r]&&null!==t[r]&&""!==t[r]||e.push(`${t.id}(${t.component})缺必填字段 ${r}`);for(const n of["child","children"]){const r=t[n];if(void 0===r)continue;const i=Array.isArray(r)?r:o(r)?[r.componentId]:[r];for(const n of i)"string"==typeof n&&(s.has(n)||e.push(`${t.id} 引用了不存在的组件 ${n}`));o(r)&&!s.has(r.componentId)&&e.push(`${t.id} 的列表模板 ${r.componentId} 不存在`)}}else e.push(`未注册的组件:${t.component}(可用:${Object.keys(CATALOG).join("/")})`)}return a&&!n.has(a.component)&&e.push(`根组件不能是 ${a.component}`),e.length?{ok:!1,errors:e}:{ok:!0,errors:[],root:a,byId:s}}const i=(t,e)=>{if((t=>r(t)&&"string"==typeof t.path)(t)){const n=resolvePath(e,t.path);return null==n?"":String(n)}return null==t?"":String(t)},s={up:"↑",down:"↓",flat:"→"};function a(t,e,n=0){if(!t||n>12)return"";const{byId:r,data:c}=e,p=t=>{const o=r.get(t);return o?a(o,e,n+1):""},l=(t,e="\n\n")=>{if(o(t)){const o=resolvePath(c,t.path);if(!Array.isArray(o))return"";const i=r.get(t.componentId);return i?o.map(t=>a(i,{byId:r,data:t},n+1)).filter(Boolean).join(e):""}return Array.isArray(t)?t.map(p).filter(Boolean).join(e):"string"==typeof t?p(t):""};switch(t.component){case"Dashboard":return(t.title?`## ${i(t.title,c)}\n`+(t.subtitle?`${i(t.subtitle,c)}\n`:""):"")+"\n"+l(t.children);case"Row":case"Column":{const e=(o(t.children)?[t.children.componentId]:Array.isArray(t.children)?t.children:[t.children]).every(t=>"string"==typeof t&&"Metric"===r.get(t)?.component);return l(t.children,e?"\n":"\n\n")}case"Card":return`**${i(t.title,c)}**`+(t.subtitle?` ${i(t.subtitle,c)}`:"")+"\n\n"+p(t.child);case"Metric":{const e=t.trend?` ${s[i(t.trend,c)]||""}${i(t.delta,c)}`:"",n=t.hint?i(t.hint,c):"";return`- **${i(t.label,c)}**:${i(t.value,c)}${e}`+(n?`(${n})`:"")}case"Text":{const e=i(t.tone,c),n=i(t.text,c);return"muted"===e?`_${n}_`:"warn"===e?`⚠️ ${n}`:n}case"Table":{const e=Array.isArray(t.columns)?t.columns:[],n=resolvePath(c,"string"==typeof t.rows?t.rows:t.rows?.path);if(!e.length||!Array.isArray(n))return"";const r=`| ${e.map(t=>t.title||t.key).join(" | ")} |`,o=`| ${e.map(()=>"---").join(" | ")} |`,s=n.map(t=>`| ${e.map(e=>{const n=t?.[e.key];return null==n?"":String(n)}).join(" | ")} |`);return(t.caption?`${i(t.caption,c)}\n\n`:"")+[r,o,...s].join("\n")}case"Chart":{const e=resolvePath(c,"string"==typeof t.series?t.series:t.series?.path);if(!Array.isArray(e))return"";return(t.title?`**${i(t.title,c)}**(${i(t.kind,c)} 图)\n\n`:"")+[`| ${t.x} | ${t.y} |\n| --- | --- |`,...e.map(e=>`| ${e?.[t.x]??""} | ${e?.[t.y]??""} |`)].join("\n")}case"Checklist":{const e=resolvePath(c,"string"==typeof t.items?t.items:t.items?.path);return Array.isArray(e)?(t.title?`**${i(t.title,c)}**\n\n`:"")+e.map(t=>`- [${t?.done?"x":" "}] ${t?.text??""}`).join("\n"):""}case"Timeline":{const e=resolvePath(c,"string"==typeof t.items?t.items:t.items?.path);return Array.isArray(e)?(t.title?`**${i(t.title,c)}**\n\n`:"")+e.map(t=>`- \`${t?.time??""}\` **${t?.title??""}**`+(t?.status?` [${t.status}]`:"")+(t?.desc?`\n ${t.desc}`:"")).join("\n"):""}case"KeyValue":{const e=resolvePath(c,"string"==typeof t.items?t.items:t.items?.path);return Array.isArray(e)?(t.title?`**${i(t.title,c)}**\n\n`:"")+e.map(t=>`- **${t?.k??""}**:${t?.v??""}`).join("\n"):""}default:return""}}export function toMarkdown(t){const e=validateSpec(t);if(!e.ok)return"";const n=a(e.root,{byId:e.byId,data:t.data||{}});return String(n||"").replace(/\n{3,}/g,"\n\n").trim()}export function componentsUsed(t){const e=Array.isArray(t?.components)?t.components:[];return[...new Set(e.map(t=>t?.component).filter(Boolean))]}export function defaultUiPath(t){const n=String(t?.title||"界面").replace(/[\\/:*?"<>|\s]+/g,"-").slice(0,40);return e.join("docs","surfaces",`${n||"界面"}.ui.json`)}export function makeUiTools({cwd:t}){return[{readOnly:!0,spec:{type:"function",function:{name:"ui_catalog",description:"查看可用的界面组件目录与字段说明(做仪表盘/看板/清单前不确定字段就先查,别猜组件名)。",parameters:{type:"object",properties:{},required:[]}}},run:async()=>catalogText()+'\n\n写法:components给组件树(每个组件一个id,root为根),data单独给数据,\n两者靠 {"path":"字段名"} 绑定;列表用 {"componentId":"模板id","path":"数组字段"} 展开。\n示例:\n'+JSON.stringify(EXAMPLE_SPEC,null,2)},{readOnly:!1,spec:{type:"function",function:{name:"render_ui",description:"把结构化数据渲染成**可视化界面**:仪表盘、指标卡、数据表格+图表、检查清单、项目时间线、文档摘要卡。用户要「看板/仪表盘/做成表/可视化/一览」这类产出物时用它,不要自己拼Markdown表格糊弄。\n**你只给内容结构与数据,排版与呈现由引擎负责**——不要手写HTML/CSS。\n组件只能用注册过的那些(先调ui_catalog看清单),自造组件名会被判非法。",parameters:{type:"object",properties:{path:{type:"string",description:"输出路径(相对项目根,如docs/surfaces/销售看板.ui.json);省略则按标题自动命名"},spec:{type:"object",description:'界面描述:{ title?, components:[{id,component,...}], data:{} }。组件树写结构、data给数据,靠 {"path":"字段"} 绑定。',additionalProperties:!0,properties:{title:{type:"string",description:"界面标题"},components:{type:"array",description:"组件数组,每项含id与component;根组件id用root",items:{type:"object",additionalProperties:!0}},data:{type:"object",description:"数据对象,被组件树里的path绑定引用",additionalProperties:!0}},required:["components"]}},required:["spec"]}}},run:async e=>renderUi(t,e)}]}export const EXAMPLE_SPEC={title:"季度销售看板",components:[{id:"root",component:"Dashboard",title:{path:"title"},children:["kpis","trend"]},{id:"kpis",component:"Row",children:["m1","m2"]},{id:"m1",component:"Metric",label:"总营收",value:{path:"revenue"},trend:"up",delta:"+12%"},{id:"m2",component:"Metric",label:"新客数",value:{path:"newCustomers"}},{id:"trend",component:"Chart",kind:"bar",series:"monthly",x:"month",y:"amount",title:"月度趋势"}],data:{title:"2026 Q3 销售看板",revenue:"¥1,240 万",newCustomers:"318",monthly:[{month:"7月",amount:380},{month:"8月",amount:420},{month:"9月",amount:440}]}};export function renderUi(n,r={}){const o=r?.spec,i=validateSpec(o);if(!i.ok)return"界面描述不合法,已降级为文本呈现:\n"+i.errors.slice(0,5).map(t=>" · "+t).join("\n")+"\n\n用ui_catalog核对组件名与必填字段后重试;数据本身没问题的话,也可以直接用Markdown表格给出。";const s=toMarkdown(o);let a=String(r.path||"").trim()||defaultUiPath(o);a.endsWith(".ui.json")||(a+=".ui.json");let c=null;try{const r=e.resolve(n||".",a);t.mkdirSync(e.dirname(r),{recursive:!0}),t.writeFileSync(r,JSON.stringify({...o,_kind:"huicai.ui.surface",_version:1},null,2),"utf8"),c=a}catch(t){c=null}return s+(c?`\n\n(界面已保存到 ${c};桌面端会渲染为可视化面板,其它终端看上面的文本版。改动请改spec重新生成。)`:"\n\n(界面文件保存失败,仅以文本呈现。)")}
|
package/src/uitree.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{execFile as e}from"node:child_process";export const DEFAULT_MAX_NODES=60;export const HARD_MAX_NODES=200;export const MIN_AREA=100;export const MAX_OUTPUT_CHARS=12e3;export const INTERACTIVE_ROLES=new Set(["AXButton","AXMenuButton","AXPopUpButton","AXCheckBox","AXRadioButton","AXTextField","AXTextArea","AXSecureTextField","AXComboBox","AXSlider","AXIncrementor","AXMenuItem","AXMenuBarItem","AXTab","AXLink","AXDisclosureTriangle","AXCell","AXRow","AXColorWell","AXStepper","AXSegmentedControl","Button","MenuItem","CheckBox","RadioButton","Edit","Document","ComboBox","List","ListItem","Tab","TabItem","Hyperlink","Slider","Spinner","TreeItem","SplitButton","Text"]);const n={AXButton:"按钮",Button:"按钮",SplitButton:"按钮",AXMenuButton:"菜单按钮",AXPopUpButton:"下拉",ComboBox:"下拉",AXComboBox:"下拉",AXCheckBox:"复选",CheckBox:"复选",AXRadioButton:"单选",RadioButton:"单选",AXTextField:"输入框",Edit:"输入框",AXTextArea:"文本域",Document:"文本域",AXSecureTextField:"密码框",AXMenuItem:"菜单项",MenuItem:"菜单项",AXMenuBarItem:"菜单",AXTab:"标签页",Tab:"标签页",TabItem:"标签页",AXLink:"链接",Hyperlink:"链接",AXSlider:"滑块",Slider:"滑块",AXCell:"单元格",AXRow:"行",ListItem:"列表项",List:"列表",TreeItem:"树节点",AXDisclosureTriangle:"折叠",AXStepper:"步进",Spinner:"步进",AXSegmentedControl:"分段",AXColorWell:"取色",Text:"文本"},t=/-25211|不允许辅助访问|not allowed assistive|assistive access/i;export function uiTreeSupported(e=process.platform){return"darwin"===e||"win32"===e}export function unsupportedNote(e=process.platform){return"linux"===e?"Linux下暂不支持读界面结构(AT-SPI需要额外运行时,各桌面环境差异很大,装不装得上、读不读得到都不确定,与其半可用不如明说)。请改用run_native_script(xdotool)或computer_screenshot + computer_click的视觉路径。":`当前平台(${e})不支持读界面结构,请改用computer_screenshot + computer_click的视觉路径。`}function o(n,t,o){return new Promise(r=>{const i=e(n,t,{timeout:25e3,maxBuffer:8388608},(e,n,t)=>r({err:e,stdout:String(n||""),stderr:String(t||"")}));if(void 0!==o)try{i.stdin.write(o),i.stdin.end()}catch{}})}export async function readUiTree(e={},n={}){const r=e.platform||process.platform;if(!uiTreeSupported(r))return{ok:!1,error:unsupportedNote(r)};const i=Math.min(200,Math.max(5,Math.floor(Number(e.maxNodes)||60))),s=n.execImpl||o;let a;if("darwin"===r){const n=e.menu?function(e,n){return`\nfunction run() {\n const se = Application('System Events');\n let proc, appName;\n try {\n proc = ${e?`se.applicationProcesses.byName(${JSON.stringify(e)})`:"se.applicationProcesses.whose({ frontmost: true })[0]"};\n appName = proc.name();\n } catch (e) { return JSON.stringify({ error: 'noproc' }); }\n if (!appName) return JSON.stringify({ error: 'noproc' });\n const out = [];\n let bars;\n try { bars = proc.menuBars(); } catch (e) { return JSON.stringify({ app: appName, error: 'nomenu' }); }\n if (!bars.length) return JSON.stringify({ app: appName, nodes: [] });\n const tops = bars[0].menuBarItems();\n for (let t = 0; t < tops.length && out.length < ${n}; t++) {\n let topName = '';\n try { topName = tops[t].name() || ''; } catch (e) { continue; }\n let items;\n try { items = tops[t].menus()[0].menuItems(); } catch (e) { continue; }\n let names = [], keys = [], mods = [], ens = [];\n try { names = items.name(); } catch (e) { continue; }\n try { keys = items.menuItemCmdChar ? items.menuItemCmdChar() : []; } catch (e) { keys = []; }\n try { ens = items.enabled(); } catch (e) { ens = []; }\n for (let i = 0; i < names.length && out.length < ${n}; i++) {\n if (!names[i] || !keys[i]) continue; // 没有快捷键的菜单项在这里没价值\n if (ens[i] === false) continue;\n out.push({ menu: topName, name: names[i], key: keys[i] });\n }\n }\n return JSON.stringify({ app: appName, nodes: out });\n}`}(e.app||"",i):function(e,n){return`\nfunction run() {\n const se = Application('System Events');\n se.includeStandardAdditions = false;\n // byName返回的是惰性引用,应用不存在时不会返回null,而是在**取属性**时才抛。\n // 所以判存活必须靠"能不能取到name",光判 !proc永远不会成立。\n let proc, appName;\n try {\n proc = ${e?`se.applicationProcesses.byName(${JSON.stringify(e)})`:"se.applicationProcesses.whose({ frontmost: true })[0]"};\n appName = proc.name();\n } catch (e) { return JSON.stringify({ error: 'noproc' }); }\n if (!appName) return JSON.stringify({ error: 'noproc' });\n let wins;\n try { wins = proc.windows(); } catch (e) { return JSON.stringify({ app: appName, error: 'nowindow' }); }\n if (!wins.length) return JSON.stringify({ app: appName, error: 'nowindow' });\n const win = wins[0];\n let all;\n try {\n // entireContents一次拿到整棵子树;手工递归会退化成成千上万次Apple Events往返\n all = win.entireContents();\n } catch (e) {\n return JSON.stringify({ app: appName, error: 'contents:' + String(e) });\n }\n const cap = Math.min(all.length, ${8*n}); // 先粗筛的池子,过滤后再截到maxNodes\n const pool = all.slice(0, cap);\n // 批量取属性:每个 6 次往返,而不是每节点 6 次\n function bulk(fn) { try { return fn(); } catch (e) { return null; } }\n const roles = bulk(() => pool.role()) || [];\n const names = bulk(() => pool.name()) || [];\n const descs = bulk(() => pool.description()) || [];\n const poss = bulk(() => pool.position()) || [];\n const sizes = bulk(() => pool.size()) || [];\n const ens = bulk(() => pool.enabled()) || [];\n const out = [];\n for (let i = 0; i < pool.length && out.length < ${n}; i++) {\n const role = roles[i] || '';\n const pos = poss[i], size = sizes[i];\n if (!pos || !size || pos.length < 2 || size.length < 2) continue;\n out.push({\n role: role,\n name: names[i] || descs[i] || '',\n x: pos[0], y: pos[1], w: size[0], h: size[1],\n enabled: ens[i] === false ? false : true,\n });\n }\n let title = '';\n try { title = win.name() || ''; } catch (e) { title = ''; }\n return JSON.stringify({ app: appName, window: title, nodes: out, total: all.length });\n}`}(e.app||"",i,e.maxDepth);a=await s("osascript",["-l","JavaScript","-e",n])}else a=await s("powershell",["-NoProfile","-NonInteractive","-Command","-"],function(e,n){return`\n$ErrorActionPreference = 'Stop'\nAdd-Type -AssemblyName UIAutomationClient\nAdd-Type -AssemblyName UIAutomationTypes\nAdd-Type @"\nusing System;\nusing System.Runtime.InteropServices;\npublic class Win32Fg { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); }\n"@\n${e?`$procs = Get-Process | Where-Object { $_.MainWindowHandle -ne 0 -and $_.ProcessName -like '*${e.replace(/'/g,"''")}*' }`:"$fg = [Win32Fg]::GetForegroundWindow(); $procs = Get-Process | Where-Object { $_.MainWindowHandle -eq $fg }"}\nif (-not $procs) { Write-Output '{"error":"noproc"}'; exit }\n$p = $procs | Select-Object -First 1\n$root = [System.Windows.Automation.AutomationElement]::FromHandle($p.MainWindowHandle)\nif (-not $root) { Write-Output '{"error":"nowindow"}'; exit }\n# TreeScope::Descendants一次拿全,逐层walk会慢好几倍\n$cond = [System.Windows.Automation.Condition]::TrueCondition\n$all = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, $cond)\n$nodes = New-Object System.Collections.ArrayList\nforeach ($e in $all) {\n if ($nodes.Count -ge ${8*n}) { break }\n try {\n $r = $e.Current.BoundingRectangle\n if ($r.Width -le 0 -or $r.Height -le 0) { continue }\n $null = $nodes.Add([pscustomobject]@{\n role = $e.Current.ControlType.ProgrammaticName -replace '^ControlType\\.', ''\n name = $e.Current.Name\n x = [int]$r.X; y = [int]$r.Y; w = [int]$r.Width; h = [int]$r.Height\n enabled = $e.Current.IsEnabled\n key = $e.Current.AccessKey\n })\n } catch { continue }\n}\n$res = [pscustomobject]@{ app = $p.ProcessName; window = $p.MainWindowTitle; nodes = $nodes; total = $all.Count }\n$res | ConvertTo-Json -Compress -Depth 4\n`}(e.app||"",i));const c=(a.stdout||"")+"\n"+(a.stderr||"");if("darwin"===r&&t.test(c))return{ok:!1,error:PERMISSION_GUIDE_MAC,needsPermission:!0};if(a.err&&!a.stdout.trim()){return{ok:!1,error:"读取界面结构失败:"+(a.err.killed?"读取超时(25秒)——目标窗口的控件太多或应用无响应":a.stderr.trim()||a.err.message)}}let p;try{p=JSON.parse(function(e){const n=String(e||"").trim(),t=n.indexOf("{"),o=n.lastIndexOf("}");return t>=0&&o>t?n.slice(t,o+1):n}(a.stdout))}catch{return{ok:!1,error:"界面结构返回内容无法解析(可能是系统脚本环境异常):"+a.stdout.slice(0,200)}}if("noproc"===p?.error)return{ok:!1,error:e.app?`没找到应用「${e.app}」,请先确认它已启动。`:"取不到前台应用。"};if("nowindow"===p?.error)return{ok:!1,error:`「${p.app||e.app||"该应用"}」当前没有可读的窗口(可能已最小化)。`};if("nomenu"===p?.error)return{ok:!1,error:`「${p.app||e.app||"该应用"}」读不到菜单栏(该应用可能没有标准菜单)。`};if(p?.error)return{ok:!1,error:`读取界面结构失败:${p.error}`};const u=Array.isArray(p.nodes)?p.nodes:[];if(e.menu)return{ok:!0,text:renderMenu(p,u),nodes:u};const l=filterNodes(u,i);return{ok:!0,text:render(p,l,u.length),nodes:l}}export const PERMISSION_GUIDE_MAC="读界面结构需要**辅助功能**权限,当前未授权(系统返回 -25211)。请这样开启:\n1. 打开「系统设置 → 隐私与安全性 → 辅助功能」;\n2. 在列表里找到运行本程序的那个App(终端里跑就是终端/iTerm,桌面端就是本应用),打开它的开关;\n 列表里没有的话点「+」把它加进去;\n3. **重启该程序**(授权对已在运行的进程不生效,这一步最容易被漏掉)。\n在此之前请改用computer_screenshot + computer_click的视觉路径,或run_native_script。";export function filterNodes(e,n=60){const t=[],o=new Set;for(const r of e||[]){if(t.length>=n)break;const e=String(r?.role||"");if(!INTERACTIVE_ROLES.has(e))continue;const i=Number(r.w)||0,s=Number(r.h)||0,a=Number(r.x)||0,c=Number(r.y)||0;if(i<=0||s<=0||i*s<100)continue;if(a+i<=0||c+s<=0)continue;if(a>2e4||c>2e4)continue;if(!1===r.enabled)continue;const p=String(r.name||"").trim();if(!p)continue;const u=e+"|"+p+"|"+Math.round(a)+","+Math.round(c);o.has(u)||(o.add(u),t.push({role:e,name:p,x:a,y:c,w:i,h:s,key:String(r.key||"").trim()}))}return t}export function render(e,t,o=0){if(!t.length)return`【${e?.app||"前台应用"}】未找到可交互控件。可能是该应用没有实现可访问性接口(常见于Electron旧版、游戏、自绘界面),请改用computer_screenshot的视觉路径。`;const i=`【${e?.app||"应用"}${e?.window?" · "+e.window:""}】可交互控件 ${t.length} 个(原始节点 ${e?.total||o} 个,已按可交互/可见/可用/有名筛选):`,s=t.map((e,t)=>{const o=n[e.role]||e.role,i=Math.round(e.x+e.w/2),s=Math.round(e.y+e.h/2);return`${t+1}. ${o}「${r(e.name,40)}」中心(${i},${s}) ${Math.round(e.w)}×${Math.round(e.h)}`+(e.key?` 快捷键${e.key}`:"")});return r(i+"\n"+s.join("\n")+"\n\n用computer_click点「中心」坐标即可。**有快捷键的优先用computer_key**——不用定位、不受窗口移动影响,也不会点错到旁边的控件上。",12e3)}export function renderMenu(e,n){if(!n.length)return`【${e?.app||"应用"}】没读到带快捷键的菜单项。`;const t=new Map;for(const e of n){const n=e.menu||"菜单";t.has(n)||t.set(n,[]),t.get(n).push(`${e.name}(⌘${e.key})`)}const o=[...t.entries()].map(([e,n])=>`${e}:${n.join("、")}`);return r(`【${e?.app||"应用"}】菜单快捷键:\n`+o.join("\n")+"\n\n用computer_key直接发快捷键,比点菜单快得多也稳得多。",12e3)}function r(e,n){const t=String(e??"");return t.length<=n?t:t.slice(0,n)+`…(已截断,共 ${t.length} 字)`}
|