@huicai/cli 2026.714.0 → 2026.804.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/README.md +13 -5
- package/bin/huicai.js +83 -0
- package/package.json +2 -2
- package/skills/ab-testing/SKILL.md +58 -0
- package/skills/ab-testing/scripts/__pycache__/ab_test.cpython-312.pyc +0 -0
- package/skills/ab-testing/scripts/ab_test.py +71 -0
- package/skills/academic-writing/SKILL.md +54 -0
- package/skills/accounting-ops/SKILL.md +68 -0
- package/skills/accounting-ops/scripts/__pycache__/acct_tools.cpython-312.pyc +0 -0
- package/skills/accounting-ops/scripts/acct_tools.py +89 -0
- package/skills/ai-agent/SKILL.md +85 -0
- package/skills/ai-agent/references/tool-design-checklist.md +51 -0
- package/skills/ai-aiops/SKILL.md +73 -0
- package/skills/ai-aiops/references/aiops-rollout-checklist.md +36 -0
- package/skills/ai-coding/SKILL.md +89 -0
- package/skills/ai-coding/references/task-brief-template.md +48 -0
- package/skills/ai-compliance/SKILL.md +74 -0
- package/skills/ai-compliance/references/genai-compliance-checklist.md +40 -0
- package/skills/ai-eval/SKILL.md +80 -0
- package/skills/ai-eval/scripts/eval-runner.py +150 -0
- package/skills/ai-llm-app/SKILL.md +90 -0
- package/skills/ai-llm-app/references/llm-integration-checklist.md +39 -0
- package/skills/ai-mlops/SKILL.md +72 -0
- package/skills/ai-mlops/scripts/token-cost-calc.py +108 -0
- package/skills/ai-multimodal/SKILL.md +86 -0
- package/skills/ai-multimodal/scripts/field-validate.py +182 -0
- package/skills/ai-prompt/SKILL.md +75 -0
- package/skills/ai-prompt/references/prompt-template.md +46 -0
- package/skills/ai-rag/SKILL.md +76 -0
- package/skills/ai-rag/scripts/chunk-inspect.py +129 -0
- package/skills/anomaly-detection/SKILL.md +54 -0
- package/skills/anomaly-detection/scripts/__pycache__/anomaly.cpython-312.pyc +0 -0
- package/skills/anomaly-detection/scripts/anomaly.py +99 -0
- package/skills/api-integration/SKILL.md +74 -0
- package/skills/api-integration/references/signing-recipes.md +141 -0
- package/skills/arch-design/SKILL.md +57 -0
- package/skills/arch-distributed/SKILL.md +81 -0
- package/skills/arch-distributed/references/idempotency-review.md +43 -0
- package/skills/arch-multitenant/SKILL.md +73 -0
- package/skills/arch-multitenant/references/tenant-isolation-checklist.md +34 -0
- package/skills/audit-assist/SKILL.md +63 -0
- package/skills/audit-assist/scripts/__pycache__/audit_sampling.cpython-312.pyc +0 -0
- package/skills/audit-assist/scripts/audit_sampling.py +93 -0
- package/skills/audit-chain/SKILL.md +60 -0
- package/skills/banking-credit/SKILL.md +59 -0
- package/skills/banking-credit/scripts/__pycache__/credit_metrics.cpython-312.pyc +0 -0
- package/skills/banking-credit/scripts/credit_metrics.py +53 -0
- package/skills/brand-design/SKILL.md +56 -0
- package/skills/business-plan/SKILL.md +56 -0
- package/skills/capital-ops/SKILL.md +52 -0
- package/skills/cicd-pipeline/SKILL.md +85 -0
- package/skills/cicd-pipeline/references/pipeline-review-checklist.md +39 -0
- package/skills/content-creation/SKILL.md +56 -0
- package/skills/corporate-governance/SKILL.md +56 -0
- package/skills/crawl-chain/SKILL.md +61 -0
- package/skills/customer-service/SKILL.md +51 -0
- package/skills/data-analysis/SKILL.md +46 -0
- package/skills/data-analysis/scripts/cjk_font.py +56 -0
- package/skills/data-chain/SKILL.md +54 -0
- package/skills/data-governance/SKILL.md +54 -0
- package/skills/data-report-pro/SKILL.md +65 -0
- package/skills/data-report-pro/scripts/__pycache__/bigdata.cpython-312.pyc +0 -0
- package/skills/data-report-pro/scripts/bigdata.py +79 -0
- package/skills/data-streaming/SKILL.md +87 -0
- package/skills/data-streaming/references/job-checklist.md +36 -0
- package/skills/data-wrangling/SKILL.md +52 -0
- package/skills/data-wrangling/scripts/__pycache__/join_tools.cpython-312.pyc +0 -0
- package/skills/data-wrangling/scripts/join_tools.py +99 -0
- package/skills/db-es/SKILL.md +68 -0
- package/skills/db-es/scripts/es-health.sh +72 -0
- package/skills/db-migration/SKILL.md +72 -0
- package/skills/db-migration/scripts/checksum-mysql.sql +46 -0
- package/skills/db-migration/scripts/checksum-postgres.sql +39 -0
- package/skills/db-migration/scripts/compare-reports.py +98 -0
- package/skills/db-mysql/SKILL.md +78 -0
- package/skills/db-mysql/scripts/mysql-health.sql +80 -0
- package/skills/db-postgres/SKILL.md +86 -0
- package/skills/db-postgres/scripts/pg-health.sql +67 -0
- package/skills/db-redis/SKILL.md +83 -0
- package/skills/db-redis/scripts/redis-health.sh +92 -0
- package/skills/dev-cpp/SKILL.md +94 -0
- package/skills/dev-cpp/references/cmake-sanitizer-snippets.md +56 -0
- package/skills/dev-cross-platform/SKILL.md +73 -0
- package/skills/dev-cross-platform/references/capability-matrix-template.md +39 -0
- package/skills/dev-csharp/SKILL.md +96 -0
- package/skills/dev-csharp/references/review-checklist.md +37 -0
- package/skills/dev-frontend/SKILL.md +91 -0
- package/skills/dev-frontend/scripts/bundle-budget.py +106 -0
- package/skills/dev-go/SKILL.md +88 -0
- package/skills/dev-go/scripts/go-triage.sh +54 -0
- package/skills/dev-harmony/SKILL.md +64 -0
- package/skills/dev-harmony/references/harmony-checklist.md +40 -0
- package/skills/dev-java/SKILL.md +77 -0
- package/skills/dev-java/scripts/jvm-triage.sh +84 -0
- package/skills/dev-mobile/SKILL.md +82 -0
- package/skills/dev-mobile/references/store-compliance-checklist.md +33 -0
- package/skills/dev-node/SKILL.md +82 -0
- package/skills/dev-node/scripts/node-triage.sh +78 -0
- package/skills/dev-php/SKILL.md +86 -0
- package/skills/dev-php/scripts/fpm-calc.py +48 -0
- package/skills/dev-python/SKILL.md +81 -0
- package/skills/dev-python/scripts/venv-doctor.sh +75 -0
- package/skills/dev-rust/SKILL.md +84 -0
- package/skills/dev-rust/references/cargo-ci-snippets.md +50 -0
- package/skills/dev-toolchain/SKILL.md +78 -0
- package/skills/dev-toolchain/scripts/secret-scan.sh +83 -0
- package/skills/doc-chain/SKILL.md +58 -0
- package/skills/doc-compare/SKILL.md +44 -0
- package/skills/doc-compare/scripts/compare.py +93 -0
- package/skills/docx-author/SKILL.md +39 -0
- package/skills/docx-author/scripts/docx_doc.py +161 -0
- package/skills/ecommerce-ops/SKILL.md +54 -0
- package/skills/ecommerce-ops/scripts/__pycache__/gmv_decompose.cpython-312.pyc +0 -0
- package/skills/ecommerce-ops/scripts/gmv_decompose.py +61 -0
- package/skills/education-training/SKILL.md +53 -0
- package/skills/ehs-safety/SKILL.md +63 -0
- package/skills/email-writing/SKILL.md +52 -0
- package/skills/eng-ai/SKILL.md +74 -0
- package/skills/eng-ai/references/ai-feature-checklist.md +36 -0
- package/skills/eng-architecture/SKILL.md +85 -0
- package/skills/eng-architecture/scripts/capacity-calc.py +100 -0
- package/skills/eng-automation/SKILL.md +84 -0
- package/skills/eng-automation/references/automation-inventory.md +38 -0
- package/skills/eng-automation/references/routine-presets.md +73 -0
- package/skills/eng-collaboration/SKILL.md +76 -0
- package/skills/eng-collaboration/references/collab-templates.md +43 -0
- package/skills/eng-complex-env/SKILL.md +84 -0
- package/skills/eng-complex-env/references/env-dossier-template.md +52 -0
- package/skills/eng-complex-env/scripts/env-recon.sh +152 -0
- package/skills/eng-data-platform/SKILL.md +81 -0
- package/skills/eng-data-platform/references/table-spec-template.md +39 -0
- package/skills/eng-database/SKILL.md +79 -0
- package/skills/eng-database/references/schema-review-checklist.md +37 -0
- package/skills/eng-develop/SKILL.md +76 -0
- package/skills/eng-develop/references/dod-checklist.md +43 -0
- package/skills/eng-docs/SKILL.md +71 -0
- package/skills/eng-docs/references/runbook-template.md +57 -0
- package/skills/eng-efficiency/SKILL.md +75 -0
- package/skills/eng-efficiency/scripts/dora-calc.py +104 -0
- package/skills/eng-infra/SKILL.md +81 -0
- package/skills/eng-infra/references/production-readiness.md +46 -0
- package/skills/eng-integration/SKILL.md +82 -0
- package/skills/eng-integration/references/integration-dossier-template.md +38 -0
- package/skills/eng-legacy/SKILL.md +81 -0
- package/skills/eng-legacy/references/modernization-workbook.md +46 -0
- package/skills/eng-observability/SKILL.md +78 -0
- package/skills/eng-observability/scripts/slo-calc.py +82 -0
- package/skills/eng-ops/SKILL.md +74 -0
- package/skills/eng-ops/references/postmortem-template.md +67 -0
- package/skills/eng-performance/SKILL.md +73 -0
- package/skills/eng-performance/scripts/perf-baseline.py +110 -0
- package/skills/eng-planning/SKILL.md +79 -0
- package/skills/eng-planning/scripts/schedule-calc.py +101 -0
- package/skills/eng-quality/SKILL.md +74 -0
- package/skills/eng-quality/references/test-plan-templates.md +46 -0
- package/skills/eng-release/SKILL.md +78 -0
- package/skills/eng-release/references/release-plan-template.md +36 -0
- package/skills/eng-reliability/SKILL.md +72 -0
- package/skills/eng-reliability/references/resilience-review.md +43 -0
- package/skills/eng-requirements/SKILL.md +86 -0
- package/skills/eng-requirements/references/story-templates.md +47 -0
- package/skills/eng-research/SKILL.md +73 -0
- package/skills/eng-research/references/research-templates.md +42 -0
- package/skills/eng-security/SKILL.md +73 -0
- package/skills/eng-security/references/security-review-checklist.md +33 -0
- package/skills/eng-ux/SKILL.md +85 -0
- package/skills/eng-ux/references/ux-review-checklist.md +36 -0
- package/skills/engineering-chain/SKILL.md +101 -0
- package/skills/engineering-chain/references/eng-health-review.md +37 -0
- package/skills/env-offline/SKILL.md +81 -0
- package/skills/env-offline/scripts/bundle-manifest.sh +94 -0
- package/skills/env-private-deploy/SKILL.md +72 -0
- package/skills/env-private-deploy/references/delivery-templates.md +35 -0
- package/skills/env-xinchuang/SKILL.md +73 -0
- package/skills/env-xinchuang/references/adaptation-matrix-template.md +41 -0
- package/skills/equipment-tpm/SKILL.md +56 -0
- package/skills/equipment-tpm/scripts/oee.py +58 -0
- package/skills/esg-report/SKILL.md +58 -0
- package/skills/esg-report/scripts/carbon_calc.py +61 -0
- package/skills/event-management/SKILL.md +51 -0
- package/skills/excel-analysis-chain/SKILL.md +73 -0
- package/skills/file-ingest/SKILL.md +51 -0
- package/skills/file-ingest/scripts/__pycache__/file_toolkit.cpython-312.pyc +0 -0
- package/skills/file-ingest/scripts/file_toolkit.py +404 -0
- package/skills/finance-analysis/SKILL.md +60 -0
- package/skills/finance-analysis/references/ratios.md +54 -0
- package/skills/finance-analysis/scripts/fin_ratios.py +132 -0
- package/skills/finance-chain/SKILL.md +59 -0
- package/skills/financial-modeling/SKILL.md +58 -0
- package/skills/financial-modeling/scripts/fin_model.py +123 -0
- package/skills/foreign-trade/SKILL.md +54 -0
- package/skills/funnel-cohort/SKILL.md +53 -0
- package/skills/funnel-cohort/scripts/__pycache__/funnel.cpython-312.pyc +0 -0
- package/skills/funnel-cohort/scripts/funnel.py +105 -0
- package/skills/gov-document/SKILL.md +57 -0
- package/skills/hr-chain/SKILL.md +51 -0
- package/skills/hr-operations/SKILL.md +53 -0
- package/skills/info-disclosure/SKILL.md +56 -0
- package/skills/infra-gateway/SKILL.md +85 -0
- package/skills/infra-gateway/references/golive-checklist.md +36 -0
- package/skills/infra-k8s/SKILL.md +78 -0
- package/skills/infra-k8s/scripts/k8s-triage.sh +84 -0
- package/skills/infra-mq/SKILL.md +87 -0
- package/skills/infra-mq/references/mq-review-checklist.md +36 -0
- package/skills/insurance-advisory/SKILL.md +57 -0
- package/skills/internal-audit/SKILL.md +66 -0
- package/skills/ip-management/SKILL.md +54 -0
- package/skills/ipo-listing/SKILL.md +60 -0
- package/skills/it-audit/SKILL.md +56 -0
- package/skills/itsm-ops/SKILL.md +55 -0
- package/skills/knowledge-sop/SKILL.md +51 -0
- package/skills/legal-chain/SKILL.md +55 -0
- package/skills/legal-contract/SKILL.md +58 -0
- package/skills/legal-contract/references/contract-checklists.md +59 -0
- package/skills/litigation-support/SKILL.md +53 -0
- package/skills/logistics-warehouse/SKILL.md +53 -0
- package/skills/logistics-warehouse/scripts/freight_check.py +71 -0
- package/skills/long-doc-analysis/SKILL.md +52 -0
- package/skills/management-accounting/SKILL.md +57 -0
- package/skills/management-accounting/scripts/__pycache__/costing.cpython-312.pyc +0 -0
- package/skills/management-accounting/scripts/costing.py +90 -0
- package/skills/manufacturing-chain/SKILL.md +57 -0
- package/skills/manufacturing-quality/SKILL.md +57 -0
- package/skills/manufacturing-quality/scripts/__pycache__/spc.cpython-312.pyc +0 -0
- package/skills/manufacturing-quality/scripts/spc.py +86 -0
- package/skills/marketing-analytics/SKILL.md +59 -0
- package/skills/marketing-analytics/scripts/__pycache__/rfm.cpython-312.pyc +0 -0
- package/skills/marketing-analytics/scripts/rfm.py +60 -0
- package/skills/marketing-chain/SKILL.md +57 -0
- package/skills/meeting-minutes/SKILL.md +55 -0
- package/skills/metrics-system/SKILL.md +55 -0
- package/skills/negotiation-prep/SKILL.md +56 -0
- package/skills/obs-prometheus/SKILL.md +74 -0
- package/skills/obs-prometheus/references/promql-cheatsheet.md +78 -0
- package/skills/office-automation/SKILL.md +54 -0
- package/skills/okr-review/SKILL.md +44 -0
- package/skills/ops-fleet/SKILL.md +69 -0
- package/skills/ops-fleet/scripts/batch-exec-guard.sh +119 -0
- package/skills/ops-linux/SKILL.md +69 -0
- package/skills/ops-linux/scripts/quick-triage.sh +138 -0
- package/skills/optimization-solver/SKILL.md +53 -0
- package/skills/optimization-solver/scripts/__pycache__/solver.cpython-312.pyc +0 -0
- package/skills/optimization-solver/scripts/solver.py +107 -0
- package/skills/pdf-toolkit/SKILL.md +43 -0
- package/skills/pdf-toolkit/scripts/pdf_ops.py +137 -0
- package/skills/pivot-crosstab/SKILL.md +44 -0
- package/skills/pivot-crosstab/scripts/__pycache__/pivot_tools.cpython-312.pyc +0 -0
- package/skills/pivot-crosstab/scripts/pivot_tools.py +90 -0
- package/skills/policy-analysis/SKILL.md +47 -0
- package/skills/portfolio-analysis/SKILL.md +56 -0
- package/skills/portfolio-analysis/scripts/__pycache__/portfolio.cpython-312.pyc +0 -0
- package/skills/portfolio-analysis/scripts/portfolio.py +78 -0
- package/skills/ppt-chain/SKILL.md +61 -0
- package/skills/pptx-author/SKILL.md +48 -0
- package/skills/pptx-author/scripts/pptx_deck.py +193 -0
- package/skills/pr-comms/SKILL.md +58 -0
- package/skills/pr-comms/references/pr-templates.md +54 -0
- package/skills/procurement-supply/SKILL.md +47 -0
- package/skills/procurement-supply/scripts/__pycache__/supply.cpython-312.pyc +0 -0
- package/skills/procurement-supply/scripts/supply.py +71 -0
- package/skills/product-design/SKILL.md +66 -0
- package/skills/product-design/references/prd-framework.md +60 -0
- package/skills/production-planning/SKILL.md +56 -0
- package/skills/production-planning/scripts/__pycache__/mrp_capacity.cpython-312.pyc +0 -0
- package/skills/production-planning/scripts/mrp_capacity.py +70 -0
- package/skills/project-management/SKILL.md +53 -0
- package/skills/project-management/scripts/__pycache__/cpm.cpython-312.pyc +0 -0
- package/skills/project-management/scripts/cpm.py +84 -0
- package/skills/project-proposal/SKILL.md +54 -0
- package/skills/project-proposal/scripts/budget_sheet.py +90 -0
- package/skills/qa-api-automation/SKILL.md +67 -0
- package/skills/qa-api-automation/references/three-layer-skeleton.md +61 -0
- package/skills/qa-mobile-automation/SKILL.md +70 -0
- package/skills/qa-mobile-automation/references/device-matrix-template.md +34 -0
- package/skills/qa-ui-automation/SKILL.md +80 -0
- package/skills/qa-ui-automation/references/stability-conventions.md +41 -0
- package/skills/quant-analysis/SKILL.md +60 -0
- package/skills/quant-analysis/scripts/__pycache__/quant.cpython-312.pyc +0 -0
- package/skills/quant-analysis/scripts/quant.py +102 -0
- package/skills/real-estate-property/SKILL.md +51 -0
- package/skills/real-estate-property/scripts/__pycache__/property_calc.cpython-312.pyc +0 -0
- package/skills/real-estate-property/scripts/property_calc.py +106 -0
- package/skills/regression-forecast/SKILL.md +52 -0
- package/skills/regression-forecast/scripts/__pycache__/regress.cpython-312.pyc +0 -0
- package/skills/regression-forecast/scripts/regress.py +106 -0
- package/skills/retail-store-ops/SKILL.md +57 -0
- package/skills/retail-store-ops/scripts/store_ops.py +57 -0
- package/skills/risk-compliance/SKILL.md +51 -0
- package/skills/sec-auth/SKILL.md +89 -0
- package/skills/sec-auth/references/auth-review-checklist.md +38 -0
- package/skills/sec-compliance/SKILL.md +74 -0
- package/skills/sec-compliance/references/dengbao-checklist.md +43 -0
- package/skills/securities-chain/SKILL.md +69 -0
- package/skills/securities-research/SKILL.md +58 -0
- package/skills/securities-research/scripts/__pycache__/valuation.cpython-312.pyc +0 -0
- package/skills/securities-research/scripts/valuation.py +93 -0
- package/skills/slides-html/SKILL.md +50 -0
- package/skills/slides-html/scripts/template.html +117 -0
- package/skills/statistical-testing/SKILL.md +51 -0
- package/skills/statistical-testing/scripts/__pycache__/stat_tests.cpython-312.pyc +0 -0
- package/skills/statistical-testing/scripts/stat_tests.py +131 -0
- package/skills/strategy-planning/SKILL.md +53 -0
- package/skills/supply-chain/SKILL.md +53 -0
- package/skills/tax-compliance/SKILL.md +57 -0
- package/skills/tax-compliance/scripts/tax_calc.py +90 -0
- package/skills/tech-research/SKILL.md +55 -0
- package/skills/tech-research/references/research-framework.md +50 -0
- package/skills/tender-bidding/SKILL.md +65 -0
- package/skills/tender-bidding/references/bid-checklist.md +44 -0
- package/skills/translation-pro/SKILL.md +44 -0
- package/skills/treasury-mgmt/SKILL.md +56 -0
- package/skills/treasury-mgmt/scripts/__pycache__/cashflow_forecast.cpython-312.pyc +0 -0
- package/skills/treasury-mgmt/scripts/cashflow_forecast.py +51 -0
- package/skills/web-scraping/SKILL.md +81 -0
- package/skills/web-search/SKILL.md +67 -0
- package/skills/xlsx-analysis/SKILL.md +96 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_toolkit.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/xlsx_style.py +71 -0
- package/skills/xlsx-analysis/scripts/xlsx_toolkit.py +335 -0
- package/src/advisor.js +1 -1
- package/src/agent.js +1 -1
- package/src/api.js +1 -1
- package/src/assets-import.js +1 -0
- package/src/autofix.js +1 -1
- package/src/automation.js +1 -1
- package/src/codeindex.js +1 -1
- package/src/commands.js +1 -1
- package/src/compaction.js +1 -1
- package/src/config.js +1 -1
- package/src/context-report.js +1 -1
- package/src/context.js +1 -1
- package/src/decisions.js +1 -0
- package/src/doccache.js +1 -0
- package/src/doctor.js +1 -1
- package/src/executors.js +1 -1
- package/src/expert-catalog.js +1 -0
- package/src/experts-store.js +1 -0
- package/src/gitflow.js +1 -1
- package/src/input.js +1 -1
- package/src/mcp.js +1 -1
- package/src/permissions.js +1 -1
- package/src/pyruntime.js +1 -0
- package/src/repl.js +1 -1
- package/src/scan.js +1 -1
- package/src/schedule-freq.js +1 -0
- package/src/schedule.js +1 -1
- package/src/session.js +1 -1
- package/src/skills.js +1 -1
- package/src/squad.js +1 -1
- package/src/subagent.js +1 -1
- package/src/title.js +1 -0
- package/src/tools.js +1 -1
- package/src/transcript.js +1 -1
- package/src/tui/app.js +1 -1
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Excel 分析工具箱:真实办公表格「脏在哪」这里就治哪——一次摸清结构、稳读脏表、按期对账。
|
|
3
|
+
|
|
4
|
+
经 run_python 使用(pandas/openpyxl/xlrd 已随托管运行时就绪)。典型三步:
|
|
5
|
+
|
|
6
|
+
# read_skill_file(skill="xlsx-analysis", path="scripts/xlsx_toolkit.py") 拿到本文件源码后,
|
|
7
|
+
# 连同你的调用代码一起交给 run_python 执行(下面 exec 到当前命名空间):
|
|
8
|
+
exec(open(__file__).read()) # 若已落盘;一般是把源码直接内联
|
|
9
|
+
|
|
10
|
+
sheet_map('attachments/收款单202602.xlsx') # ① 摸清每个 Sheet 的结构与关键列
|
|
11
|
+
df = smart_read('attachments/收款单202602.xlsx', '凌川峰应收账款') # ② 稳读(自动表头/清洗/剔合计)
|
|
12
|
+
detail = sum_by_period(df, value_col='贷方金额', date_col='记账日期') # ③ 按月汇总贷方
|
|
13
|
+
reconcile(detail, summary_map, left_name='明细', right_name='汇总') # 与汇总核对
|
|
14
|
+
|
|
15
|
+
为什么需要它:真实表格的表头几乎从不在第 1 行(标题行/说明行/合并单元格在前),金额常是带千分位的
|
|
16
|
+
文本,日期格式五花八门("2024-08-01 0" / "2025/2/28" / excel 序列号 / "2024.1"),还混着「合计」行。
|
|
17
|
+
天真的 pd.read_excel(header=0) 会读出一排 Unnamed 列与错误答案。这里把这些坑一次性收拾干净。
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import warnings
|
|
23
|
+
warnings.simplefilter('ignore')
|
|
24
|
+
import numpy as np
|
|
25
|
+
import pandas as pd
|
|
26
|
+
|
|
27
|
+
# ── 关键词表(判列/剔行用,可按需扩充)────────────────────────────────────────
|
|
28
|
+
AMOUNT_HINTS = ('金额', '贷方', '借方', '余额', '合计', '税额', '价税', '本期', '发生', '收入',
|
|
29
|
+
'成本', '费用', '数量', '单价', '工资', '总额', '应收', '应付', '预收', '预付')
|
|
30
|
+
DATE_HINTS = ('日期', '时间', '月份', '期间', '账期', '账龄', '年月', '日 期')
|
|
31
|
+
SUBTOTAL_MARKS = ('合计', '小计', '总计', '合 计', '小 计', '总 计', 'total', 'subtotal', '本页合计', '累计')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ── 金额 / 日期 / 期间 的稳健转换 ─────────────────────────────────────────────
|
|
35
|
+
# 全角数字/小数点/正负号 → 半角(中文系统导出常见)
|
|
36
|
+
_FULLWIDTH = str.maketrans('0123456789.+-', '0123456789.+-')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def to_amount(s: pd.Series) -> pd.Series:
|
|
40
|
+
"""任意「金额列」→ **float64**:清千分位/货币符/元/全角括号负数/短横线空值/全角数字,文本数字一并救回。
|
|
41
|
+
统一返回 float64(缺失为 np.nan,非可空 pd.NA)——下游 float()/scipy/groupby 一致不踩坑。"""
|
|
42
|
+
if pd.api.types.is_numeric_dtype(s):
|
|
43
|
+
return pd.to_numeric(s, errors='coerce').astype('float64')
|
|
44
|
+
t = s.astype('string').str.strip().str.translate(_FULLWIDTH)
|
|
45
|
+
# 括号(含全角)表示负数:(1,234.5) / (1234)
|
|
46
|
+
neg = t.str.match(r'^[((].*[))]$').fillna(False)
|
|
47
|
+
t = t.str.replace(r'[,,、\s¥¥$元人民币]', '', regex=True)
|
|
48
|
+
t = t.str.replace(r'^[((]|[))]$', '', regex=True)
|
|
49
|
+
t = t.replace({'-': np.nan, '—': np.nan, '-': np.nan, '': np.nan, 'nan': np.nan, 'None': np.nan})
|
|
50
|
+
v = pd.to_numeric(t, errors='coerce')
|
|
51
|
+
v[neg] = -v[neg].abs()
|
|
52
|
+
return v.astype('float64')
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def to_period(s: pd.Series, freq: str = 'M') -> pd.Series:
|
|
56
|
+
"""任意「日期列」→ 期间字符串(freq='M'→'YYYY-MM','Q'→'YYYYQn','Y'→'YYYY')。
|
|
57
|
+
覆盖:真日期、'2024-08-01 0'、'2025/2/28'、excel 序列号、'2024.1'、'2024年2月'、'202402'。"""
|
|
58
|
+
raw = s.copy()
|
|
59
|
+
num = pd.to_numeric(raw, errors='coerce')
|
|
60
|
+
# excel 序列号(20000~60000 约合 1954~2064 年)先行:否则 to_datetime 会把 45658 当 epoch 误判成 1970 年
|
|
61
|
+
is_serial = num.between(20000, 60000).fillna(False)
|
|
62
|
+
ser = pd.to_datetime(num.where(is_serial), unit='D', origin='1899-12-30', errors='coerce')
|
|
63
|
+
# 其余按常规解析。不预置 datetime64[ns]:pandas 3.0 会自选更宽分辨率容纳极端年份,强转 [ns] 反而在
|
|
64
|
+
# 8021/1 年这类脏值上 OutOfBoundsDatetime;这里让 pandas 自选,越界年份统一在下方 clamp 成 NaT。
|
|
65
|
+
dt = pd.to_datetime(raw.mask(is_serial), errors='coerce').fillna(ser)
|
|
66
|
+
# 纯文本年月兜底:2024.1 / 2024年2月 / 2024-2 / 202402。年份必须落在 1900~2200,否则多是金额/编码
|
|
67
|
+
# 被误当年月(如 "4090.09"/"409009"),构造日期会 OutOfBounds——直接不认。
|
|
68
|
+
def _ym(years, months, idx):
|
|
69
|
+
ok = years.between(1900, 2200) & months.between(1, 12)
|
|
70
|
+
if not ok.any():
|
|
71
|
+
return None
|
|
72
|
+
return pd.to_datetime(dict(year=years[ok], month=months[ok], day=1), errors='coerce').reindex(idx)
|
|
73
|
+
if dt.isna().any():
|
|
74
|
+
txt = raw.astype('string').str.strip()
|
|
75
|
+
m = txt.str.extract(r'(?P<y>\d{4})\s*[-/.年]\s*(?P<mo>\d{1,2})')
|
|
76
|
+
got = m['y'].notna() & m['mo'].notna()
|
|
77
|
+
if got.any():
|
|
78
|
+
ym = _ym(m['y'][got].astype(int), m['mo'][got].astype(int), raw.index)
|
|
79
|
+
if ym is not None:
|
|
80
|
+
dt = dt.fillna(ym)
|
|
81
|
+
m6 = txt.str.fullmatch(r'(\d{4})(\d{2})').fillna(False)
|
|
82
|
+
if m6.any():
|
|
83
|
+
ym6 = _ym(txt[m6].str.slice(0, 4).astype(int), txt[m6].str.slice(4, 6).astype(int), raw.index)
|
|
84
|
+
if ym6 is not None:
|
|
85
|
+
dt = dt.fillna(ym6)
|
|
86
|
+
# 统一成 datetime64(跨分辨率 fillna 可能退化成 object,.dt 会失效);实在统一不了就整列判无日期
|
|
87
|
+
if not pd.api.types.is_datetime64_any_dtype(dt):
|
|
88
|
+
try:
|
|
89
|
+
dt = pd.to_datetime(dt, errors='coerce')
|
|
90
|
+
except Exception:
|
|
91
|
+
return pd.Series(pd.NA, index=raw.index, dtype='string')
|
|
92
|
+
# 越界/垃圾年份(8021、year 1、"2025/1/1 汇总"解析出的怪值)一律 NaT:业务日期落在 1900~2200
|
|
93
|
+
dt = dt.where(dt.dt.year.between(1900, 2200))
|
|
94
|
+
try:
|
|
95
|
+
p = dt.dt.to_period({'M': 'M', 'Q': 'Q', 'Y': 'Y', 'A': 'Y'}.get(freq.upper(), 'M'))
|
|
96
|
+
except Exception:
|
|
97
|
+
return pd.Series(pd.NA, index=raw.index, dtype='string')
|
|
98
|
+
return p.astype('string')
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ── 稳健打开(按真实内容而非扩展名选引擎,失败给可读原因)──────────────────────
|
|
102
|
+
def _sniff(path: str) -> str:
|
|
103
|
+
try:
|
|
104
|
+
if os.path.getsize(path) == 0:
|
|
105
|
+
return 'empty'
|
|
106
|
+
except OSError:
|
|
107
|
+
return 'missing'
|
|
108
|
+
with open(path, 'rb') as fh:
|
|
109
|
+
head = fh.read(8)
|
|
110
|
+
if head[:4] == b'PK\x03\x04':
|
|
111
|
+
return 'zip' # xlsx/xlsm(zip 容器)
|
|
112
|
+
if head[:4] == b'\xd0\xcf\x11\xe0':
|
|
113
|
+
return 'ole2' # xls / 加密 / OLE2 复合文档
|
|
114
|
+
low = head.lower()
|
|
115
|
+
if low.startswith((b'<html', b'<?xml', b'<table', b'\xef\xbb\xbf<')):
|
|
116
|
+
return 'html' # 部分系统把网页表格存成 .xls/.xlsx
|
|
117
|
+
return 'unknown'
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def open_workbook(path: str):
|
|
121
|
+
"""稳健打开工作簿:按内容嗅探选引擎(治「.xls 存成 .xlsx」这类扩展名错配),
|
|
122
|
+
打不开时返回可读原因而非抛裸异常。返回 (pd.ExcelFile | None, reason)。"""
|
|
123
|
+
kind = _sniff(path)
|
|
124
|
+
if kind == 'missing':
|
|
125
|
+
return None, '文件不存在'
|
|
126
|
+
if kind == 'empty':
|
|
127
|
+
return None, '空文件(0 字节)——多半是导出失败,请重新导出'
|
|
128
|
+
if kind == 'html':
|
|
129
|
+
return None, 'HTML 伪装的表格(非真 Excel):用 pandas.read_html(path) 读取,或先另存为真正的 .xlsx'
|
|
130
|
+
engines = {'ole2': ['xlrd', None], 'zip': ['openpyxl', None]}.get(kind, [None, 'openpyxl', 'xlrd'])
|
|
131
|
+
last = ''
|
|
132
|
+
for eng in engines:
|
|
133
|
+
try:
|
|
134
|
+
return (pd.ExcelFile(path) if eng is None else pd.ExcelFile(path, engine=eng)), ''
|
|
135
|
+
except Exception as e:
|
|
136
|
+
last = f'{type(e).__name__}: {str(e)[:90]}'
|
|
137
|
+
if kind == 'ole2' and ('ole2' in last.lower() or 'encrypt' in last.lower() or 'workbook' in last.lower()):
|
|
138
|
+
return None, '疑似加密或损坏的旧格式(.xls):需要密码,或请对方另存为未加密 .xlsx'
|
|
139
|
+
if kind == 'zip' and 'Content_Types' in last:
|
|
140
|
+
return None, 'xlsx 压缩包损坏(缺 [Content_Types].xml)——请重新导出/保存该文件'
|
|
141
|
+
return None, last or '无法识别的表格格式'
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ── 表头识别与稳健读取 ───────────────────────────────────────────────────────
|
|
145
|
+
def _read_raw(path: str, sheet):
|
|
146
|
+
xf, why = open_workbook(path)
|
|
147
|
+
if xf is None:
|
|
148
|
+
raise ValueError(why)
|
|
149
|
+
return xf.parse(sheet, header=None, dtype=object)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def detect_header(raw: pd.DataFrame, scan: int = 15) -> int:
|
|
153
|
+
"""在前 scan 行里找真表头:优先命中关键词最多的行;否则取「非空文本格」最密的行。
|
|
154
|
+
数据体宽度(多数行都填了的列数)作锚,表头须覆盖其大部且以文字标签为主。"""
|
|
155
|
+
if raw.empty:
|
|
156
|
+
return 0
|
|
157
|
+
n = min(scan, len(raw))
|
|
158
|
+
fill = raw.notna().sum(axis=0)
|
|
159
|
+
body_cols = fill[fill >= max(1, len(raw) * 0.3)].index
|
|
160
|
+
if len(body_cols) == 0:
|
|
161
|
+
body_cols = raw.columns
|
|
162
|
+
best_i, best_score = 0, -1.0
|
|
163
|
+
for i in range(n):
|
|
164
|
+
row = raw.iloc[i]
|
|
165
|
+
vals = [row[c] for c in body_cols]
|
|
166
|
+
nonnull = [v for v in vals if pd.notna(v)]
|
|
167
|
+
if not nonnull:
|
|
168
|
+
continue
|
|
169
|
+
strs = [v for v in nonnull if isinstance(v, str) and v.strip() and not _looks_number(v)]
|
|
170
|
+
kw = sum(any(h in str(v) for h in (AMOUNT_HINTS + DATE_HINTS + ('编码', '名称', '组织', '单位', '摘要', '科目')))
|
|
171
|
+
for v in nonnull)
|
|
172
|
+
cover = len(nonnull) / max(1, len(body_cols))
|
|
173
|
+
# 覆盖度 + 文字占比 + 关键词命中;关键词权重最高(能一击命中真表头)
|
|
174
|
+
score = cover * 1.0 + (len(strs) / max(1, len(nonnull))) * 1.0 + kw * 2.0
|
|
175
|
+
if score > best_score:
|
|
176
|
+
best_i, best_score = i, score
|
|
177
|
+
return best_i
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _looks_number(v) -> bool:
|
|
181
|
+
try:
|
|
182
|
+
float(str(v).replace(',', '').replace(',', ''))
|
|
183
|
+
return True
|
|
184
|
+
except (ValueError, TypeError):
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _dedup_columns(cols):
|
|
189
|
+
out, seen = [], {}
|
|
190
|
+
for j, c in enumerate(cols):
|
|
191
|
+
name = '' if pd.isna(c) else str(c).strip()
|
|
192
|
+
if not name:
|
|
193
|
+
name = f'列{j + 1}'
|
|
194
|
+
if name in seen:
|
|
195
|
+
seen[name] += 1
|
|
196
|
+
name = f'{name}.{seen[name]}'
|
|
197
|
+
else:
|
|
198
|
+
seen[name] = 0
|
|
199
|
+
out.append(name)
|
|
200
|
+
return out
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def smart_read(path: str, sheet=0, header=None, drop_subtotal=True, coerce=True) -> pd.DataFrame:
|
|
204
|
+
"""稳读一个 Sheet:自动定位表头 → 去空白spacer行列 → 规整列名 → 剔「合计」行 →
|
|
205
|
+
(coerce)金额列救成 float。header 传数字可手动指定表头行。返回干净 DataFrame。"""
|
|
206
|
+
raw = _read_raw(path, sheet)
|
|
207
|
+
raw = raw.dropna(how='all').dropna(how='all', axis=1).reset_index(drop=True)
|
|
208
|
+
if raw.empty:
|
|
209
|
+
return pd.DataFrame()
|
|
210
|
+
hdr = detect_header(raw) if header is None else int(header)
|
|
211
|
+
cols = _dedup_columns(raw.iloc[hdr].tolist())
|
|
212
|
+
df = raw.iloc[hdr + 1:].copy()
|
|
213
|
+
df.columns = cols
|
|
214
|
+
df = df.dropna(how='all').dropna(how='all', axis=1).reset_index(drop=True)
|
|
215
|
+
if drop_subtotal and len(df):
|
|
216
|
+
mark = df.apply(lambda r: any(any(m in str(v).lower() for m in SUBTOTAL_MARKS) for v in r), axis=1)
|
|
217
|
+
df = df[~mark].reset_index(drop=True)
|
|
218
|
+
if coerce:
|
|
219
|
+
for c in df.columns:
|
|
220
|
+
name = str(c)
|
|
221
|
+
# 日期名优先("发生时间" 含 "发生" 但不是金额):命中日期词的列不当金额处理
|
|
222
|
+
if any(h in name for h in AMOUNT_HINTS) and not any(h in name for h in DATE_HINTS):
|
|
223
|
+
conv = to_amount(df[c])
|
|
224
|
+
if conv.notna().mean() >= 0.5: # 过半能转成数才认作金额列,避免误伤编码/文本列
|
|
225
|
+
df[c] = conv
|
|
226
|
+
return df
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ── 结构总览:一次摸清整本工作簿 ─────────────────────────────────────────────
|
|
230
|
+
def sheet_map(path: str, scan: int = 15):
|
|
231
|
+
"""打印每个 Sheet 的:满形状、探到的表头行、列清单,并标注疑似金额/日期列。
|
|
232
|
+
这是所有分析的第 0 步——看清结构再动手,是「答案对不对」的分水岭。返回结构 dict。"""
|
|
233
|
+
xf, why = open_workbook(path)
|
|
234
|
+
if xf is None:
|
|
235
|
+
print(f'📕 {path}\n ❌ 无法打开:{why}')
|
|
236
|
+
return {'error': why}
|
|
237
|
+
print(f'📘 {path}\n 共 {len(xf.sheet_names)} 个 Sheet:{xf.sheet_names}')
|
|
238
|
+
info = {}
|
|
239
|
+
for s in xf.sheet_names:
|
|
240
|
+
try:
|
|
241
|
+
raw = xf.parse(s, header=None, dtype=object) # 复用已打开的工作簿,免每 Sheet 重开
|
|
242
|
+
full = raw.shape
|
|
243
|
+
raw2 = raw.dropna(how='all').dropna(how='all', axis=1)
|
|
244
|
+
hdr = detect_header(raw2.reset_index(drop=True), scan=scan) if not raw2.empty else 0
|
|
245
|
+
cols = _dedup_columns(raw2.iloc[hdr].tolist()) if not raw2.empty else []
|
|
246
|
+
dat = [c for c in cols if any(h in c for h in DATE_HINTS)]
|
|
247
|
+
amt = [c for c in cols if any(h in c for h in AMOUNT_HINTS) and c not in dat] # 日期名优先
|
|
248
|
+
print(f'\n ── {s} | 满形状 {full} | 表头行≈{hdr} | 数据列 {len(cols)}')
|
|
249
|
+
print(f' 列: {cols[:20]}{" …" if len(cols) > 20 else ""}')
|
|
250
|
+
if amt:
|
|
251
|
+
print(f' 💰 疑似金额列: {amt}')
|
|
252
|
+
if dat:
|
|
253
|
+
print(f' 📅 疑似日期列: {dat}')
|
|
254
|
+
info[s] = {'shape': full, 'header': hdr, 'columns': cols, 'amount_cols': amt, 'date_cols': dat}
|
|
255
|
+
except Exception as e:
|
|
256
|
+
print(f'\n ── {s} | ⚠ 读取失败: {type(e).__name__}: {e}')
|
|
257
|
+
info[s] = {'error': f'{type(e).__name__}: {e}'}
|
|
258
|
+
return info
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ── 按期汇总与对账 ───────────────────────────────────────────────────────────
|
|
262
|
+
def sum_by_period(df: pd.DataFrame, value_col: str, date_col: str, freq: str = 'M') -> pd.Series:
|
|
263
|
+
"""明细表 → {期间: 金额合计}。value_col 自动金额清洗、date_col 自动日期归一。索引为 'YYYY-MM' 等。"""
|
|
264
|
+
v = to_amount(df[value_col])
|
|
265
|
+
p = to_period(df[date_col], freq=freq)
|
|
266
|
+
out = pd.Series(v.values, index=p.values).dropna()
|
|
267
|
+
out = out.groupby(level=0).sum()
|
|
268
|
+
out.index.name = f'期间({freq})'
|
|
269
|
+
out.name = value_col
|
|
270
|
+
return out.sort_index()
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _as_period_map(x) -> pd.Series:
|
|
274
|
+
"""把 Series / dict / DataFrame(单值列) 统一成 期间→数值 的 Series。"""
|
|
275
|
+
if isinstance(x, pd.Series):
|
|
276
|
+
s = x
|
|
277
|
+
elif isinstance(x, dict):
|
|
278
|
+
s = pd.Series(x)
|
|
279
|
+
elif isinstance(x, pd.DataFrame):
|
|
280
|
+
s = x.iloc[:, -1]
|
|
281
|
+
s.index = x.iloc[:, 0]
|
|
282
|
+
else:
|
|
283
|
+
raise TypeError('reconcile 需要 Series / dict / DataFrame')
|
|
284
|
+
s.index = s.index.astype('string')
|
|
285
|
+
return pd.to_numeric(s, errors='coerce')
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def reconcile(left, right, left_name='左', right_name='右', tol: float = 0.01, print_table: bool = True) -> pd.DataFrame:
|
|
289
|
+
"""按期对账:对齐两组 期间→金额,逐期算差额并判「对平/差异」。返回对账表 DataFrame。
|
|
290
|
+
left/right 可为 Series、dict 或两列 DataFrame。tol 为容差(默认 1 分)。"""
|
|
291
|
+
L = _as_period_map(left).groupby(level=0).sum()
|
|
292
|
+
R = _as_period_map(right).groupby(level=0).sum()
|
|
293
|
+
idx = sorted(set(L.index) | set(R.index))
|
|
294
|
+
t = pd.DataFrame(index=idx)
|
|
295
|
+
t[left_name] = L.reindex(idx)
|
|
296
|
+
t[right_name] = R.reindex(idx)
|
|
297
|
+
t['差额'] = t[left_name].fillna(0) - t[right_name].fillna(0)
|
|
298
|
+
t['状态'] = np.where(t['差额'].abs() <= tol, '✅对平', '❌差异')
|
|
299
|
+
t.index.name = '期间'
|
|
300
|
+
if print_table:
|
|
301
|
+
show = t.copy()
|
|
302
|
+
for c in (left_name, right_name, '差额'):
|
|
303
|
+
show[c] = show[c].map(lambda x: '' if pd.isna(x) else f'{x:,.2f}')
|
|
304
|
+
try:
|
|
305
|
+
print(show.to_markdown())
|
|
306
|
+
except Exception:
|
|
307
|
+
print(show.to_string())
|
|
308
|
+
bad = t[t['状态'] == '❌差异']
|
|
309
|
+
print(f'\n结论:共 {len(t)} 期,对平 {len(t) - len(bad)} 期'
|
|
310
|
+
+ (',全部对平 ✅' if bad.empty else f',差异 {len(bad)} 期:{list(bad.index)} ❗'))
|
|
311
|
+
return t
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ── 自测(run_python 直接跑本文件即可验证工具箱可用)─────────────────────────
|
|
315
|
+
if __name__ == '__main__':
|
|
316
|
+
print('自测:to_amount / to_period / reconcile\n' + '-' * 50)
|
|
317
|
+
amt = to_amount(pd.Series(['1,234.56', '(2,000.00)', '¥3,000元', '-', '', '4000']))
|
|
318
|
+
assert list(amt.fillna(-999)) == [1234.56, -2000.0, 3000.0, -999, -999, 4000.0], list(amt)
|
|
319
|
+
assert str(amt.dtype) == 'float64', amt.dtype # 统一 float64(缺失 np.nan,非 pd.NA)
|
|
320
|
+
fw = to_amount(pd.Series(['1234', '1,234.50', '(1,000)'])) # 全角数字/千分位/括号负
|
|
321
|
+
assert list(fw) == [1234.0, 1234.5, -1000.0], list(fw)
|
|
322
|
+
serial = (pd.Timestamp('2024-12-15') - pd.Timestamp('1899-12-30')).days # excel 序列号(应回到 2024-12)
|
|
323
|
+
per = to_period(pd.Series(['2024-08-01 0', '2025/2/28', '2024.1', '202403', serial]))
|
|
324
|
+
assert list(per) == ['2024-08', '2025-02', '2024-01', '2024-03', '2024-12'], list(per)
|
|
325
|
+
# 脏日期稳健性(真实台账常见:越界年份/6位编码/混入文字)——绝不抛异常;含真日期的照常提取,纯垃圾归 <NA>
|
|
326
|
+
junk = to_period(pd.Series(['2024-05', '8021-01-01', '1-01-01', '2025/1/1 汇总', '409009', '4090.09', '正常']))
|
|
327
|
+
assert junk.iloc[0] == '2024-05' and junk.iloc[3] == '2025-01', list(junk) # 真日期(含带文字的)
|
|
328
|
+
assert junk.iloc[[1, 2, 4, 5, 6]].isna().all(), list(junk) # 越界年份/编码/纯文字→NA
|
|
329
|
+
detail = pd.DataFrame({'贷方金额': ['1,000', '2,000', '3,000'],
|
|
330
|
+
'记账日期': ['2024-01-05', '2024-01-20', '2024-02-10']})
|
|
331
|
+
s = sum_by_period(detail, '贷方金额', '记账日期')
|
|
332
|
+
assert s['2024-01'] == 3000 and s['2024-02'] == 3000, s.to_dict()
|
|
333
|
+
r = reconcile(s, {'2024-01': 3000, '2024-02': 2999}, '明细', '汇总', print_table=False)
|
|
334
|
+
assert r.loc['2024-01', '状态'] == '✅对平' and r.loc['2024-02', '状态'] == '❌差异', r
|
|
335
|
+
print('✅ 全部自测通过')
|
package/src/advisor.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{streamChat as e}from"./api.js";export function makeAdvisorTool({config:
|
|
1
|
+
import{streamChat as e}from"./api.js";export function makeAdvisorTool({config:t}){if(!t?.advisorModel)return null;return{readOnly:!0,spec:{type:"function",function:{name:"ask_advisor",description:"遇到难题、复杂决策或反复卡住时,向一个更强的「技术顾问」模型请教独立的第二意见。把问题背景、已知信息、已尝试过什么、待决策点写清楚(自足),它会给出判断/方案与关键风险。用于真正的难点或高风险决策,不要滥用。",parameters:{type:"object",properties:{question:{type:"string",description:"自足的难题描述(含背景、已尝试、待决策点)"}},required:["question"]}}},run:async r=>{const n=String(r?.question||"").trim();if(!n)return"缺少 question";try{const r=await e({baseUrl:t.baseUrl,apiKey:t.apiKey,model:t.advisorModel,client:t.client,aux:!0,messages:[{role:"system",content:"你是资深技术顾问,正被一个编码智能体咨询一个难题。基于它提供的信息,给出清晰、具体、可执行的判断或方案:指出关键风险、推荐做法、以及为何。简洁直接,不要寒暄。"},{role:"user",content:n}]});return`【顾问(${t.advisorModel})意见】\n`+(r.content||"(顾问无回复)").trim()}catch(e){return"咨询顾问失败:"+e.message}}}}
|
package/src/agent.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:fs";import o from"node:path";import{streamChat as e}from"./api.js";import{MarkupFilter as n,stripMarkup as r}from"./markup-filter.js";import{TOOL_SPECS as s,READONLY_TOOLS as a,runTool as l}from"./tools.js";import{toolLabel as i,indent as c,renderFileDiff as u}from"./render.js";import{compactIfNeeded as m,buildModelMessages as p}from"./compaction.js";import{makeTodoTool as g,formatTodos as f,TODO_TOOL_NAME as d}from"./todo.js";import{makeMemoryTool as h}from"./memory.js";import{loadHooks as y,runHooks as w}from"./hooks.js";import*as S from"./ide.js";import{c as _}from"./colors.js";function T(t){const o=String(t);return o.startsWith("工具执行失败")||o.startsWith("命令启动失败")||o.includes("不是合法 JSON")||o.includes("未找到要替换的内容")||o.includes("路径超出项目目录")||o.includes("是目录而非文件")}function k(t){return"\n\n# 当前目标(用户设定)\n"+String(t).trim()+"\n在本回合结束、停止作答前,检查该目标是否已达成:若未达成,应继续推进(补充调研/改动/验证),不要过早停止;达成后用一句话说明已达成。"}function x(t){return"string"==typeof t?t:Array.isArray(t)?t.filter(t=>"text"===t?.type).map(t=>t.text||"").join("\n"):""}async function b(t,o,e){const n=new Array(t.length);let r=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(o,t.length))},async()=>{for(;r<t.length;){const o=r++;n[o]=await e(t[o],o)}})),n}export function isPlatformModel(t){return/^(agent|workflow|datahuman|squad)-/.test(String(t||""))}function P(t,o=200){const e=String(t).split("\n")[0];return e.length>o?e.slice(0,o)+"…":e}export function capToolResult(t){const o=String(t);if(o.length<=2e4)return o;const e=o.length-16e3-3e3;return o.slice(0,16e3)+`\n…【工具输出过长,中间已省略 ${e} 字符】…\n`+o.slice(-3e3)}export async function runTurn({config:$,messages:j,permissions:M,cwd:E,ask:I,signal:U,extraTools:v=[],depth:A=0,io:O,events:R,todoState:C,compaction:N,pullQueued:D,maxIters:J,fileJournal:q}){const F=O&&O.write||(t=>process.stdout.write(t)),K=R||null,W=t=>{K?K.onInfo?.(t):F(_.gray(" · "+t)+"\n")};let L=0,Q=0,z=0;const B=Number(J||$.maxTurns)||120,G=isPlatformModel($.model);let H=$.model;if(0===A&&$.autoModelStrong&&!G){const t=[...j].reverse().find(t=>"user"===t.role),o=x(t?.content);($.planMode||o.length>400||/重构|架构|设计|复杂|排查|调试|debug|优化|性能|并发|死锁|重写|迁移|安全审查|根因/i.test(o))&&$.autoModelStrong!==$.model&&(H=$.autoModelStrong,$.debug&&W(`[automodel] 复杂回合升配 → ${H}`))}const V=N||{},X=[...v];if(0===A&&!G&&!X.some(t=>t.spec?.function?.name===d)){const t=C||{list:[]};X.push(g({state:t,emit:t=>{K?K.onTodo?K.onTodo(t.map(t=>({...t}))):K.onInfo?.("任务清单:\n"+f(t)):F(_.gray(c(f(t)," "))+"\n")}}))}0!==A||G||!1===$.memoryEnabled||X.some(t=>"memory_write"===t.spec?.function?.name)||X.push(h({cwd:E,emit:t=>{K?K.onMemory?K.onMemory(t):K.onInfo?.("已记入长期记忆:"+P(t,80)):F(_.gray(" ✎ 已记入长期记忆:"+P(t,80))+"\n")}}));const Y=new Map;for(const t of X)Y.set(t.spec.function.name,t);const Z=G?void 0:[...s,...X.map(t=>t.spec)];let tt=0,ot=!1,et=0,nt=null,rt=0,st="";const at=0!==A||G?null:y(E),lt=!(!at||!at.all.length);if(lt){const t=[];if(j.filter(t=>"user"===t.role).length<=1){const o=w("SessionStart",{cwd:E},at);o.injectContext&&t.push(o.injectContext);for(const e of o.outputs)t.push(e.stdout)}const o=[...j].reverse().find(t=>"user"===t.role),e=w("UserPromptSubmit",{cwd:E,prompt:x(o?.content)},at);if(e.block)return W("提交被 UserPromptSubmit 钩子阻断:"+e.reason),{error:"提交被钩子阻断:"+e.reason,usage:{prompt:L,completion:Q},lastPromptTokens:z};e.injectContext&&t.push(e.injectContext);for(const o of e.outputs)t.push(o.stdout);t.length&&(nt="【钩子提供的附加上下文】\n"+t.join("\n"))}for(let s=0;s<B;s++){const c=s===B-1;!c||G||ot||nt||(nt="已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。");const g=await m(j,$,{state:V,lastPromptTokens:z});g>0&&W(`已压缩上下文(${g} 条较早消息已摘要,页面与会话记录不受影响)`);let f=!1,d=!1;const h=new n,y=t=>{t&&(f||!d||K||F("\n"),f=!0,K?K.onDelta?.(t):F(t))},x=p(j,V);let I=x;if(!G&&x.length&&"system"===x[0]?.role){let t="";$.planMode&&(t+="\n\n# 计划模式(当前开启)\n你现在处于计划模式:只能用只读工具(read_file/glob/grep/list_files/fetch_url)调研,不得做任何改动——write_file/edit_file/run_bash 等写或执行操作都会被拒绝,不要反复尝试。请先充分理解需求与现状,然后产出一份清晰、分步的实施方案:涉及哪些文件、每步改动要点、如何验证、风险点,用 todo_write 列出步骤,随后停下等待用户确认。用户关闭计划模式后你再执行。"),$.sandbox&&(t+="\n\n# 沙箱模式(当前开启)\n禁止执行 shell 命令(run_bash)、联网(fetch_url)与环境/应用自动化工具(install_*/setup_environment/open_*),调用会被拒绝,不要反复尝试。只做本地文件读写与分析。"),$.goal&&String($.goal).trim()&&(t+=k($.goal)),t&&(I=[{...x[0],content:String(x[0].content||"")+t},...x.slice(1)])}const v=[];st?v.push({role:"assistant",content:st},{role:"user",content:"你上一条回复因达到单轮输出长度上限而被截断,用户尚未看到剩余部分。请从截断处直接继续输出剩余内容:无缝衔接上文(若截断处在句子、代码块或表格中间,从中断字符处接着写),不要重复任何已输出的内容,不要道歉或解释,不要重新开头或总结前文。"}):nt&&v.push({role:"user",content:nt});const A=Date.now();let O;try{O=await e({baseUrl:$.baseUrl,apiKey:$.apiKey,model:H,messages:v.length?[...I,...v]:I,tools:ot||c?void 0:Z,maxTokens:$.maxTokens,reasoningEffort:$.reasoningEffort,client:$.client,sessionId:$.sessionId},{onContent:t=>y(h.feed(t)),onReasoning:t=>{!1!==$.showReasoning&&(K?K.onReasoning?.(t):(d||F(_.gray("\n[思考] ")),F(_.gray(t))),d=!0)},onRetry:(t,o,e)=>W(`模型响应异常,${Math.round(o/1e3)}s 后自动重试(第 ${t} 次):${e}`)},U)}catch(t){return"AbortError"===t.name?(K?K.onInfo?.("已中断"):F(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:L,completion:Q},lastPromptTokens:z}):(K?K.onInfo?.("✗ 模型调用失败:"+t.message):F("\n"+_.red("✗ 模型调用失败:")+t.message+"\n"),{error:t.message,usage:{prompt:L,completion:Q},lastPromptTokens:z})}y(h.flush()),O.usage&&(L+=O.usage.prompt_tokens||0,Q+=O.usage.completion_tokens||0,z=O.usage.prompt_tokens||z),$.debug&&W(`[debug] 轮${s+1} · ${$.model} · in ${O.usage?.prompt_tokens||0} / out ${O.usage?.completion_tokens||0} tok · ${Date.now()-A}ms · ${(O.toolCalls||[]).length} 工具调用`);const{text:R,sawMarkup:C}=r(O.content),N=C||h.sawMarkup;nt=null;const J=G?[]:O.toolCalls;if(!J.length){if(N&&!R.trim()&&!G){if(tt<2){tt++,2===tt&&(ot=!0),nt=1===tt?"你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。":"请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,不要输出任何工具调用或协议标记。",W(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${tt} 次)`);continue}W("模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)")}if(!G&&"length"===O.finishReason&&R&&rt<3){rt++,st+=R,W(`输出达单轮长度上限,自动续写中(第 ${rt} 段)`);continue}f&&!K&&F("\n");const t=st?st+R:R;st="",t&&j.push({role:"assistant",content:t}),K&&K.onTurnEnd?.(t);const o=D?.();if(o){j.push({role:"user",content:o});continue}if(lt){const o=w("Stop",{cwd:E,result:t},at);for(const t of o.outputs)W("钩子·Stop:"+P(t.stdout,160))}return{usage:{prompt:L,completion:Q},lastPromptTokens:z}}f&&!K&&F("\n"),K&&R&&K.onAssistantText?.(R),j.push({role:"assistant",content:st?st+R:R,tool_calls:J}),st="";const X=J.map((t,o)=>{const e=t.function?.name||"";let n={},r=null;const s=t.function?.arguments||"";if(s)try{n=JSON.parse(s)}catch(t){r=`工具参数不是合法 JSON(${t.message}):${String(s).slice(0,200)}…请修正参数后重新调用。`}const l=Y.get(e),i=S.isActive()&&("write_file"===e||"edit_file"===e);return{tc:t,idx:o,name:e,args:n,argsError:r,extra:l,readOnly:l?!!l.readOnly:a.has(e),deferToIde:i}}),it=t=>{for(let o=t;o<X.length;o++)j.push({role:"tool",tool_call_id:X[o].tc.id||`call_${o}`,content:"(用户中断,该工具未执行)"});return K?K.onInfo?.("已中断"):F(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:L,completion:Q},lastPromptTokens:z}},ct=async t=>{if(t.argsError)return t.argsError;let o;try{o=t.extra?await t.extra.run(t.args):await l(t.name,t.args,E,$)}catch(t){o=`工具执行失败:${t.message}`}return capToolResult(String(o))},ut=t=>{K?K.onToolStart?.(t.name,t.args):F(_.cyan(" ⏺ ")+i(t.name,t.args)+"\n")},mt=(t,o)=>{T(o)&&et++,K?K.onToolResult?.(t.name,String(o)):F(_.gray(" "+P(o))+"\n"),j.push({role:"tool",tool_call_id:t.tc.id,content:String(o)})},pt=(t,o)=>{K?K.onSkip?.(t.name,t.args):F(_.gray(` ⊘ ${i(t.name,t.args)} — 已跳过\n`)),j.push({role:"tool",tool_call_id:t.tc.id,content:o||"未授权,未执行。"})},gt=t=>"edit_file"===t||"write_file"===t,ft=o=>{try{return t.readFileSync(o,"utf8")}catch{return""}},dt=(t,e)=>{const n=o.resolve(E,String(t.args.path)),r=u(e,ft(n),{path:String(t.args.path)});r&&(K?K.onFileDiff?.(r):F(r+"\n"))};let ht=0;for(;ht<X.length;){if(U?.aborted)return it(ht);const e=X[ht];if(e.argsError){ut(e),mt(e,e.argsError),ht++;continue}if(e.readOnly){let t=ht;for(;t<X.length&&X[t].readOnly&&!X[t].argsError;)t++;const o=X.slice(ht,t),e=await b(o,8,async t=>{const o=await M.request(t.name,t.args,E,!0);return o.allow?(ut(t),{item:t,result:await ct(t)}):{item:t,denied:o.reason||"未授权,未执行。"}});for(const t of e)null!=t.denied?pt(t.item,t.denied):mt(t.item,t.result);if(ht=t,U?.aborted)return it(ht);continue}const n=await M.request(e.name,e.args,E,e.readOnly||e.deferToIde);if(U?.aborted)return it(ht);if(!n.allow){pt(e,n.reason),ht++;continue}if(lt){const t=w("PreToolUse",{cwd:E,toolName:e.name,args:e.args},at);for(const o of t.outputs)W("钩子·PreToolUse:"+P(o.stdout,120));if(t.block){pt(e,"PreToolUse 钩子阻断:"+t.reason),ht++;continue}t.modifiedArgs&&(e.args={...e.args,...t.modifiedArgs},W("钩子·PreToolUse 已修改工具参数"))}ut(e);const r=gt(e.name)&&e.args?.path?ft(o.resolve(E,String(e.args.path))):null;if(q&&null!=r&&e.args?.path){const n=t.existsSync(o.resolve(E,String(e.args.path)));q.push(r.length>2097152?{path:String(e.args.path),tooLarge:!0}:{path:String(e.args.path),before:r,existed:n})}let s=await ct(e);if(lt){const t=w("PostToolUse",{cwd:E,toolName:e.name,args:e.args,result:String(s)},at);if(t.outputs.length){for(const o of t.outputs)W("钩子·PostToolUse:"+P(o.stdout,120));s=String(s)+"\n\n[PostToolUse 钩子输出]\n"+t.outputs.map(t=>t.stdout).join("\n")}}mt(e,s),null!=r&&dt(e,r),ht++}et>=3&&!ot&&!nt&&(nt="你刚才连续多次工具调用失败。请停下来重新审视:是不是文件路径/内容/前提假设错了?先用 read_file / grep / list_files 核实现状,再换一种更可靠的方法,绝不要重复同样会失败的调用。",et=0,W("检测到连续工具失败,已提示智能体换一种更可靠的方法"));const yt=D?.();yt&&j.push({role:"user",content:yt})}return st&&(j.push({role:"assistant",content:st}),K&&K.onTurnEnd?.(st)),K?K.onInfo?.("已达最大工具轮数,停止"):F(_.yellow("\n[已达最大工具轮数,停止]\n")),{usage:{prompt:L,completion:Q},lastPromptTokens:z}}
|
|
1
|
+
import t from"node:fs";import e from"node:os";import n from"node:path";import{streamChat as o,getPlatformConfig as r}from"./api.js";import{MarkupFilter as s,stripMarkup as i}from"./markup-filter.js";import{TOOL_SPECS as a,READONLY_TOOLS as l,runTool as c}from"./tools.js";import{toolLabel as u,indent as m,renderFileDiff as f}from"./render.js";import{compactIfNeeded as g,buildModelMessages as p}from"./compaction.js";import{makeTodoTool as d,formatTodos as h,TODO_TOOL_NAME as y}from"./todo.js";import{makeDecisionTool as w,DECISION_TOOL_NAME as b,recordDecision as S,extractDecisionFromTurn as x}from"./decisions.js";import{makeMemoryTool as _}from"./memory.js";import{loadHooks as T,runHooks as k}from"./hooks.js";import*as M from"./ide.js";import{c as $}from"./colors.js";export const MAX_TOOL_RESULT_CHARS=6e4;export function toolConcurrency(t){const n=Number(t?.toolConcurrency);if(Number.isFinite(n)&&n>0)return Math.min(24,Math.floor(n));let o=4;try{o=e.cpus()?.length||4}catch{}return Math.max(8,Math.min(24,o-2))}const E=/context[_\s-]?length|maximum context|context window|too many tokens|reduce the length|prompt is too long|上下文.*(超|过长|超出|溢出)|输入.*过长|token.*(超出|超过).*(上限|限制)/i;export function isContextOverflowError(t){return E.test(String(t||""))}const P=/(现在|接下来|下面|这就|马上|我来|让我|我要|我先|我这就|咱们|让我们|稍等|待我)/,j=/(做|进行|生成|编写|写(一|个|完|出)?|制作|计算|统计|核对|汇总|整理|分析|梳理|输出|列出|准备|运行|执行|开始)/,I=/\b(let me|now let me|i.?ll|i will|let.?s|next,? i)\b[^.!?\n]*\b(write|create|generat|calculat|comput|do|produc|compil|analyz|summar|prepar|run|make|list)\b/i;function v(t){const e=String(t||"").trim().split(/\n+/).map(t=>t.trim()).filter(Boolean),n=e[e.length-1]||"";return!(!n||n.length>120)&&(!!I.test(n)||P.test(n)&&j.test(n))}function C(t){const e=String(t);return e.startsWith("工具执行失败")||e.startsWith("命令启动失败")||e.includes("不是合法 JSON")||e.includes("未找到要替换的内容")||e.includes("路径超出项目目录")||e.includes("是目录而非文件")}function A(t){return"\n\n# 当前目标(用户设定)\n"+String(t).trim()+"\n在本回合结束、停止作答前,检查该目标是否已达成:若未达成,应继续推进(补充调研/改动/验证),不要过早停止;达成后用一句话说明已达成。"}const U="【用户中断】",O="\n\n# 上一轮被用户中断(重要)\n上一回合被用户主动中断:会话里标注「"+U+"」的工具调用并未真正执行、没有任何结果。请勿臆测这些未完成工具的输出,也不要假设被中断的工作已完成或推断其结论;一切以用户最新一条消息为准——如与被中断的工作无关,就按新指令重新开始,需要时先与用户确认再继续。";function R(t){if(!Array.isArray(t))return!1;let e=-1;for(let n=t.length-1;n>=0;n--)if("user"===t[n].role){e=n;break}if(e<=0)return!1;for(let n=e-1;n>=0;n--){const e=t[n];if("user"===e.role)break;if("tool"===e.role&&"string"==typeof e.content&&e.content.includes(U))return!0}return!1}function N(t){return"string"==typeof t?t:Array.isArray(t)?t.filter(t=>"text"===t?.type).map(t=>t.text||"").join("\n"):""}async function D(t,e,n){const o=new Array(t.length);let r=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(e,t.length))},async()=>{for(;r<t.length;){const e=r++;o[e]=await n(t[e],e)}})),o}export function isPlatformModel(t){return/^(agent|workflow|datahuman|squad)-/.test(String(t||""))}function J(t,e=200){const n=String(t).split("\n")[0];return n.length>e?n.slice(0,e)+"…":n}export function capToolResult(t){const e=String(t);if(e.length<=6e4)return e;const n=e.length-48e3-9e3;return e.slice(0,48e3)+`\n…【工具输出过长,中间已省略 ${n} 字符】…\n`+e.slice(-9e3)}export async function runTurn({config:e,messages:E,permissions:P,cwd:j,ask:I,signal:q,extraTools:F=[],depth:K=0,io:L,events:W,todoState:z,compaction:B,pullQueued:H,maxIters:Q,fileJournal:X}){const G=L&&L.write||(t=>process.stdout.write(t)),V=W||null,Y=t=>{V?V.onInfo?.(t):G($.gray(" · "+t)+"\n")};let Z=0,tt=0,et=0;const nt=Number(Q||e.maxTurns)||120,ot=isPlatformModel(e.model),rt={low:24e4,medium:36e4,high:6e5}[e.reasoningEffort];let st=!1,it=String(e.fallbackModel||"").trim(),at=0,lt=e.model;if(0===K&&e.autoModelStrong&&!ot){const t=[...E].reverse().find(t=>"user"===t.role),n=N(t?.content);(e.planMode||n.length>400||/重构|架构|设计|复杂|排查|调试|debug|优化|性能|并发|死锁|重写|迁移|安全审查|根因/i.test(n))&&e.autoModelStrong!==e.model&&(lt=e.autoModelStrong,e.debug&&Y(`[automodel] 复杂回合升配 → ${lt}`))}const ct=B||{},ut=[...F];if(0===K&&!ot&&!ut.some(t=>t.spec?.function?.name===y)){const t=z||{list:[]};ut.push(d({state:t,emit:t=>{V?V.onTodo?V.onTodo(t.map(t=>({...t}))):V.onInfo?.("任务清单:\n"+h(t)):G($.gray(m(h(t)," "))+"\n")}}))}if(0!==K||ot||!1===e.memoryEnabled||ut.some(t=>"memory_write"===t.spec?.function?.name)||ut.push(_({cwd:j,emit:t=>{V?V.onMemory?V.onMemory(t):V.onInfo?.("已记入长期记忆:"+J(t,80)):G($.gray(" ✎ 已记入长期记忆:"+J(t,80))+"\n")}})),0===K&&!ot&&!1!==e.decisionsEnabled&&!ut.some(t=>t.spec?.function?.name===b)){const t=E.filter(t=>"user"===t.role).length;ut.push(w({cwd:j,sessionId:e.sessionId||"",turn:t,emit:t=>{St=!0,V?V.onDecision?V.onDecision(t):V.onInfo?.("已记录决策:"+J(t.title,80)):G($.gray(" ◆ 已记录决策:"+J(t.title,80))+"\n")}}))}const mt=new Map;for(const t of ut)mt.set(t.spec.function.name,t);const ft=ot?void 0:[...a,...ut.map(t=>t.spec)];let gt=0,pt=!1,dt=0,ht=null,yt=0,wt=0,bt=!1,St=!1;const xt=new Set;let _t="";const Tt=0!==K||ot?null:T(j),kt=!(!Tt||!Tt.all.length);if(kt){const t=[];if(E.filter(t=>"user"===t.role).length<=1){const e=k("SessionStart",{cwd:j},Tt);e.injectContext&&t.push(e.injectContext);for(const n of e.outputs)t.push(n.stdout)}const e=[...E].reverse().find(t=>"user"===t.role),n=k("UserPromptSubmit",{cwd:j,prompt:N(e?.content)},Tt);if(n.block)return Y("提交被 UserPromptSubmit 钩子阻断:"+n.reason),{error:"提交被钩子阻断:"+n.reason,usage:{prompt:Z,completion:tt},lastPromptTokens:et};n.injectContext&&t.push(n.injectContext);for(const e of n.outputs)t.push(e.stdout);t.length&&(ht="【钩子提供的附加上下文】\n"+t.join("\n"))}for(let a=0;a<nt;a++){const m=a===nt-1;!m||ot||pt||ht||(ht="已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。");const d=await g(E,e,{state:ct,lastPromptTokens:et});d>0&&e.debug&&Y(`[debug] 已压缩上下文(${d} 条较早消息已摘要)`);let h=!1,y=!1;const w=new s,b=t=>{t&&(h||!y||V||G("\n"),h=!0,V?V.onDelta?.(t):G(t))},_=p(E,ct);let T=_;if(!ot&&_.length&&"system"===_[0]?.role){let t="";e.planMode&&(t+="\n\n# 计划模式(当前开启)\n你现在处于计划模式:只能用只读工具(read_file/glob/grep/list_files/fetch_url)调研,不得做任何改动——write_file/edit_file/run_bash 等写或执行操作都会被拒绝,不要反复尝试。请先充分理解需求与现状,然后产出一份清晰、分步的实施方案:涉及哪些文件、每步改动要点、如何验证、风险点,用 todo_write 列出步骤,随后停下等待用户确认。用户关闭计划模式后你再执行。"),e.sandbox&&(t+="\n\n# 沙箱模式(当前开启)\n禁止执行 shell 命令(run_bash)、联网(fetch_url)与环境/应用自动化工具(install_*/setup_environment/open_*),调用会被拒绝,不要反复尝试。只做本地文件读写与分析。"),e.goal&&String(e.goal).trim()&&(t+=A(e.goal)),R(E)&&(t+=O),t&&(T=[{..._[0],content:String(_[0].content||"")+t},..._.slice(1)])}const I=[];_t?I.push({role:"assistant",content:_t},{role:"user",content:"你上一条回复因达到单轮输出长度上限而被截断,用户尚未看到剩余部分。请从截断处直接继续输出剩余内容:无缝衔接上文(若截断处在句子、代码块或表格中间,从中断字符处接着写),不要重复任何已输出的内容,不要道歉或解释,不要重新开头或总结前文。"}):ht&&I.push({role:"user",content:ht});const N=Date.now();let F,L=!1,W="";try{F=await o({baseUrl:e.baseUrl,apiKey:e.apiKey,model:lt,messages:I.length?[...T,...I]:T,tools:pt||m?void 0:ft,maxTokens:e.maxTokens||8192,reasoningEffort:e.reasoningEffort,stallTimeoutMs:rt,client:e.client,sessionId:e.sessionId,aux:e.aux||void 0,continuation:!!_t,tailCount:I.length},{onContent:t=>{L=!0,W+=t,b(w.feed(t))},onReasoning:t=>{!1!==e.showReasoning&&(_t||(V?V.onReasoning?.(t):(y||G($.gray("\n[思考] ")),G($.gray(t))),y=!0))},onRetry:(t,n,o)=>{e.debug&&Y(`[retry] 模型响应异常,${Math.round(n/1e3)}s 后自动重试(第 ${t} 次):${o}`)}},q)}catch(t){if("AbortError"===t.name)return V?V.onInfo?.("已中断"):G($.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:Z,completion:tt},lastPromptTokens:et};if(!ot&&!q?.aborted&&isContextOverflowError(t.message)&&at<3){at++;const t=await g(E,e,{state:ct,force:!0});if(t>0){L&&W&&(_t+=W),e.debug&&Y(`[overflow] 上下文超出模型窗口,已强制压缩 ${t} 条较早消息后重试`);continue}e.debug&&Y("[overflow] 上下文超限但无可压缩内容,转兜底模型")}if(!ot&&!st&&!q?.aborted){if(!it)try{const t=await r(e.baseUrl,e.apiKey);it=String(t&&t.fallback_model||"").trim()}catch{}if(it&&it!==lt){st=!0,lt=it,L&&W&&(_t+=W),e.debug&&Y(`[fallback] 主模型失败(${t.message}),静默切兜底模型 ${it}${L?"(续写)":""}`);continue}}return V?V.onInfo?.("✗ 模型调用失败:"+t.message):G("\n"+$.red("✗ 模型调用失败:")+t.message+"\n"),{error:t.message,usage:{prompt:Z,completion:tt},lastPromptTokens:et}}b(w.flush()),F.usage&&(Z+=F.usage.prompt_tokens||0,tt+=F.usage.completion_tokens||0,et=F.usage.prompt_tokens||et,0===K&&V&&V.onUsage?.({lastPromptTokens:et,prompt:Z,completion:tt})),e.debug&&Y(`[debug] 轮${a+1} · ${e.model} · in ${F.usage?.prompt_tokens||0} / out ${F.usage?.completion_tokens||0} tok · ${Date.now()-N}ms · ${(F.toolCalls||[]).length} 工具调用`);const{text:z,sawMarkup:B}=i(F.content),Q=B||w.sawMarkup;ht=null;const ut=ot?[]:F.toolCalls;if(!ut.length){if(Q&&!z.trim()&&!ot){if(gt<2){gt++,2===gt&&(pt=!0),ht=1===gt?"你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。":"请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,不要输出任何工具调用或协议标记。",Y(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${gt} 次)`);continue}Y("模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)")}if(!ot&&"length"===F.finishReason&&z&&yt<3){yt++,_t+=z,e.debug&&Y(`[debug] 输出达单轮长度上限,自动续写(第 ${yt} 段)`);continue}h&&!V&&G("\n");const t=_t?_t+z:z;_t="",t&&E.push({role:"assistant",content:t});const n=!t.trim()||v(t);if(bt&&n&&"length"!==F.finishReason&&!pt&&wt<2){wt++,e.debug&&Y(`[debug] 检测到「宣告未落地」收尾(空正文或预告式),注入续跑纠偏(第 ${wt} 次)`),E.push({role:"user",content:'你上一条只是说明"接下来要做什么",但既没有发起工具调用、也没有给出结果——用户还没拿到任何最终产物。请立即执行你刚才所说的步骤(需要就用 run_python/工具,不需要就直接算),并直接输出面向用户的完整最终结果(例如完整的按月汇总表与逐月核对表、差异结论),不要再只描述你打算做什么。'});continue}V&&V.onTurnEnd?.(t);const o=H?.();if(o){E.push({role:"user",content:o});continue}if(kt){const e=k("Stop",{cwd:j,result:t},Tt);for(const t of e.outputs)Y("钩子·Stop:"+J(t.stdout,160))}if(!ot&&e.decisionAutoExtract&&!1!==e.decisionsEnabled&&bt&&!St&&t&&t.trim())try{const n=[...E].reverse().find(t=>"user"===t.role),o=n&&"string"==typeof n.content?n.content:"",r=await x(e,{userText:o,assistantText:t,toolNames:[...xt]}),s="number"==typeof e.decisionExtractMinConfidence?e.decisionExtractMinConfidence:.55;if(r&&(r.confidence??0)>=s){const t=E.filter(t=>"user"===t.role).length,n=S(j,{...r,title:r.what,status:"draft",origin:"agent",confidence:Math.min(r.confidence??.5,.7),sessionId:e.sessionId||"",turn:t},{origin:"agent"});V?V.onDecision?V.onDecision(n):V.onInfo?.("已抽取决策草稿(待确认):"+J(n.title,80)):G($.gray(" ◇ 已抽取决策草稿(待确认):"+J(n.title,80))+"\n")}}catch{}return{usage:{prompt:Z,completion:tt},lastPromptTokens:et}}bt=!0,h&&!V&&G("\n"),V&&z&&V.onAssistantText?.(z),E.push({role:"assistant",content:_t?_t+z:z,tool_calls:ut}),_t="";const Mt=ut.map((t,e)=>{const n=t.function?.name||"";let o={},r=null;const s=t.function?.arguments||"";if(s)try{o=JSON.parse(s)}catch(t){r=`工具参数不是合法 JSON(${t.message}):${String(s).slice(0,200)}…请修正参数后重新调用。`}const i=mt.get(n),a=M.isActive()&&("write_file"===n||"edit_file"===n);return{tc:t,idx:e,name:n,args:o,argsError:r,extra:i,readOnly:i?!!i.readOnly:l.has(n),deferToIde:a}}),$t=t=>{for(let e=t;e<Mt.length;e++)E.push({role:"tool",tool_call_id:Mt[e].tc.id||`call_${e}`,content:U+" 该工具未执行,无任何结果,请勿臆测其输出。"});return V?V.onInfo?.("已中断"):G($.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:Z,completion:tt},lastPromptTokens:et}},Et=async t=>{if(t.argsError)return t.argsError;let n;try{n=t.extra?await t.extra.run(t.args):await c(t.name,t.args,j,e,q)}catch(t){n=`工具执行失败:${t.message}`}return capToolResult(String(n))},Pt=t=>{xt.add(t.name),V?V.onToolStart?.(t.name,t.args):G($.cyan(" ⏺ ")+u(t.name,t.args)+"\n")},jt=(t,e)=>{C(e)&&dt++,V?V.onToolResult?.(t.name,String(e)):G($.gray(" "+J(e))+"\n"),E.push({role:"tool",tool_call_id:t.tc.id,content:String(e)})},It=(t,e)=>{V?V.onSkip?.(t.name,t.args):G($.gray(` ⊘ ${u(t.name,t.args)} — 已跳过\n`)),E.push({role:"tool",tool_call_id:t.tc.id,content:e||"未授权,未执行。"})},vt=t=>"edit_file"===t||"write_file"===t,Ct=e=>{try{return t.readFileSync(e,"utf8")}catch{return""}},At=(t,e)=>{const o=n.resolve(j,String(t.args.path)),r=f(e,Ct(o),{path:String(t.args.path)});r&&(V?V.onFileDiff?.(r):G(r+"\n"))};let Ut=0;for(;Ut<Mt.length;){if(q?.aborted)return $t(Ut);const o=Mt[Ut];if(o.argsError){Pt(o),jt(o,o.argsError),Ut++;continue}if(o.readOnly){let t=Ut;for(;t<Mt.length&&Mt[t].readOnly&&!Mt[t].argsError;)t++;const n=Mt.slice(Ut,t),o=await D(n,toolConcurrency(e),async t=>{const e=await P.request(t.name,t.args,j,!0);return e.allow?(Pt(t),{item:t,result:await Et(t)}):{item:t,denied:e.reason||"未授权,未执行。"}});for(const t of o)null!=t.denied?It(t.item,t.denied):jt(t.item,t.result);if(Ut=t,q?.aborted)return $t(Ut);continue}const r=await P.request(o.name,o.args,j,o.readOnly||o.deferToIde);if(q?.aborted)return $t(Ut);if(!r.allow){It(o,r.reason),Ut++;continue}if(kt){const t=k("PreToolUse",{cwd:j,toolName:o.name,args:o.args},Tt);for(const e of t.outputs)Y("钩子·PreToolUse:"+J(e.stdout,120));if(t.block){It(o,"PreToolUse 钩子阻断:"+t.reason),Ut++;continue}t.modifiedArgs&&(o.args={...o.args,...t.modifiedArgs},Y("钩子·PreToolUse 已修改工具参数"))}Pt(o);const s=vt(o.name)&&o.args?.path?Ct(n.resolve(j,String(o.args.path))):null;if(X&&null!=s&&o.args?.path){const e=t.existsSync(n.resolve(j,String(o.args.path)));X.push(s.length>2097152?{path:String(o.args.path),tooLarge:!0}:{path:String(o.args.path),before:s,existed:e})}let i=await Et(o);if(kt){const t=k("PostToolUse",{cwd:j,toolName:o.name,args:o.args,result:String(i)},Tt);if(t.outputs.length){for(const e of t.outputs)Y("钩子·PostToolUse:"+J(e.stdout,120));i=String(i)+"\n\n[PostToolUse 钩子输出]\n"+t.outputs.map(t=>t.stdout).join("\n")}}jt(o,i),null!=s&&At(o,s),Ut++}dt>=3&&!pt&&!ht&&(ht="你刚才连续多次工具调用失败。请停下来重新审视:是不是文件路径/内容/前提假设错了?先用 read_file / grep / list_files 核实现状,再换一种更可靠的方法,绝不要重复同样会失败的调用。",dt=0,Y("检测到连续工具失败,已提示智能体换一种更可靠的方法"));const Ot=H?.();Ot&&E.push({role:"user",content:Ot})}return _t&&(E.push({role:"assistant",content:_t}),V&&V.onTurnEnd?.(_t)),V?V.onInfo?.("已达最大工具轮数,停止"):G($.yellow("\n[已达最大工具轮数,停止]\n")),{usage:{prompt:Z,completion:tt},lastPromptTokens:et}}
|
package/src/api.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const t=new Set([429,500,502,503,504]),e=15e3;function n(t){return{"Content-Type":"application/json",Authorization:`Bearer ${t}`}}async function o(t){let e="";try{e=await t.text()}catch{}try{const t=JSON.parse(e),n=t?.error?.type||t?.type||"";if(t?.error?.message)return{message:t.error.message,type:n};if(t?.message)return{message:t.message,type:n};if(t?.msg)return{message:t.msg,type:n}}catch{}return{message:e||`HTTP ${t.status}`,type:""}}function r(t,e){for(const n of e){const e="number"==typeof n.index?n.index:t.length;t[e]||(t[e]={id:"",type:"function",function:{name:"",arguments:""}}),n.id&&(t[e].id=n.id),n.type&&(t[e].type=n.type),n.function?.name&&(t[e].function.name+=n.function.name),n.function?.arguments&&(t[e].function.arguments+=n.function.arguments)}}function a(t,e){return new Promise((n,o)=>{if(e?.aborted)return o(s());const r=setTimeout(()=>{e?.removeEventListener?.("abort",a),n()},t),a=()=>{clearTimeout(r),o(s())};e?.addEventListener?.("abort",a,{once:!0})})}function s(){const t=new Error("已中断");return t.name="AbortError",t}function i(t,e){const n=e?.headers?.get?.("retry-after"),o=null!=n?Number(n):NaN;return Number.isFinite(o)&&o>=0?Math.min(1e3*o,3e4):Math.min(500*2**t+Math.floor(250*Math.random()),8e3)}export async function streamChat(e,r={},l){const u=`${e.baseUrl}/v1/chat/completions`,f={model:e.model,messages:e.messages,stream:!0,stream_options:{include_usage:!0}};e.tools&&e.tools.length&&(f.tools=e.tools,f.tool_choice="auto"),"number"==typeof e.temperature&&(f.temperature=e.temperature),Number.isFinite(e.maxTokens)&&e.maxTokens>0&&(f.max_tokens=Math.floor(e.maxTokens)),e.reasoningEffort&&"off"!==e.reasoningEffort&&(f.reasoning_effort=e.reasoningEffort);const m=n(e.apiKey);e.client&&(m["X-Huicai-Client"]=e.client),e.sessionId&&(m["X-Huicai-Session"]=e.sessionId);const g=JSON.stringify(f);for(let n=0;;n++){let f;try{f=await fetch(u,{method:"POST",headers:m,body:g,signal:l})}catch(t){if("AbortError"===t?.name||l?.aborted)throw s();if(n<2){const e=i(n,null);r.onRetry?.(n+1,e,"网络错误:"+(t?.message||t)),await a(e,l);continue}throw new Error("网络错误:"+(t?.message||t))}if(!f.ok||!f.body){const e=await o(f);if(!("usage_limit_exceeded"===e.type)&&t.has(f.status)&&n<2){const t=i(n,f);r.onRetry?.(n+1,t,`HTTP ${f.status}:${e.message}`),await a(t,l);continue}throw new Error(e.message)}return c(f,r,l,e.stallTimeoutMs??9e4)}}async function c(t,e,n,o){const a=t.body.getReader(),i=new TextDecoder;let c="",l="";const u=[];let f=null,m=null,g=!1,d=null;const h=()=>{d&&clearTimeout(d),d=setTimeout(()=>{g=!0,a.cancel().catch(()=>{})},o)},w=()=>a.cancel().catch(()=>{});n?.addEventListener?.("abort",w,{once:!0});try{for(h();;){let t;try{t=await a.read()}catch(t){if(n?.aborted)throw s();if(g)throw new Error(`响应停滞超过 ${Math.round(o/1e3)}s,已中止(可重试)`);throw t}if(n?.aborted)throw s();if(g)throw new Error(`响应停滞超过 ${Math.round(o/1e3)}s,已中止(可重试)`);const{done:d,value:w}=t;if(d)break;h(),c+=i.decode(w,{stream:!0});const y=c.split("\n");c=y.pop()??"";for(const t of y){const n=t.trim();if(!n.startsWith("data:"))continue;const o=n.slice(5).trim();if("[DONE]"===o||""===o)continue;let a;try{a=JSON.parse(o)}catch{continue}a.usage&&(m=a.usage);const s=a.choices?.[0];if(!s)continue;const i=s.delta||{},c=i.reasoning_content??i.reasoning;c&&e.onReasoning?.(c),i.content&&(l+=i.content,e.onContent?.(i.content)),Array.isArray(i.tool_calls)&&r(u,i.tool_calls),s.finish_reason&&(f=s.finish_reason)}}}finally{d&&clearTimeout(d),n?.removeEventListener?.("abort",w)}return{content:l,toolCalls:u.filter(Boolean),finishReason:f,usage:m}}export async function embed(t,e,r,a){const s=await fetch(`${t}/v1/embeddings`,{method:"POST",headers:n(e),body:JSON.stringify({model:r,input:Array.isArray(a)?a:[a]}),signal:AbortSignal.timeout(3e4)});if(!s.ok)throw new Error((await o(s)).message);const i=await s.json();return Array.isArray(i?.data)?i.data.map(t=>t.embedding):null}export async function syncKnowledge(t,e,r,a){const s=await fetch(`${t}/v1/knowledge/${encodeURIComponent(r)}/sync`,{method:"POST",headers:n(e),body:JSON.stringify({files:a}),signal:AbortSignal.timeout(12e4)});if(!s.ok)throw new Error((await o(s)).message);return s.json()}export async function listModels(t,r){const a=await fetch(`${t}/v1/models`,{headers:n(r),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await o(a)).message);const s=await a.json();return Array.isArray(s?.data)?s.data:[]}export async function getPlatformConfig(t,o){try{const r=await fetch(`${t}/v1/config`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!r.ok)return{};const a=await r.json();return a&&"object"==typeof a?a:{}}catch{return{}}}export async function getAgentPersona(t,r,a){const s=await fetch(`${t}/v1/agents/${encodeURIComponent(a)}/persona`,{headers:n(r),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await o(s)).message);return s.json()}export async function fetchUsage(t,r){const a=await fetch(`${t}/v1/usage`,{headers:n(r),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await o(a)).message);return a.json()}export async function extractDocument(t,e,r){const a=await fetch(`${t}/v1/files/extract`,{method:"POST",headers:n(e),body:JSON.stringify(r),signal:AbortSignal.timeout(12e4)});if(!a.ok)throw new Error((await o(a)).message);return a.json()}export async function listPlatformSkills(t,r){const a=await fetch(`${t}/v1/skills`,{headers:n(r),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await o(a)).message);const s=await a.json();return Array.isArray(s?.data)?s.data:[]}export async function getPlatformSkill(t,r,a){const s=await fetch(`${t}/v1/skills/${encodeURIComponent(a)}`,{headers:n(r),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await o(s)).message);return s.json()}export async function pushPlatformSkill(t,r,a){const s=await fetch(`${t}/v1/skills/import`,{method:"POST",headers:{...n(r),"Content-Type":"application/json"},body:JSON.stringify(a),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await o(s)).message);return s.json()}
|
|
1
|
+
const t=new Set([429,500,502,503,504]),e=15e3;function n(t){return{"Content-Type":"application/json",Authorization:`Bearer ${t}`}}function o(t,e){try{const e=JSON.parse(t),n=e?.error?.type||e?.type||"";if(e?.error?.message)return{message:e.error.message,type:n};if(e?.message)return{message:e.message,type:n};if(e?.msg)return{message:e.msg,type:n}}catch{}return{message:t||e,type:""}}async function r(t){let e="";try{e=await t.text()}catch{}return o(e,`HTTP ${t.status}`)}function a(t,e){for(const n of e){const e="number"==typeof n.index?n.index:t.length;t[e]||(t[e]={id:"",type:"function",function:{name:"",arguments:""}}),n.id&&(t[e].id=n.id),n.type&&(t[e].type=n.type),n.function?.name&&(t[e].function.name+=n.function.name),n.function?.arguments&&(t[e].function.arguments+=n.function.arguments)}}function s(t,e){return new Promise((n,o)=>{if(e?.aborted)return o(i());const r=setTimeout(()=>{e?.removeEventListener?.("abort",a),n()},t),a=()=>{clearTimeout(r),o(i())};e?.addEventListener?.("abort",a,{once:!0})})}function i(){const t=new Error("已中断");return t.name="AbortError",t}function c(t,e){const n=new AbortController;let o=!1;const r=()=>n.abort();t&&(t.aborted?n.abort():t.addEventListener("abort",r,{once:!0}));const a=setTimeout(()=>{o=!0,n.abort()},e);return{signal:n.signal,ms:e,get timedOut(){return o},done(){clearTimeout(a),t?.removeEventListener?.("abort",r)}}}function u(t){const e=String(t?.message||t||""),n=t?.cause?.code||t?.code||"";return"ECONNREFUSED"===n||/ECONNREFUSED|connection refused/i.test(e)?"无法连接模型服务(连接被拒绝):请检查网络或稍后重试。":"ENOTFOUND"===n||"EAI_AGAIN"===n||/getaddrinfo|ENOTFOUND|EAI_AGAIN/i.test(e)?"无法解析模型服务地址(DNS 异常):请检查网络连接后重试。":"ETIMEDOUT"===n||/ETIMEDOUT|timeout|timed out/i.test(e)?"连接模型服务超时:网络较慢或服务繁忙,请稍后重试。":"ECONNRESET"===n||/ECONNRESET|socket hang up|network|fetch failed/i.test(e)?"与模型服务的连接中断:网络波动,请稍后重试。":"网络错误:"+e}function f(t,e){const n=e?.headers?.get?.("retry-after"),o=null!=n?Number(n):NaN;return Number.isFinite(o)&&o>=0?Math.min(1e3*o,3e4):Math.min(500*2**t+Math.floor(250*Math.random()),8e3)}export async function streamChat(e,o={},a){const m=`${e.baseUrl}/v1/chat/completions`,d={model:e.model,messages:e.messages,stream:!0,stream_options:{include_usage:!0}};e.tools&&e.tools.length&&(d.tools=e.tools,d.tool_choice="auto"),"number"==typeof e.temperature&&(d.temperature=e.temperature),Number.isFinite(e.maxTokens)&&e.maxTokens>0&&(d.max_tokens=Math.floor(e.maxTokens)),e.reasoningEffort&&"off"!==e.reasoningEffort&&(d.reasoning_effort=e.reasoningEffort),e.extraBody&&"object"==typeof e.extraBody&&Object.assign(d,e.extraBody);const g=n(e.apiKey);e.client&&(g["X-Huicai-Client"]=e.client),e.sessionId&&(g["X-Huicai-Session"]=e.sessionId),e.aux&&(g["X-Huicai-Aux"]="1"),e.continuation&&(g["X-Huicai-Continue"]="1"),e.tailCount>0&&(g["X-Huicai-Tail"]=String(e.tailCount));const h=JSON.stringify(d);for(let n=0;;n++){let d;const y=c(a,e.headersTimeoutMs??12e4);try{d=await fetch(m,{method:"POST",headers:g,body:h,signal:y.signal})}catch(t){if(a?.aborted)throw i();if(y.timedOut){const t=`等待模型服务响应超时(${Math.round(y.ms/1e3)}s 内未返回响应头)`;if(n<2){const e=f(n,null);o.onRetry?.(n+1,e,t),await s(e,a);continue}throw new Error(t+",请稍后重试。")}if("AbortError"===t?.name)throw i();if(n<2){const e=f(n,null);o.onRetry?.(n+1,e,u(t)),await s(e,a);continue}throw new Error(u(t))}finally{y.done()}if(!d.ok||!d.body){const e=await r(d);if(!("usage_limit_exceeded"===e.type)&&t.has(d.status)&&n<2){const t=f(n,d);o.onRetry?.(n+1,t,`HTTP ${d.status}:${e.message}`),await s(t,a);continue}throw new Error(e.message)}return l(d,o,a,e.stallTimeoutMs??18e4)}}async function l(t,e,n,o){const r=t.body.getReader(),s=new TextDecoder;let c="",u="";const f=[];let l=null,m=null,d=!1,g=null,h=!1;const y=()=>{g&&clearTimeout(g),g=setTimeout(()=>{d=!0,r.cancel().catch(()=>{})},o)},w=()=>{h=!0,y()},p=()=>new Error(h?`响应停滞超过 ${Math.round(o/1e3)}s,已中止(可重试)`:`模型 ${Math.round(o/1e3)}s 内没有任何输出,已中止(可重试)`),b=()=>r.cancel().catch(()=>{});n?.addEventListener?.("abort",b,{once:!0});try{for(y();;){let t;try{t=await r.read()}catch(t){if(n?.aborted)throw i();if(d)throw p();throw t}if(n?.aborted)throw i();if(d)throw p();const{done:o,value:g}=t;if(o)break;c+=s.decode(g,{stream:!0});const h=c.split("\n");c=h.pop()??"";for(const t of h){const n=t.trim();if(!n.startsWith("data:"))continue;const o=n.slice(5).trim();if("[DONE]"===o||""===o)continue;let r;try{r=JSON.parse(o)}catch{continue}if(r.error){const t=r.error&&(r.error.message||r.error.msg||r.error);throw new Error("模型返回错误:"+("string"==typeof t?t:JSON.stringify(t)))}r.usage&&(m=r.usage,w());const s=r.choices?.[0];if(!s)continue;const i=s.delta||{},c=i.reasoning_content??i.reasoning;c&&(w(),e.onReasoning?.(c)),i.content&&(w(),u+=i.content,e.onContent?.(i.content)),Array.isArray(i.tool_calls)&&(w(),a(f,i.tool_calls)),s.finish_reason&&(w(),l=s.finish_reason)}}}finally{g&&clearTimeout(g),n?.removeEventListener?.("abort",b)}return{content:u,toolCalls:f.filter(Boolean),finishReason:l,usage:m}}export async function embed(t,e,o,a){const s=await fetch(`${t}/v1/embeddings`,{method:"POST",headers:n(e),body:JSON.stringify({model:o,input:Array.isArray(a)?a:[a]}),signal:AbortSignal.timeout(3e4)});if(!s.ok)throw new Error((await r(s)).message);const i=await s.json();return Array.isArray(i?.data)?i.data.map(t=>t.embedding):null}export async function syncKnowledge(t,e,o,a){const s=await fetch(`${t}/v1/knowledge/${encodeURIComponent(o)}/sync`,{method:"POST",headers:n(e),body:JSON.stringify({files:a}),signal:AbortSignal.timeout(12e4)});if(!s.ok)throw new Error((await r(s)).message);return s.json()}export async function pushDecisions(t,e,o,a){const s=await fetch(`${t}/v1/decisions/sync`,{method:"POST",headers:n(e),body:JSON.stringify({projectKey:o,nodes:a}),signal:AbortSignal.timeout(6e4)});if(!s.ok)throw new Error((await r(s)).message);const i=await s.json();return Number(i?.accepted||0)}export async function pullDecisions(t,e,o,a){const s=new URLSearchParams({projectKey:o});a&&s.set("since",String(a));const i=await fetch(`${t}/v1/decisions?${s.toString()}`,{headers:n(e),signal:AbortSignal.timeout(6e4)});if(!i.ok)throw new Error((await r(i)).message);const c=await i.json();return Array.isArray(c?.data)?c.data:[]}export async function listModels(t,o){const a=await fetch(`${t}/v1/models`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await r(a)).message);const s=await a.json();return Array.isArray(s?.data)?s.data:[]}export async function getPlatformConfig(t,o){try{const r=await fetch(`${t}/v1/config`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!r.ok)return{};const a=await r.json();return a&&"object"==typeof a?a:{}}catch{return{}}}export async function getAgentPersona(t,o,a){const s=await fetch(`${t}/v1/agents/${encodeURIComponent(a)}/persona`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await r(s)).message);return s.json()}export async function fetchUsage(t,o){const a=await fetch(`${t}/v1/usage`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await r(a)).message);return a.json()}export async function extractDocument(t,e,o,a){const s=o.bytes||(o.data?Buffer.from(String(o.data),"base64"):null);if(s&&!1!==d.get(t))try{const{body:r,contentType:i}=function(t,e,n,o){const r="----huicai"+Date.now().toString(36)+Math.random().toString(36).slice(2,10),a=String(t||"file").replace(/[\r\n"]/g,"_"),s=[];s.push(Buffer.from(`--${r}\r\nContent-Disposition: form-data; name="file"; filename="${a}"\r\nContent-Type: application/octet-stream\r\n\r\n`)),s.push(e,Buffer.from("\r\n"));for(const[t,e]of[["maxChars",n],["offset",o]])null!=e&&s.push(Buffer.from(`--${r}\r\nContent-Disposition: form-data; name="${t}"\r\n\r\n${e}\r\n`));return s.push(Buffer.from(`--${r}--\r\n`)),{body:Buffer.concat(s),contentType:`multipart/form-data; boundary=${r}`}}(o.filename,s,o.maxChars,o.offset),c=m(await w(`${t}/v1/files/extract`,{...n(e),"content-type":i},r,a),!0);return d.set(t,!0),c}catch(e){if(!e||!e._noMultipart)throw e;d.set(t,!1)}const i=o.bytes?{...o,bytes:void 0,data:o.bytes.toString("base64")}:o,c=JSON.stringify(i);if("function"==typeof a){return m(await w(`${t}/v1/files/extract`,{...n(e)},Buffer.from(c),a),!1)}const u=await fetch(`${t}/v1/files/extract`,{method:"POST",headers:n(e),body:c,signal:AbortSignal.timeout(12e4)});if(!u.ok)throw new Error((await r(u)).message);return m(await u.text(),!1)}function m(t,e){let n=null;try{n=JSON.parse(t)}catch{}if(n&&"string"==typeof n.text)return n;const o=n&&(n.message||n.msg||n.error?.message)||"解析服务返回异常响应",r=new Error(o);throw e&&(r._noMultipart=!0),r}const d=new Map;const g=12e4,h=25,y=9e5;async function w(t,e,n,r){const{request:a}=await import("http:"===new URL(t).protocol?"node:http":"node:https"),s={};for(const[t,n]of Object.entries(e||{}))s[String(t).toLowerCase()]=n;return s["content-type"]||(s["content-type"]="application/json"),s["content-length"]=n.length,new Promise((e,i)=>{const c=a(t,{method:"POST",headers:s,agent:!1},t=>{const n=[];t.on("data",t=>n.push(t)),t.on("error",i),t.on("end",()=>{const r=Buffer.concat(n).toString("utf8");t.statusCode>=200&&t.statusCode<300?e(r):415===t.statusCode||405===t.statusCode?i(Object.assign(new Error(o(r,`HTTP ${t.statusCode}`).message),{_noMultipart:!0})):i(new Error(o(r,`HTTP ${t.statusCode}`).message))})}),u=Math.min(y,g+Math.floor(n.length/h));c.setTimeout(u,()=>c.destroy(new Error(`网络传输/服务端处理超时(已等待 ${Math.round(u/1e3)} 秒无响应),请检查网络后重试`))),c.on("error",i);let f=0;const l=()=>{for(;f<n.length;){const t=Math.min(f+262144,n.length),e=c.write(n.subarray(f,t));f=t;try{r&&r(f,n.length)}catch{}if(!e)return void c.once("drain",l)}c.end()};l()})}export function startParseTicker(t,e,n=200){const o=Math.max(600,400+t/15360),r=Date.now(),a=setInterval(()=>{const t=Date.now()-r,n=Math.min(99,Math.round(100*(1-Math.exp(-t/o))));try{e(n,t)}catch{}},n);return()=>clearInterval(a)}export async function listPlatformSkills(t,o){const a=await fetch(`${t}/v1/skills`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await r(a)).message);const s=await a.json();return Array.isArray(s?.data)?s.data:[]}export async function getPlatformSkill(t,o,a){const s=await fetch(`${t}/v1/skills/${encodeURIComponent(a)}`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await r(s)).message);return s.json()}export async function pushPlatformSkill(t,o,a){const s=await fetch(`${t}/v1/skills/import`,{method:"POST",headers:{...n(o),"Content-Type":"application/json"},body:JSON.stringify(a),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await r(s)).message);return s.json()}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"node:fs";import s from"node:path";import t from"node:os";import{parseFrontmatter as i}from"./skills.js";export const EXTERNAL_SOURCES=[{cli:"Claude Code",agents:".claude/agents",skills:".claude/skills"},{cli:"Codex",agents:".codex/agents",skills:".codex/skills"},{cli:"Gemini CLI",agents:".gemini/agents",skills:".gemini/skills"},{cli:"Cursor",agents:".cursor/agents",skills:".cursor/skills"}];const n=new Set([".md",".txt",".py",".js",".ts",".sh",".json",".yaml",".yml",".csv",".sql",".toml",".ini"]),r=/^[a-zA-Z0-9_-]{1,64}$/;function c(){return s.join(t.homedir(),".huicai","agents")}function o(){return s.join(t.homedir(),".huicai","skills")}export function discoverExternalAssets({base:n=t.homedir()}={}){const l=function(){try{return new Set(e.readdirSync(c()).filter(e=>e.endsWith(".md")).map(e=>e.replace(/\.md$/,"")))}catch{return new Set}}(),a=function(){try{return new Set(e.readdirSync(o(),{withFileTypes:!0}).filter(e=>e.isDirectory()).map(e=>e.name))}catch{return new Set}}(),m=[],d=[],p=new Set,u=new Set;for(const t of EXTERNAL_SOURCES){const c=s.join(n,t.agents);let o=[];try{o=e.readdirSync(c).filter(e=>e.endsWith(".md"))}catch{}for(const n of o){const o=s.join(c,n);let a;try{a=i(e.readFileSync(o,"utf8"))}catch{continue}const d=String(a.name||n.replace(/\.md$/,"")).trim();r.test(d)&&!p.has(d)&&(p.add(d),m.push({name:d,description:(a.description||"").slice(0,200),cli:t.cli,src:o,exists:l.has(d)}))}const f=s.join(n,t.skills);let h=[];try{h=e.readdirSync(f,{withFileTypes:!0}).filter(e=>e.isDirectory())}catch{}for(const n of h){const c=n.name.trim(),o=s.join(f,c),l=s.join(o,"SKILL.md");if(!r.test(c)||u.has(c)||!e.existsSync(l))continue;u.add(c);let m="";try{m=(i(e.readFileSync(l,"utf8")).description||"").slice(0,200)}catch{}d.push({name:c,description:m,cli:t.cli,src:o,exists:a.has(c)})}}return{agents:m,skills:d}}function l(t,i,r=0){if(r>3)return 0;let c=0,o=[];try{o=e.readdirSync(t,{withFileTypes:!0})}catch{return 0}e.mkdirSync(i,{recursive:!0});for(const a of o){const o=s.join(t,a.name),m=s.join(i,a.name);if(a.isDirectory())c+=l(o,m,r+1);else if(a.isFile()&&("SKILL.md"===a.name||n.has(s.extname(a.name).toLowerCase())))try{e.copyFileSync(o,m),c++}catch{}}return c}export function importExternalAssets(t={},{overwrite:i=!1}={}){const n=[],a={agents:{imported:0,skipped:0},skills:{imported:0,skipped:0},details:n},m=c(),d=o();for(const c of t.agents||[]){if(!c||!r.test(String(c.name))||!c.src)continue;const t=s.join(m,c.name+".md");if(i||!e.existsSync(t))try{e.mkdirSync(m,{recursive:!0}),e.copyFileSync(c.src,t),a.agents.imported++,n.push(`导入 agent:${c.name}`)}catch(e){n.push(`失败 agent ${c.name}:${e.message}`)}else a.agents.skipped++,n.push(`跳过 agent(已存在):${c.name}`)}for(const c of t.skills||[]){if(!c||!r.test(String(c.name))||!c.src)continue;const t=s.join(d,c.name);if(!i&&e.existsSync(t)){a.skills.skipped++,n.push(`跳过 skill(已存在):${c.name}`);continue}const o=l(c.src,t);o>0?(a.skills.imported++,n.push(`导入 skill:${c.name}(${o} 文件)`)):(a.skills.skipped++,n.push(`跳过 skill(无内容):${c.name}`))}return a}export function importAllExternalAssets(e={}){const s=discoverExternalAssets(e);return importExternalAssets({agents:s.agents.filter(e=>!e.exists),skills:s.skills.filter(e=>!e.exists)},e)}
|
package/src/autofix.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawn as t}from"node:child_process";import e from"node:fs";import
|
|
1
|
+
import{spawn as t}from"node:child_process";import{StringDecoder as e}from"node:string_decoder";import r from"node:fs";import n from"node:path";const o=16e3;export function captureCommand(r,n,s=18e4){const i="win32"===process.platform;return new Promise(c=>{let d="",a=!1;const p=i?t(process.env.COMSPEC||"cmd.exe",["/d","/s","/c",n],{cwd:r,windowsVerbatimArguments:!0}):t(process.env.SHELL||"/bin/sh",["-c",n],{cwd:r,detached:!0}),u=setTimeout(()=>{a=!0;try{!i&&p.pid?process.kill(-p.pid,"SIGKILL"):p.kill("SIGKILL")}catch{}},s),m=new e("utf8"),l=new e("utf8"),f=t=>{d.length<32e3&&(d+=t)};p.stdout.on("data",t=>f(m.write(t))),p.stderr.on("data",t=>f(l.write(t))),p.on("error",t=>{clearTimeout(u),c({code:-1,output:"命令启动失败:"+t.message})}),p.on("close",t=>{var e;clearTimeout(u),f(m.end()),f(l.end()),c({code:a?124:null==t?1:t,output:(e=d,((e=String(e)).length>o?e.slice(0,o)+"\n…(输出过长已截断)":e)||"(无输出)")})})})}export function detectVerifyCommand(t){const e=e=>{try{return r.existsSync(n.join(t,e))}catch{return!1}};try{if(e("package.json")){const e=JSON.parse(r.readFileSync(n.join(t,"package.json"),"utf8"));if(e.scripts?.test&&!/no test specified/i.test(e.scripts.test))return"npm test";if(e.scripts?.build)return"npm run build"}}catch{}return e("pyproject.toml")||e("pytest.ini")||e("tox.ini")?"pytest -q":e("pom.xml")?"mvn -q test":e("build.gradle")||e("build.gradle.kts")?"./gradlew test":e("Cargo.toml")?"cargo test":e("go.mod")?"go test ./...":e("Makefile")?"make test":null}export async function runVerifyLoop({command:t,maxIters:e=5,cwd:r,runTurnOnce:n,emit:o}){for(let s=0;s<e;s++){o(`▶ 验证 ${s+1}/${e}:${t}`);const{code:i,output:c}=await captureCommand(r,t);if(0===i)return o(`✅ 验证通过(第 ${s+1} 轮)。`),{passed:!0,iters:s+1};if(o(`✗ 未通过(退出码 ${i})`+(s<e-1?",让智能体定位修复后自动重跑…":"")),s===e-1)return o(`⚠ 已达上限 ${e} 轮仍未通过。`),{passed:!1,iters:s+1,output:c};const d=await n(`自动验证:运行 \`${t}\` 失败(退出码 ${i})。输出如下:\n\n${c}\n\n请定位根因并**修复代码/配置**。不要自己重跑该命令——我会自动重跑验证。本轮只做必要的修复。`);if(d&&d.aborted)return o("已中断修复循环。"),{passed:!1,aborted:!0,iters:s+1}}return{passed:!1}}
|
package/src/automation.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:fs";import e from"node:path";import{spawn as n}from"node:child_process";import{runBash as o}from"./executors.js";const r=3e4,a=3e5;export const AUTOMATION_TOOLS=new Set(["install_packages","setup_environment","install_plugin","open_browser","open_path","open_app","auto_execute"]);export function automationActionLabel(t,e={}){switch(t){case"install_packages":return`安装 Node.js 包:${Array.isArray(e.packages)&&e.packages.length?e.packages.join(" "):"package.json 全部依赖"}${e.global?"(全局)":""}${e.dev?"(devDependencies)":""}`;case"setup_environment":return Array.isArray(e.commands)&&e.commands.length?`自动配置运行环境:执行 ${e.commands.length} 条配置命令`:"自动配置运行环境:检测项目技术栈并安装依赖";case"install_plugin":return`安装${"jetbrains"===e.target?"JetBrains 插件":"npm"===e.target?"npm 全局工具":"VS Code 扩展"}:${e.id??""}`;case"open_browser":return`打开浏览器访问:${e.url??""}`;case"open_path":return`在文件管理器中打开:${e.path??""}`;case"open_app":return`启动应用:${e.name??""}${e.target?"("+e.target+")":""}`;case"auto_execute":{const t=String(e.goal||"").replace(/\s+/g," ");return"自动执行已规划的任务"+(t?":"+(t.length>60?t.slice(0,60)+"…":t):"")}default:return t}}export function automationDetail(t,e={}){return"setup_environment"===t&&Array.isArray(e.commands)&&e.commands.length?e.commands.slice(0,12).map(t=>"$ "+String(t)).join("\n"):"open_browser"===t?String(e.url??""):"open_path"===t?String(e.path??""):"open_app"===t?String(e.target??""):""}export function which(n,{pathStr:o=process.env.PATH||"",platform:r=process.platform}={}){const a="win32"===r?";":":",s="win32"===r?[".cmd",".exe",".bat",""]:[""];for(const r of o.split(a))if(r)for(const o of s){const a=e.join(r,n+o);try{if(t.accessSync(a,t.constants.X_OK),t.statSync(a).isFile())return a}catch{}}return null}export function runArgv(t,e,{cwd:o,timeoutMs:a=3e5,winShell:s=!1}={}){const i="win32"===process.platform;return new Promise(c=>{let l,p="",u=!1;try{l=n(t,e,i?{cwd:o,shell:!!s}:{cwd:o,detached:!0})}catch(t){return void c({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}const d=setTimeout(()=>{u=!0,(()=>{try{!i&&l.pid?process.kill(-l.pid,"SIGKILL"):l.kill("SIGKILL")}catch{try{l.kill("SIGKILL")}catch{}}})()},a),m=t=>{p.length<r&&(p+=t.toString())};l.stdout?.on("data",m),l.stderr?.on("data",m),l.on("error",t=>{clearTimeout(d),c({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}),l.on("close",t=>{clearTimeout(d),c({code:t,killed:u,output:p.length>r?p.slice(0,r)+"\n…(输出已截断)":p})})})}function s(t,e){return new Promise(o=>{let r,a="";try{r=n(t,e,{stdio:["ignore","pipe","pipe"]})}catch(t){return void o({code:-1,output:`命令启动失败:${t.message}`})}const s=setTimeout(()=>{try{r.unref()}catch{}o({code:0,output:a,lingering:!0})},8e3);r.stdout.on("data",t=>a+=t),r.stderr.on("data",t=>a+=t),r.on("error",t=>{clearTimeout(s),o({code:-1,output:`命令启动失败:${t.message}`})}),r.on("close",t=>{clearTimeout(s),o({code:t,output:a})})})}function i(t){if(!t||"string"!=typeof t)throw new Error("工作目录未设置:请在项目目录中运行(VS Code / IDEA 中需先「打开文件夹」)")}function c(t,e=1200){const n=String(t||"").trim();return n.length>e?"…"+n.slice(-e):n}export function detectManager(n){try{if(t.existsSync(e.join(n,"pnpm-lock.yaml")))return"pnpm";if(t.existsSync(e.join(n,"yarn.lock")))return"yarn"}catch{}return"npm"}export function validPackageToken(t){return!("string"!=typeof t||!t.trim()||/\s/.test(t)||t.startsWith("-"))&&/^(@[a-z0-9~][\w.-]*\/)?[a-z0-9~][\w.-]*(@[\w.^~=+*:-]+)?$/i.test(t)}export function buildInstallPlan(t,e={},n=process.platform){const o=["npm","pnpm","yarn"].includes(String(e.manager||"").toLowerCase())?String(e.manager).toLowerCase():detectManager(t),r=(Array.isArray(e.packages)?e.packages:[]).map(t=>String(t).trim()).filter(Boolean);for(const t of r)if(!validPackageToken(t))throw new Error(`包名不合法(仅支持 名字[@版本],不接受选项/空白/特殊字符):${t}`);const a=!!e.dev,s=!!e.global;if(s&&!r.length)throw new Error("全局安装(global=true)需要明确的 packages 列表");let i;i=r.length?"yarn"===o?s?["global","add",...r]:["add",...a?["-D"]:[],...r]:"pnpm"===o?["add",...s?["-g"]:[],...a?["-D"]:[],...r]:["install",...s?["-g"]:[],...a?["--save-dev"]:[],...r]:["install"];return{manager:o,bin:o,argv:i,winShell:"win32"===n,display:`${o} ${i.join(" ")}`}}export async function installPackages(t,e){i(t);const n=buildInstallPlan(t,e||{});if(!which(n.bin))return`未找到 ${n.bin}:请先安装 Node.js(https://nodejs.org,macOS 可 brew install node)`+("npm"!==n.manager?',或改用 manager:"npm"':"");const o=Number.isFinite(e?.timeout)?Math.min(e.timeout,6e5):a,r=await runArgv(n.bin,n.argv,{cwd:t,timeoutMs:o,winShell:n.winShell}),s=r.killed?"【超时被终止】":`退出码 ${r.code}`,l=`$ ${n.display}\n${s}`;return 0===r.code?`${l}\n已安装完成。${c(r.output,800)?"\n"+c(r.output,800):""}`:`${l}\n${c(r.output)||"(无输出)"}`}export function buildSetupSteps(n,o={},r={}){const a=r.platform||process.platform,s=r.pathStr,i=Array.isArray(o.commands)?o.commands.map(t=>String(t).trim()).filter(Boolean).slice(0,12):null;if(i&&i.length)return{mode:"commands",commands:i,detected:[]};const c=o=>{try{return t.existsSync(e.join(n,o))}catch{return!1}},l=[];c("package.json")&&l.push("node"),(c("requirements.txt")||c("pyproject.toml"))&&l.push("python"),c("go.mod")&&l.push("go"),c("Cargo.toml")&&l.push("rust"),c("pom.xml")&&l.push("maven"),(c("build.gradle")||c("build.gradle.kts"))&&l.push("gradle");const p=Array.isArray(o.stacks)&&o.stacks.length?l.filter(t=>o.stacks.includes(t)):l,u=[],d=[],m=(t,e)=>which(t,{pathStr:s,platform:a})?null:e;for(const t of p)if("node"===t){const e=detectManager(n),o=m(e,`未找到 ${e}:请先安装 Node.js(https://nodejs.org,macOS 可 brew install node)`);u.push(o?{stack:t,label:"安装 Node.js 依赖",missing:o}:{stack:t,label:`安装 Node.js 依赖(${e} install)`,bin:e,argv:["install"],winShell:!0})}else if("python"===t){const o="win32"===a?"python":"python3",r=m(o,`未找到 ${o}:macOS 可 brew install python,或从 python.org 安装`);if(r){u.push({stack:t,label:"配置 Python 环境",missing:r});continue}c(".venv")?d.push(".venv 已存在,跳过创建"):u.push({stack:t,label:"创建 Python 虚拟环境 .venv",bin:o,argv:["-m","venv",".venv"]});const s="win32"===a?".venv\\Scripts\\pip.exe":e.join(n,".venv/bin/pip");u.push(c("requirements.txt")?{stack:t,label:"安装 Python 依赖(requirements.txt)",bin:s,argv:["install","-r","requirements.txt"]}:{stack:t,label:"安装 Python 依赖(pyproject.toml)",bin:s,argv:["install","-e","."]})}else if("go"===t){const e=m("go","未找到 go:可从 https://go.dev/dl 安装(macOS 可 brew install go)");u.push(e?{stack:t,label:"下载 Go 依赖",missing:e}:{stack:t,label:"下载 Go 依赖(go mod download)",bin:"go",argv:["mod","download"]})}else if("rust"===t){const e=m("cargo","未找到 cargo:可用 rustup 安装(https://rustup.rs)");u.push(e?{stack:t,label:"拉取 Rust 依赖",missing:e}:{stack:t,label:"拉取 Rust 依赖(cargo fetch)",bin:"cargo",argv:["fetch"]})}else if("maven"===t){const e=m("mvn","未找到 mvn:macOS 可 brew install maven");u.push(e?{stack:t,label:"解析 Maven 依赖",missing:e}:{stack:t,label:"解析 Maven 依赖(mvn dependency:resolve)",bin:"mvn",argv:["-q","-DskipTests","dependency:resolve"],winShell:!0})}else"gradle"===t&&d.push("检测到 Gradle 项目:构建脚本可执行任意代码,出于安全未自动执行;可手动运行 ./gradlew build,或让我用 run_bash 执行");return{mode:"auto",detected:l,want:p,steps:u,notes:d}}export async function setupEnvironment(t,e){i(t);const n=e||{},r=buildSetupSteps(t,n),a=Number.isFinite(n.timeout)?Math.min(n.timeout,6e5):6e5;if("commands"===r.mode){const e=["环境配置(按给定命令逐条执行):"];for(const n of r.commands){const r=await o(t,{command:n,timeout:a});if(e.push(`$ ${n}\n${r}`),!/^退出码 0\b/.test(String(r))){e.push("(上一步未成功,中止后续配置命令)");break}}return e.join("\n\n")}if(!r.detected.length)return"未检测到已知技术栈(package.json / requirements.txt / pyproject.toml / go.mod / Cargo.toml / pom.xml / build.gradle)。可改用 commands 参数显式指定要执行的配置命令。";const s=[`检测到技术栈:${r.detected.join("、")}`+(r.want.length!==r.detected.length?`(本次仅处理:${r.want.join("、")||"无"})`:"")];for(const e of r.steps){if(e.missing){s.push(`✘ ${e.label}:${e.missing}`);continue}const n=await runArgv(e.bin,e.argv,{cwd:t,timeoutMs:a,winShell:e.winShell});if(0!==n.code){s.push(`✘ ${e.label} → ${n.killed?"超时被终止":"退出码 "+n.code}${c(n.output)?"\n"+c(n.output):""}`),s.push("(该步失败,停止后续步骤;可修正后重试,或用 commands 显式指定命令)");break}s.push(`✔ ${e.label} → 退出码 0`)}for(const t of r.notes||[])s.push("· "+t);return"环境配置报告:\n"+s.join("\n")}const l=["code","code-insiders","cursor","windsurf"],p=["idea","idea64","webstorm","pycharm","goland","rider","clion"];export function buildPluginPlan(t={},e={}){const n=e.platform||process.platform,o=e.pathStr,r=String(t.id||"").trim();if(!r||r.startsWith("-")||!/^[@\w./:-]+$/.test(r))throw new Error(`插件 id 不合法:${r||"(空)"}`);const a=String(t.target||"").trim().toLowerCase();if("vscode"===a){for(const t of l)if(which(t,{pathStr:o,platform:n}))return{kind:"vscode",bin:t,argv:["--install-extension",r],winShell:!0,note:`经 ${t} CLI 安装`};throw new Error("未找到 VS Code 命令行工具(code)。请在 VS Code 命令面板执行「Shell Command: Install 'code' command in PATH」后重试,或到扩展市场手动安装 "+r)}if("jetbrains"===a){for(const t of p)if(which(t,{pathStr:o,platform:n}))return{kind:"jetbrains",bin:t,argv:["installPlugins",r],note:"安装后需重启 IDE 生效"};throw new Error("未找到 JetBrains IDE 命令行启动器(idea 等)。可在 IDE 的 Tools → Create Command-line Launcher 生成,或到 Settings → Plugins 里搜索安装 "+r)}if("npm"===a){if(!which("npm",{pathStr:o,platform:n}))throw new Error("未找到 npm:请先安装 Node.js(https://nodejs.org)");return{kind:"npm",bin:"npm",argv:["install","-g",r],winShell:!0,note:"全局 CLI 工具"}}throw new Error("target 需为 vscode | jetbrains | npm")}export async function installPlugin(t,e){const n=buildPluginPlan(e||{}),o="jetbrains"===n.kind?18e4:a,r=await runArgv(n.bin,n.argv,{cwd:t||void 0,timeoutMs:o,winShell:n.winShell}),s=`${n.bin} ${n.argv.join(" ")}`;return 0===r.code?`$ ${s}\n退出码 0\n已安装 ${e?.id}${n.note?"("+n.note+")":""}${c(r.output,600)?"\n"+c(r.output,600):""}`:`$ ${s}\n${r.killed?"【超时被终止】":"退出码 "+r.code}\n${c(r.output)||"(无输出)"}`}export async function openBrowser(t,e){const n=String(e?.url||"").trim();if(!/^https?:\/\/\S+$/i.test(n))throw new Error("仅支持 http/https 网址");const o=process.platform,r="darwin"===o?await s("open",[n]):"win32"===o?await s("rundll32",["url.dll,FileProtocolHandler",n]):await s("xdg-open",[n]);return 0!==r.code?`打开浏览器失败(退出码 ${r.code}):${c(r.output)||"未知原因"}`:`已在系统默认浏览器打开:${n}`}export async function openPath(n,o){const r=String(o?.path||"").trim();if(!r)throw new Error("缺少路径 path");const a=e.resolve(n||".",r);if(!t.existsSync(a))throw new Error("路径不存在:"+a);const i=t.statSync(a).isDirectory(),l=null!=o?.reveal?!!o.reveal:!i,p=process.platform;let u;return"darwin"===p?u=await s("open",l&&!i?["-R",a]:[a]):"win32"===p?(u=await s("explorer.exe",l&&!i?["/select,"+a]:[a]),u={code:0,output:u.output}):u=await s("xdg-open",[l&&!i?e.dirname(a):a]),0!==u.code?`打开文件管理器失败(退出码 ${u.code}):${c(u.output)||"未知原因"}`:`已在文件管理器中${l&&!i?"定位":"打开"}:${a}`}export async function openApp(t,e){const o=String(e?.name||"").trim(),r=null!=e?.target?String(e.target).trim():"";if(!o)throw new Error("缺少应用名 name");if(o.length>120||/[\0-\x1f"'`$\\;|&<>{}[\]*?%]/.test(o))throw new Error("应用名含不允许的字符:"+o);if(r&&/[\0-\x1f"'`$\\;|&<>]/.test(r))throw new Error("target 含不允许的字符:"+r);const a=process.platform;if("darwin"===a){const t=await s("open",r?["-a",o,r]:["-a",o]);return 0!==t.code?`启动应用失败(退出码 ${t.code}):${c(t.output)||"未找到应用 "+o}`:`已启动应用:${o}${r?"(打开 "+r+")":""}`}if("win32"===a)return await new Promise(t=>{const e=`start "" "${o}"${r?` "${r}"`:""}`,a=n(process.env.COMSPEC||"cmd.exe",["/d","/s","/c",e],{windowsVerbatimArguments:!0});a.on("error",e=>t(`启动应用失败:${e.message}`)),a.on("close",e=>t(0===e?`已启动应用:${o}`:`启动应用失败(退出码 ${e})`))});if(!/^[\w./+-]+$/.test(o))throw new Error("Linux 下应用名需为可执行文件名(字母数字._/+-):"+o);return await new Promise(t=>{const e=n(o,r?[r]:[],{detached:!0,stdio:"ignore"});e.once("error",e=>t(`启动应用失败:${e.message}`)),e.once("spawn",()=>{e.unref(),t(`已启动应用:${o}${r?"(打开 "+r+")":""}`)})})}const u=(t,e)=>({type:"object",properties:t,required:e||[]}),d=t=>({type:"string",description:t}),m=t=>({type:"boolean",description:t}),g=t=>({type:"integer",description:t});export const AUTOMATION_TOOL_SPECS=[{type:"function",function:{name:"install_packages",description:"安装 Node.js 依赖包(npm/pnpm/yarn 按锁文件自动识别;执行前会征询用户同意)。省略 packages 时安装 package.json 的全部依赖。仅支持包名[@版本],不接受额外命令行选项。",parameters:u({packages:{type:"array",items:{type:"string"},description:'要安装的包(如 ["lodash","@types/node@^20"]);省略=安装全部依赖'},dev:m("可选:装入 devDependencies(默认否)"),global:m("可选:全局安装(需明确 packages;默认否)"),manager:d("可选:npm | pnpm | yarn(默认按锁文件自动识别)"),timeout:g("可选:超时毫秒(默认 300000,上限 600000)")},[])}},{type:"function",function:{name:"setup_environment",description:"自动配置项目运行环境(执行前会征询用户同意):检测技术栈(Node/Python/Go/Rust/Maven)→ 检查工具链是否安装 → 安装依赖(npm install、创建 .venv 并 pip install、go mod download 等)。也可用 commands 显式指定要执行的配置命令(逐条执行,失败即停)。",parameters:u({stacks:{type:"array",items:{type:"string"},description:"可选:只处理这些技术栈(node|python|go|rust|maven)"},commands:{type:"array",items:{type:"string"},description:"可选:显式配置命令列表(优先于自动检测,最多 12 条)"},timeout:g("可选:单步超时毫秒(默认 600000)")},[])}},{type:"function",function:{name:"install_plugin",description:"安装编辑器/IDE 插件(执行前会征询用户同意):VS Code 扩展(code --install-extension)、JetBrains 插件(idea installPlugins,装后需重启 IDE)、或 npm 全局 CLI 工具(npm install -g)。",parameters:u({target:d("vscode | jetbrains | npm"),id:d("插件标识:VS Code 扩展 id(如 dbaeumer.vscode-eslint)/ JetBrains 插件 id / npm 包名")},["target","id"])}},{type:"function",function:{name:"open_browser",description:"用系统默认浏览器打开一个 http/https 网址(执行前会征询用户同意)。适合把文档、本地预览页、报告直接呈现给用户。",parameters:u({url:d("完整网址(http/https)")},["url"])}},{type:"function",function:{name:"open_path",description:"在系统文件管理器(访达/资源管理器)中打开目录或定位文件(执行前会征询用户同意)。",parameters:u({path:d("目录或文件路径(相对项目根或绝对路径)"),reveal:m("可选:定位(选中)该文件而非打开;文件默认定位、目录默认打开")},["path"])}},{type:"function",function:{name:"open_app",description:'启动本机任意应用程序(执行前会征询用户同意),可附带要打开的文件或网址。macOS 用应用名(如 "Safari"、"Visual Studio Code"),Windows 用可启动名,Linux 用可执行文件名。',parameters:u({name:d("应用名/可执行名"),target:d("可选:让该应用打开的文件路径或 URL")},["name"])}}];export const AUTOMATION_EXECUTORS={install_packages:installPackages,setup_environment:setupEnvironment,install_plugin:installPlugin,open_browser:openBrowser,open_path:openPath,open_app:openApp};
|
|
1
|
+
import t from"node:fs";import e from"node:path";import{spawn as n}from"node:child_process";import{StringDecoder as o}from"node:string_decoder";import{runBash as r}from"./executors.js";const a=3e4,s=3e5;export const AUTOMATION_TOOLS=new Set(["install_packages","setup_environment","install_plugin","open_browser","open_path","open_app","auto_execute"]);export function automationActionLabel(t,e={}){switch(t){case"install_packages":return`安装 Node.js 包:${Array.isArray(e.packages)&&e.packages.length?e.packages.join(" "):"package.json 全部依赖"}${e.global?"(全局)":""}${e.dev?"(devDependencies)":""}`;case"setup_environment":return Array.isArray(e.commands)&&e.commands.length?`自动配置运行环境:执行 ${e.commands.length} 条配置命令`:"自动配置运行环境:检测项目技术栈并安装依赖";case"install_plugin":return`安装${"jetbrains"===e.target?"JetBrains 插件":"npm"===e.target?"npm 全局工具":"VS Code 扩展"}:${e.id??""}`;case"open_browser":return`打开浏览器访问:${e.url??""}`;case"open_path":return`在文件管理器中打开:${e.path??""}`;case"open_app":return`启动应用:${e.name??""}${e.target?"("+e.target+")":""}`;case"auto_execute":{const t=String(e.goal||"").replace(/\s+/g," ");return"自动执行已规划的任务"+(t?":"+(t.length>60?t.slice(0,60)+"…":t):"")}default:return t}}export function automationDetail(t,e={}){return"setup_environment"===t&&Array.isArray(e.commands)&&e.commands.length?e.commands.slice(0,12).map(t=>"$ "+String(t)).join("\n"):"open_browser"===t?String(e.url??""):"open_path"===t?String(e.path??""):"open_app"===t?String(e.target??""):""}export function which(n,{pathStr:o=process.env.PATH||"",platform:r=process.platform}={}){const a="win32"===r?";":":",s="win32"===r?[".cmd",".exe",".bat",""]:[""];for(const r of o.split(a))if(r)for(const o of s){const a=e.join(r,n+o);try{if(t.accessSync(a,t.constants.X_OK),t.statSync(a).isFile())return a}catch{}}return null}export function runArgv(t,e,{cwd:r,timeoutMs:s=3e5,winShell:i=!1}={}){const c="win32"===process.platform;return new Promise(p=>{let l,u="",d=!1;try{l=n(t,e,c?{cwd:r,shell:!!i}:{cwd:r,detached:!0})}catch(t){return void p({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}const m=setTimeout(()=>{d=!0,(()=>{try{!c&&l.pid?process.kill(-l.pid,"SIGKILL"):l.kill("SIGKILL")}catch{try{l.kill("SIGKILL")}catch{}}})()},s),g=new o("utf8"),h=new o("utf8"),w=t=>{u.length<a&&(u+=t)};l.stdout?.on("data",t=>w(g.write(t))),l.stderr?.on("data",t=>w(h.write(t))),l.on("error",t=>{clearTimeout(m),p({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}),l.on("close",t=>{clearTimeout(m),w(g.end()),w(h.end()),p({code:t,killed:d,output:u.length>a?u.slice(0,a)+"\n…(输出已截断)":u})})})}function i(t,e){return new Promise(o=>{let r,a="";try{r=n(t,e,{stdio:["ignore","pipe","pipe"]})}catch(t){return void o({code:-1,output:`命令启动失败:${t.message}`})}const s=setTimeout(()=>{try{r.unref()}catch{}o({code:0,output:a,lingering:!0})},8e3);r.stdout.on("data",t=>a+=t),r.stderr.on("data",t=>a+=t),r.on("error",t=>{clearTimeout(s),o({code:-1,output:`命令启动失败:${t.message}`})}),r.on("close",t=>{clearTimeout(s),o({code:t,output:a})})})}function c(t){if(!t||"string"!=typeof t)throw new Error("工作目录未设置:请在项目目录中运行(VS Code / IDEA 中需先「打开文件夹」)")}function p(t,e=1200){const n=String(t||"").trim();return n.length>e?"…"+n.slice(-e):n}export function detectManager(n){try{if(t.existsSync(e.join(n,"pnpm-lock.yaml")))return"pnpm";if(t.existsSync(e.join(n,"yarn.lock")))return"yarn"}catch{}return"npm"}export function validPackageToken(t){return!("string"!=typeof t||!t.trim()||/\s/.test(t)||t.startsWith("-"))&&/^(@[a-z0-9~][\w.-]*\/)?[a-z0-9~][\w.-]*(@[\w.^~=+*:-]+)?$/i.test(t)}export function buildInstallPlan(t,e={},n=process.platform){const o=["npm","pnpm","yarn"].includes(String(e.manager||"").toLowerCase())?String(e.manager).toLowerCase():detectManager(t),r=(Array.isArray(e.packages)?e.packages:[]).map(t=>String(t).trim()).filter(Boolean);for(const t of r)if(!validPackageToken(t))throw new Error(`包名不合法(仅支持 名字[@版本],不接受选项/空白/特殊字符):${t}`);const a=!!e.dev,s=!!e.global;if(s&&!r.length)throw new Error("全局安装(global=true)需要明确的 packages 列表");let i;i=r.length?"yarn"===o?s?["global","add",...r]:["add",...a?["-D"]:[],...r]:"pnpm"===o?["add",...s?["-g"]:[],...a?["-D"]:[],...r]:["install",...s?["-g"]:[],...a?["--save-dev"]:[],...r]:["install"];return{manager:o,bin:o,argv:i,winShell:"win32"===n,display:`${o} ${i.join(" ")}`}}export async function installPackages(t,e){c(t);const n=buildInstallPlan(t,e||{});if(!which(n.bin))return`未找到 ${n.bin}:请先安装 Node.js(https://nodejs.org,macOS 可 brew install node)`+("npm"!==n.manager?',或改用 manager:"npm"':"");const o=Number.isFinite(e?.timeout)?Math.min(e.timeout,6e5):s,r=await runArgv(n.bin,n.argv,{cwd:t,timeoutMs:o,winShell:n.winShell}),a=r.killed?"【超时被终止】":`退出码 ${r.code}`,i=`$ ${n.display}\n${a}`;return 0===r.code?`${i}\n已安装完成。${p(r.output,800)?"\n"+p(r.output,800):""}`:`${i}\n${p(r.output)||"(无输出)"}`}export function buildSetupSteps(n,o={},r={}){const a=r.platform||process.platform,s=r.pathStr,i=Array.isArray(o.commands)?o.commands.map(t=>String(t).trim()).filter(Boolean).slice(0,12):null;if(i&&i.length)return{mode:"commands",commands:i,detected:[]};const c=o=>{try{return t.existsSync(e.join(n,o))}catch{return!1}},p=[];c("package.json")&&p.push("node"),(c("requirements.txt")||c("pyproject.toml"))&&p.push("python"),c("go.mod")&&p.push("go"),c("Cargo.toml")&&p.push("rust"),c("pom.xml")&&p.push("maven"),(c("build.gradle")||c("build.gradle.kts"))&&p.push("gradle");const l=Array.isArray(o.stacks)&&o.stacks.length?p.filter(t=>o.stacks.includes(t)):p,u=[],d=[],m=(t,e)=>which(t,{pathStr:s,platform:a})?null:e;for(const t of l)if("node"===t){const e=detectManager(n),o=m(e,`未找到 ${e}:请先安装 Node.js(https://nodejs.org,macOS 可 brew install node)`);u.push(o?{stack:t,label:"安装 Node.js 依赖",missing:o}:{stack:t,label:`安装 Node.js 依赖(${e} install)`,bin:e,argv:["install"],winShell:!0})}else if("python"===t){const o="win32"===a?"python":"python3",r=m(o,`未找到 ${o}:macOS 可 brew install python,或从 python.org 安装`);if(r){u.push({stack:t,label:"配置 Python 环境",missing:r});continue}c(".venv")?d.push(".venv 已存在,跳过创建"):u.push({stack:t,label:"创建 Python 虚拟环境 .venv",bin:o,argv:["-m","venv",".venv"]});const s="win32"===a?".venv\\Scripts\\pip.exe":e.join(n,".venv/bin/pip");u.push(c("requirements.txt")?{stack:t,label:"安装 Python 依赖(requirements.txt)",bin:s,argv:["install","-r","requirements.txt"]}:{stack:t,label:"安装 Python 依赖(pyproject.toml)",bin:s,argv:["install","-e","."]})}else if("go"===t){const e=m("go","未找到 go:可从 https://go.dev/dl 安装(macOS 可 brew install go)");u.push(e?{stack:t,label:"下载 Go 依赖",missing:e}:{stack:t,label:"下载 Go 依赖(go mod download)",bin:"go",argv:["mod","download"]})}else if("rust"===t){const e=m("cargo","未找到 cargo:可用 rustup 安装(https://rustup.rs)");u.push(e?{stack:t,label:"拉取 Rust 依赖",missing:e}:{stack:t,label:"拉取 Rust 依赖(cargo fetch)",bin:"cargo",argv:["fetch"]})}else if("maven"===t){const e=m("mvn","未找到 mvn:macOS 可 brew install maven");u.push(e?{stack:t,label:"解析 Maven 依赖",missing:e}:{stack:t,label:"解析 Maven 依赖(mvn dependency:resolve)",bin:"mvn",argv:["-q","-DskipTests","dependency:resolve"],winShell:!0})}else"gradle"===t&&d.push("检测到 Gradle 项目:构建脚本可执行任意代码,出于安全未自动执行;可手动运行 ./gradlew build,或让我用 run_bash 执行");return{mode:"auto",detected:p,want:l,steps:u,notes:d}}export async function setupEnvironment(t,e){c(t);const n=e||{},o=buildSetupSteps(t,n),a=Number.isFinite(n.timeout)?Math.min(n.timeout,6e5):6e5;if("commands"===o.mode){const e=["环境配置(按给定命令逐条执行):"];for(const n of o.commands){const o=await r(t,{command:n,timeout:a});if(e.push(`$ ${n}\n${o}`),!/^退出码 0\b/.test(String(o))){e.push("(上一步未成功,中止后续配置命令)");break}}return e.join("\n\n")}if(!o.detected.length)return"未检测到已知技术栈(package.json / requirements.txt / pyproject.toml / go.mod / Cargo.toml / pom.xml / build.gradle)。可改用 commands 参数显式指定要执行的配置命令。";const s=[`检测到技术栈:${o.detected.join("、")}`+(o.want.length!==o.detected.length?`(本次仅处理:${o.want.join("、")||"无"})`:"")];for(const e of o.steps){if(e.missing){s.push(`✘ ${e.label}:${e.missing}`);continue}const n=await runArgv(e.bin,e.argv,{cwd:t,timeoutMs:a,winShell:e.winShell});if(0!==n.code){s.push(`✘ ${e.label} → ${n.killed?"超时被终止":"退出码 "+n.code}${p(n.output)?"\n"+p(n.output):""}`),s.push("(该步失败,停止后续步骤;可修正后重试,或用 commands 显式指定命令)");break}s.push(`✔ ${e.label} → 退出码 0`)}for(const t of o.notes||[])s.push("· "+t);return"环境配置报告:\n"+s.join("\n")}const l=["code","code-insiders","cursor","windsurf"],u=["idea","idea64","webstorm","pycharm","goland","rider","clion"];export function buildPluginPlan(t={},e={}){const n=e.platform||process.platform,o=e.pathStr,r=String(t.id||"").trim();if(!r||r.startsWith("-")||!/^[@\w./:-]+$/.test(r))throw new Error(`插件 id 不合法:${r||"(空)"}`);const a=String(t.target||"").trim().toLowerCase();if("vscode"===a){for(const t of l)if(which(t,{pathStr:o,platform:n}))return{kind:"vscode",bin:t,argv:["--install-extension",r],winShell:!0,note:`经 ${t} CLI 安装`};throw new Error("未找到 VS Code 命令行工具(code)。请在 VS Code 命令面板执行「Shell Command: Install 'code' command in PATH」后重试,或到扩展市场手动安装 "+r)}if("jetbrains"===a){for(const t of u)if(which(t,{pathStr:o,platform:n}))return{kind:"jetbrains",bin:t,argv:["installPlugins",r],note:"安装后需重启 IDE 生效"};throw new Error("未找到 JetBrains IDE 命令行启动器(idea 等)。可在 IDE 的 Tools → Create Command-line Launcher 生成,或到 Settings → Plugins 里搜索安装 "+r)}if("npm"===a){if(!which("npm",{pathStr:o,platform:n}))throw new Error("未找到 npm:请先安装 Node.js(https://nodejs.org)");return{kind:"npm",bin:"npm",argv:["install","-g",r],winShell:!0,note:"全局 CLI 工具"}}throw new Error("target 需为 vscode | jetbrains | npm")}export async function installPlugin(t,e){const n=buildPluginPlan(e||{}),o="jetbrains"===n.kind?18e4:s,r=await runArgv(n.bin,n.argv,{cwd:t||void 0,timeoutMs:o,winShell:n.winShell}),a=`${n.bin} ${n.argv.join(" ")}`;return 0===r.code?`$ ${a}\n退出码 0\n已安装 ${e?.id}${n.note?"("+n.note+")":""}${p(r.output,600)?"\n"+p(r.output,600):""}`:`$ ${a}\n${r.killed?"【超时被终止】":"退出码 "+r.code}\n${p(r.output)||"(无输出)"}`}export async function openBrowser(t,e){const n=String(e?.url||"").trim();if(!/^https?:\/\/\S+$/i.test(n))throw new Error("仅支持 http/https 网址");const o=process.platform,r="darwin"===o?await i("open",[n]):"win32"===o?await i("rundll32",["url.dll,FileProtocolHandler",n]):await i("xdg-open",[n]);return 0!==r.code?`打开浏览器失败(退出码 ${r.code}):${p(r.output)||"未知原因"}`:`已在系统默认浏览器打开:${n}`}export async function openPath(n,o){const r=String(o?.path||"").trim();if(!r)throw new Error("缺少路径 path");const a=e.resolve(n||".",r);if(!t.existsSync(a))throw new Error("路径不存在:"+a);const s=t.statSync(a).isDirectory(),c=null!=o?.reveal?!!o.reveal:!s,l=process.platform;let u;return"darwin"===l?u=await i("open",c&&!s?["-R",a]:[a]):"win32"===l?(u=await i("explorer.exe",c&&!s?["/select,"+a]:[a]),u={code:0,output:u.output}):u=await i("xdg-open",[c&&!s?e.dirname(a):a]),0!==u.code?`打开文件管理器失败(退出码 ${u.code}):${p(u.output)||"未知原因"}`:`已在文件管理器中${c&&!s?"定位":"打开"}:${a}`}export async function openApp(t,e){const o=String(e?.name||"").trim(),r=null!=e?.target?String(e.target).trim():"";if(!o)throw new Error("缺少应用名 name");if(o.length>120||/[\0-\x1f"'`$\\;|&<>{}[\]*?%]/.test(o))throw new Error("应用名含不允许的字符:"+o);if(r&&/[\0-\x1f"'`$\\;|&<>]/.test(r))throw new Error("target 含不允许的字符:"+r);const a=process.platform;if("darwin"===a){const t=await i("open",r?["-a",o,r]:["-a",o]);return 0!==t.code?`启动应用失败(退出码 ${t.code}):${p(t.output)||"未找到应用 "+o}`:`已启动应用:${o}${r?"(打开 "+r+")":""}`}if("win32"===a)return await new Promise(t=>{const e=`start "" "${o}"${r?` "${r}"`:""}`,a=n(process.env.COMSPEC||"cmd.exe",["/d","/s","/c",e],{windowsVerbatimArguments:!0});a.on("error",e=>t(`启动应用失败:${e.message}`)),a.on("close",e=>t(0===e?`已启动应用:${o}`:`启动应用失败(退出码 ${e})`))});if(!/^[\w./+-]+$/.test(o))throw new Error("Linux 下应用名需为可执行文件名(字母数字._/+-):"+o);return await new Promise(t=>{const e=n(o,r?[r]:[],{detached:!0,stdio:"ignore"});e.once("error",e=>t(`启动应用失败:${e.message}`)),e.once("spawn",()=>{e.unref(),t(`已启动应用:${o}${r?"(打开 "+r+")":""}`)})})}const d=(t,e)=>({type:"object",properties:t,required:e||[]}),m=t=>({type:"string",description:t}),g=t=>({type:"boolean",description:t}),h=t=>({type:"integer",description:t});export const AUTOMATION_TOOL_SPECS=[{type:"function",function:{name:"install_packages",description:"安装 Node.js 依赖包(npm/pnpm/yarn 按锁文件自动识别;执行前会征询用户同意)。省略 packages 时安装 package.json 的全部依赖。仅支持包名[@版本],不接受额外命令行选项。",parameters:d({packages:{type:"array",items:{type:"string"},description:'要安装的包(如 ["lodash","@types/node@^20"]);省略=安装全部依赖'},dev:g("可选:装入 devDependencies(默认否)"),global:g("可选:全局安装(需明确 packages;默认否)"),manager:m("可选:npm | pnpm | yarn(默认按锁文件自动识别)"),timeout:h("可选:超时毫秒(默认 300000,上限 600000)")},[])}},{type:"function",function:{name:"setup_environment",description:"自动配置项目运行环境(执行前会征询用户同意):检测技术栈(Node/Python/Go/Rust/Maven)→ 检查工具链是否安装 → 安装依赖(npm install、创建 .venv 并 pip install、go mod download 等)。也可用 commands 显式指定要执行的配置命令(逐条执行,失败即停)。",parameters:d({stacks:{type:"array",items:{type:"string"},description:"可选:只处理这些技术栈(node|python|go|rust|maven)"},commands:{type:"array",items:{type:"string"},description:"可选:显式配置命令列表(优先于自动检测,最多 12 条)"},timeout:h("可选:单步超时毫秒(默认 600000)")},[])}},{type:"function",function:{name:"install_plugin",description:"安装编辑器/IDE 插件(执行前会征询用户同意):VS Code 扩展(code --install-extension)、JetBrains 插件(idea installPlugins,装后需重启 IDE)、或 npm 全局 CLI 工具(npm install -g)。",parameters:d({target:m("vscode | jetbrains | npm"),id:m("插件标识:VS Code 扩展 id(如 dbaeumer.vscode-eslint)/ JetBrains 插件 id / npm 包名")},["target","id"])}},{type:"function",function:{name:"open_browser",description:"用系统默认浏览器打开一个 http/https 网址(执行前会征询用户同意)。适合把文档、本地预览页、报告直接呈现给用户。",parameters:d({url:m("完整网址(http/https)")},["url"])}},{type:"function",function:{name:"open_path",description:"在系统文件管理器(访达/资源管理器)中打开目录或定位文件(执行前会征询用户同意)。",parameters:d({path:m("目录或文件路径(相对项目根或绝对路径)"),reveal:g("可选:定位(选中)该文件而非打开;文件默认定位、目录默认打开")},["path"])}},{type:"function",function:{name:"open_app",description:'启动本机任意应用程序(执行前会征询用户同意),可附带要打开的文件或网址。macOS 用应用名(如 "Safari"、"Visual Studio Code"),Windows 用可启动名,Linux 用可执行文件名。',parameters:d({name:m("应用名/可执行名"),target:m("可选:让该应用打开的文件路径或 URL")},["name"])}}];export const AUTOMATION_EXECUTORS={install_packages:installPackages,setup_environment:setupEnvironment,install_plugin:installPlugin,open_browser:openBrowser,open_path:openPath,open_app:openApp};
|