@huicai/cli 2026.714.0 → 2026.721.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 +2 -1
- 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/agent.js +1 -1
- package/src/api.js +1 -1
- package/src/codeindex.js +1 -1
- package/src/commands.js +1 -1
- package/src/config.js +1 -1
- package/src/context.js +1 -1
- package/src/doccache.js +1 -0
- package/src/doctor.js +1 -1
- package/src/executors.js +1 -1
- package/src/input.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/tools.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/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:path";import{streamChat as o}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 u,renderFileDiff as c}from"./render.js";import{compactIfNeeded as m,buildModelMessages as g}from"./compaction.js";import{makeTodoTool as p,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";const b=/(现在|接下来|下面|这就|马上|我来|让我|我要|我先|我这就|咱们|让我们|稍等|待我)/,T=/(做|进行|生成|编写|写(一|个|完|出)?|制作|计算|统计|核对|汇总|整理|分析|梳理|输出|列出|准备|运行|执行|开始)/,k=/\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 x(t){const e=String(t||"").trim().split(/\n+/).map(t=>t.trim()).filter(Boolean),o=e[e.length-1]||"";return!(!o||o.length>120)&&(!!k.test(o)||b.test(o)&&T.test(o))}function $(t){const e=String(t);return e.startsWith("工具执行失败")||e.startsWith("命令启动失败")||e.includes("不是合法 JSON")||e.includes("未找到要替换的内容")||e.includes("路径超出项目目录")||e.includes("是目录而非文件")}function P(t){return"\n\n# 当前目标(用户设定)\n"+String(t).trim()+"\n在本回合结束、停止作答前,检查该目标是否已达成:若未达成,应继续推进(补充调研/改动/验证),不要过早停止;达成后用一句话说明已达成。"}function j(t){return"string"==typeof t?t:Array.isArray(t)?t.filter(t=>"text"===t?.type).map(t=>t.text||"").join("\n"):""}async function M(t,e,o){const n=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++;n[e]=await o(t[e],e)}})),n}export function isPlatformModel(t){return/^(agent|workflow|datahuman|squad)-/.test(String(t||""))}function E(t,e=200){const o=String(t).split("\n")[0];return o.length>e?o.slice(0,e)+"…":o}export function capToolResult(t){const e=String(t);if(e.length<=2e4)return e;const o=e.length-16e3-3e3;return e.slice(0,16e3)+`\n…【工具输出过长,中间已省略 ${o} 字符】…\n`+e.slice(-3e3)}export async function runTurn({config:b,messages:T,permissions:k,cwd:I,ask:U,signal:v,extraTools:A=[],depth:R=0,io:O,events:C,todoState:N,compaction:D,pullQueued:J,maxIters:q,fileJournal:F}){const K=O&&O.write||(t=>process.stdout.write(t)),W=C||null,z=t=>{W?W.onInfo?.(t):K(_.gray(" · "+t)+"\n")};let B=0,L=0,Q=0;const G=Number(q||b.maxTurns)||120,H=isPlatformModel(b.model);let V=b.model;if(0===R&&b.autoModelStrong&&!H){const t=[...T].reverse().find(t=>"user"===t.role),e=j(t?.content);(b.planMode||e.length>400||/重构|架构|设计|复杂|排查|调试|debug|优化|性能|并发|死锁|重写|迁移|安全审查|根因/i.test(e))&&b.autoModelStrong!==b.model&&(V=b.autoModelStrong,b.debug&&z(`[automodel] 复杂回合升配 → ${V}`))}const X=D||{},Y=[...A];if(0===R&&!H&&!Y.some(t=>t.spec?.function?.name===d)){const t=N||{list:[]};Y.push(p({state:t,emit:t=>{W?W.onTodo?W.onTodo(t.map(t=>({...t}))):W.onInfo?.("任务清单:\n"+f(t)):K(_.gray(u(f(t)," "))+"\n")}}))}0!==R||H||!1===b.memoryEnabled||Y.some(t=>"memory_write"===t.spec?.function?.name)||Y.push(h({cwd:I,emit:t=>{W?W.onMemory?W.onMemory(t):W.onInfo?.("已记入长期记忆:"+E(t,80)):K(_.gray(" ✎ 已记入长期记忆:"+E(t,80))+"\n")}}));const Z=new Map;for(const t of Y)Z.set(t.spec.function.name,t);const tt=H?void 0:[...s,...Y.map(t=>t.spec)];let et=0,ot=!1,nt=0,rt=null,st=0,at=0,lt=!1,it="";const ut=0!==R||H?null:y(I),ct=!(!ut||!ut.all.length);if(ct){const t=[];if(T.filter(t=>"user"===t.role).length<=1){const e=w("SessionStart",{cwd:I},ut);e.injectContext&&t.push(e.injectContext);for(const o of e.outputs)t.push(o.stdout)}const e=[...T].reverse().find(t=>"user"===t.role),o=w("UserPromptSubmit",{cwd:I,prompt:j(e?.content)},ut);if(o.block)return z("提交被 UserPromptSubmit 钩子阻断:"+o.reason),{error:"提交被钩子阻断:"+o.reason,usage:{prompt:B,completion:L},lastPromptTokens:Q};o.injectContext&&t.push(o.injectContext);for(const e of o.outputs)t.push(e.stdout);t.length&&(rt="【钩子提供的附加上下文】\n"+t.join("\n"))}for(let s=0;s<G;s++){const u=s===G-1;!u||H||ot||rt||(rt="已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。");const p=await m(T,b,{state:X,lastPromptTokens:Q});p>0&&b.debug&&z(`[debug] 已压缩上下文(${p} 条较早消息已摘要)`);let f=!1,d=!1;const h=new n,y=t=>{t&&(f||!d||W||K("\n"),f=!0,W?W.onDelta?.(t):K(t))},j=g(T,X);let U=j;if(!H&&j.length&&"system"===j[0]?.role){let t="";b.planMode&&(t+="\n\n# 计划模式(当前开启)\n你现在处于计划模式:只能用只读工具(read_file/glob/grep/list_files/fetch_url)调研,不得做任何改动——write_file/edit_file/run_bash 等写或执行操作都会被拒绝,不要反复尝试。请先充分理解需求与现状,然后产出一份清晰、分步的实施方案:涉及哪些文件、每步改动要点、如何验证、风险点,用 todo_write 列出步骤,随后停下等待用户确认。用户关闭计划模式后你再执行。"),b.sandbox&&(t+="\n\n# 沙箱模式(当前开启)\n禁止执行 shell 命令(run_bash)、联网(fetch_url)与环境/应用自动化工具(install_*/setup_environment/open_*),调用会被拒绝,不要反复尝试。只做本地文件读写与分析。"),b.goal&&String(b.goal).trim()&&(t+=P(b.goal)),t&&(U=[{...j[0],content:String(j[0].content||"")+t},...j.slice(1)])}const A=[];it?A.push({role:"assistant",content:it},{role:"user",content:"你上一条回复因达到单轮输出长度上限而被截断,用户尚未看到剩余部分。请从截断处直接继续输出剩余内容:无缝衔接上文(若截断处在句子、代码块或表格中间,从中断字符处接着写),不要重复任何已输出的内容,不要道歉或解释,不要重新开头或总结前文。"}):rt&&A.push({role:"user",content:rt});const R=Date.now();let O;try{O=await o({baseUrl:b.baseUrl,apiKey:b.apiKey,model:V,messages:A.length?[...U,...A]:U,tools:ot||u?void 0:tt,maxTokens:b.maxTokens||8192,reasoningEffort:b.reasoningEffort,client:b.client,sessionId:b.sessionId},{onContent:t=>y(h.feed(t)),onReasoning:t=>{!1!==b.showReasoning&&(it||(W?W.onReasoning?.(t):(d||K(_.gray("\n[思考] ")),K(_.gray(t))),d=!0))},onRetry:(t,e,o)=>z(`模型响应异常,${Math.round(e/1e3)}s 后自动重试(第 ${t} 次):${o}`)},v)}catch(t){return"AbortError"===t.name?(W?W.onInfo?.("已中断"):K(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:B,completion:L},lastPromptTokens:Q}):(W?W.onInfo?.("✗ 模型调用失败:"+t.message):K("\n"+_.red("✗ 模型调用失败:")+t.message+"\n"),{error:t.message,usage:{prompt:B,completion:L},lastPromptTokens:Q})}y(h.flush()),O.usage&&(B+=O.usage.prompt_tokens||0,L+=O.usage.completion_tokens||0,Q=O.usage.prompt_tokens||Q),b.debug&&z(`[debug] 轮${s+1} · ${b.model} · in ${O.usage?.prompt_tokens||0} / out ${O.usage?.completion_tokens||0} tok · ${Date.now()-R}ms · ${(O.toolCalls||[]).length} 工具调用`);const{text:C,sawMarkup:N}=r(O.content),D=N||h.sawMarkup;rt=null;const q=H?[]:O.toolCalls;if(!q.length){if(D&&!C.trim()&&!H){if(et<2){et++,2===et&&(ot=!0),rt=1===et?"你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。":"请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,不要输出任何工具调用或协议标记。",z(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${et} 次)`);continue}z("模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)")}if(!H&&"length"===O.finishReason&&C&&st<3){st++,it+=C,b.debug&&z(`[debug] 输出达单轮长度上限,自动续写(第 ${st} 段)`);continue}f&&!W&&K("\n");const t=it?it+C:C;it="",t&&T.push({role:"assistant",content:t});const e=!t.trim()||x(t);if(lt&&e&&"length"!==O.finishReason&&!ot&&at<2){at++,b.debug&&z(`[debug] 检测到「宣告未落地」收尾(空正文或预告式),注入续跑纠偏(第 ${at} 次)`),T.push({role:"user",content:'你上一条只是说明"接下来要做什么",但既没有发起工具调用、也没有给出结果——用户还没拿到任何最终产物。请立即执行你刚才所说的步骤(需要就用 run_python/工具,不需要就直接算),并直接输出面向用户的完整最终结果(例如完整的按月汇总表与逐月核对表、差异结论),不要再只描述你打算做什么。'});continue}W&&W.onTurnEnd?.(t);const o=J?.();if(o){T.push({role:"user",content:o});continue}if(ct){const e=w("Stop",{cwd:I,result:t},ut);for(const t of e.outputs)z("钩子·Stop:"+E(t.stdout,160))}return{usage:{prompt:B,completion:L},lastPromptTokens:Q}}lt=!0,f&&!W&&K("\n"),W&&C&&W.onAssistantText?.(C),T.push({role:"assistant",content:it?it+C:C,tool_calls:q}),it="";const Y=q.map((t,e)=>{const o=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=Z.get(o),i=S.isActive()&&("write_file"===o||"edit_file"===o);return{tc:t,idx:e,name:o,args:n,argsError:r,extra:l,readOnly:l?!!l.readOnly:a.has(o),deferToIde:i}}),mt=t=>{for(let e=t;e<Y.length;e++)T.push({role:"tool",tool_call_id:Y[e].tc.id||`call_${e}`,content:"(用户中断,该工具未执行)"});return W?W.onInfo?.("已中断"):K(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:B,completion:L},lastPromptTokens:Q}},gt=async t=>{if(t.argsError)return t.argsError;let e;try{e=t.extra?await t.extra.run(t.args):await l(t.name,t.args,I,b)}catch(t){e=`工具执行失败:${t.message}`}return capToolResult(String(e))},pt=t=>{W?W.onToolStart?.(t.name,t.args):K(_.cyan(" ⏺ ")+i(t.name,t.args)+"\n")},ft=(t,e)=>{$(e)&&nt++,W?W.onToolResult?.(t.name,String(e)):K(_.gray(" "+E(e))+"\n"),T.push({role:"tool",tool_call_id:t.tc.id,content:String(e)})},dt=(t,e)=>{W?W.onSkip?.(t.name,t.args):K(_.gray(` ⊘ ${i(t.name,t.args)} — 已跳过\n`)),T.push({role:"tool",tool_call_id:t.tc.id,content:e||"未授权,未执行。"})},ht=t=>"edit_file"===t||"write_file"===t,yt=e=>{try{return t.readFileSync(e,"utf8")}catch{return""}},wt=(t,o)=>{const n=e.resolve(I,String(t.args.path)),r=c(o,yt(n),{path:String(t.args.path)});r&&(W?W.onFileDiff?.(r):K(r+"\n"))};let St=0;for(;St<Y.length;){if(v?.aborted)return mt(St);const o=Y[St];if(o.argsError){pt(o),ft(o,o.argsError),St++;continue}if(o.readOnly){let t=St;for(;t<Y.length&&Y[t].readOnly&&!Y[t].argsError;)t++;const e=Y.slice(St,t),o=await M(e,8,async t=>{const e=await k.request(t.name,t.args,I,!0);return e.allow?(pt(t),{item:t,result:await gt(t)}):{item:t,denied:e.reason||"未授权,未执行。"}});for(const t of o)null!=t.denied?dt(t.item,t.denied):ft(t.item,t.result);if(St=t,v?.aborted)return mt(St);continue}const n=await k.request(o.name,o.args,I,o.readOnly||o.deferToIde);if(v?.aborted)return mt(St);if(!n.allow){dt(o,n.reason),St++;continue}if(ct){const t=w("PreToolUse",{cwd:I,toolName:o.name,args:o.args},ut);for(const e of t.outputs)z("钩子·PreToolUse:"+E(e.stdout,120));if(t.block){dt(o,"PreToolUse 钩子阻断:"+t.reason),St++;continue}t.modifiedArgs&&(o.args={...o.args,...t.modifiedArgs},z("钩子·PreToolUse 已修改工具参数"))}pt(o);const r=ht(o.name)&&o.args?.path?yt(e.resolve(I,String(o.args.path))):null;if(F&&null!=r&&o.args?.path){const n=t.existsSync(e.resolve(I,String(o.args.path)));F.push(r.length>2097152?{path:String(o.args.path),tooLarge:!0}:{path:String(o.args.path),before:r,existed:n})}let s=await gt(o);if(ct){const t=w("PostToolUse",{cwd:I,toolName:o.name,args:o.args,result:String(s)},ut);if(t.outputs.length){for(const e of t.outputs)z("钩子·PostToolUse:"+E(e.stdout,120));s=String(s)+"\n\n[PostToolUse 钩子输出]\n"+t.outputs.map(t=>t.stdout).join("\n")}}ft(o,s),null!=r&&wt(o,r),St++}nt>=3&&!ot&&!rt&&(rt="你刚才连续多次工具调用失败。请停下来重新审视:是不是文件路径/内容/前提假设错了?先用 read_file / grep / list_files 核实现状,再换一种更可靠的方法,绝不要重复同样会失败的调用。",nt=0,z("检测到连续工具失败,已提示智能体换一种更可靠的方法"));const _t=J?.();_t&&T.push({role:"user",content:_t})}return it&&(T.push({role:"assistant",content:it}),W&&W.onTurnEnd?.(it)),W?W.onInfo?.("已达最大工具轮数,停止"):K(_.yellow("\n[已达最大工具轮数,停止]\n")),{usage:{prompt:B,completion:L},lastPromptTokens:Q}}
|
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}`}}
|
|
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=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 u=`${e.baseUrl}/v1/chat/completions`,l={model:e.model,messages:e.messages,stream:!0,stream_options:{include_usage:!0}};e.tools&&e.tools.length&&(l.tools=e.tools,l.tool_choice="auto"),"number"==typeof e.temperature&&(l.temperature=e.temperature),Number.isFinite(e.maxTokens)&&e.maxTokens>0&&(l.max_tokens=Math.floor(e.maxTokens)),e.reasoningEffort&&"off"!==e.reasoningEffort&&(l.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 h=JSON.stringify(l);for(let n=0;;n++){let l;try{l=await fetch(u,{method:"POST",headers:m,body:h,signal:a})}catch(t){if("AbortError"===t?.name||a?.aborted)throw i();if(n<2){const e=c(n,null);o.onRetry?.(n+1,e,"网络错误:"+(t?.message||t)),await s(e,a);continue}throw new Error("网络错误:"+(t?.message||t))}if(!l.ok||!l.body){const e=await r(l);if(!("usage_limit_exceeded"===e.type)&&t.has(l.status)&&n<2){const t=c(n,l);o.onRetry?.(n+1,t,`HTTP ${l.status}:${e.message}`),await s(t,a);continue}throw new Error(e.message)}return f(l,o,a,e.stallTimeoutMs??9e4)}}async function f(t,e,n,o){const r=t.body.getReader(),s=new TextDecoder;let c="",f="";const u=[];let l=null,m=null,h=!1,g=null;const d=()=>{g&&clearTimeout(g),g=setTimeout(()=>{h=!0,r.cancel().catch(()=>{})},o)},y=()=>r.cancel().catch(()=>{});n?.addEventListener?.("abort",y,{once:!0});try{for(d();;){let t;try{t=await r.read()}catch(t){if(n?.aborted)throw i();if(h)throw new Error(`响应停滞超过 ${Math.round(o/1e3)}s,已中止(可重试)`);throw t}if(n?.aborted)throw i();if(h)throw new Error(`响应停滞超过 ${Math.round(o/1e3)}s,已中止(可重试)`);const{done:g,value:y}=t;if(g)break;d(),c+=s.decode(y,{stream:!0});const p=c.split("\n");c=p.pop()??"";for(const t of p){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}r.usage&&(m=r.usage);const s=r.choices?.[0];if(!s)continue;const i=s.delta||{},c=i.reasoning_content??i.reasoning;c&&e.onReasoning?.(c),i.content&&(f+=i.content,e.onContent?.(i.content)),Array.isArray(i.tool_calls)&&a(u,i.tool_calls),s.finish_reason&&(l=s.finish_reason)}}}finally{g&&clearTimeout(g),n?.removeEventListener?.("abort",y)}return{content:f,toolCalls:u.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 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!==l.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=u(await d(`${t}/v1/files/extract`,{...n(e),"content-type":i},r,a),!0);return l.set(t,!0),c}catch(e){if(!e||!e._noMultipart)throw e;l.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 u(await d(`${t}/v1/files/extract`,{...n(e)},Buffer.from(c),a),!1)}const f=await fetch(`${t}/v1/files/extract`,{method:"POST",headers:n(e),body:c,signal:AbortSignal.timeout(12e4)});if(!f.ok)throw new Error((await r(f)).message);return u(await f.text(),!1)}function u(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 l=new Map;const m=12e4,h=25,g=9e5;async function d(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))})}),f=Math.min(g,m+Math.floor(n.length/h));c.setTimeout(f,()=>c.destroy(new Error(`网络传输/服务端处理超时(已等待 ${Math.round(f/1e3)} 秒无响应),请检查网络后重试`))),c.on("error",i);let u=0;const l=()=>{for(;u<n.length;){const t=Math.min(u+262144,n.length),e=c.write(n.subarray(u,t));u=t;try{r&&r(u,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()}
|
package/src/codeindex.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:fs";import e from"node:path";import{embed as n}from"./api.js";export const IGNORE=new Set([".git","node_modules","dist","build","target","out",".next",".nuxt",".venv","venv","__pycache__",".idea",".gradle",".mvn","coverage",".cache"]);const s=new Set([".js",".mjs",".cjs",".ts",".tsx",".jsx",".java",".kt",".py",".go",".rs",".rb",".php",".c",".h",".cpp",".hpp",".cs",".swift",".scala",".sql",".vue",".svelte",".sh",".css",".scss",".html",".md",".yaml",".yml",".json",".toml"]);export const MAX_FILE_BYTES=524288;const o=new Map;export function tokenize(t){const e=String(t),n=e.replace(/([a-z0-9])([A-Z])/g,"$1 $2").toLowerCase().split(/[^a-z0-9_]+/).filter(t=>t.length>=2&&t.length<=40),s=e.match(/[一-鿿]/g);if(s)for(const t of s)n.push(t);const o=e.match(/[一-鿿]{2,}/g);if(o)for(const t of o)for(let e=0;e<t.length-1;e++)n.push(t.slice(e,e+2));return n}function*i(n,o=0,r=14){let c;try{c=t.readdirSync(n,{withFileTypes:!0})}catch{return}for(const t of c){if(IGNORE.has(t.name)||t.name.startsWith("."))continue;const c=e.join(n,t.name);t.isDirectory()?o<r&&(yield*i(c,o+1,r)):t.isFile()&&s.has(e.extname(t.name).toLowerCase())&&(yield c)}}export function buildIndex(n){const s=[],o=new Map;let r=0;t:for(const c of i(n)){let i;try{if(t.statSync(c).size>524288)continue;const e=t.readFileSync(c);if(e.includes(0))continue;i=e.toString("utf8")}catch{continue}const l=e.relative(n,c).split(e.sep).join("/"),a=i.split("\n");for(let t=0;t<a.length;t+=40){const e=Math.min(a.length,t+50),n=a.slice(t,e),i=tokenize(n.join("\n"));if(!i.length)continue;const c=new Map;for(const t of i)c.set(t,(c.get(t)||0)+1);for(const t of c.keys())o.set(t,(o.get(t)||0)+1);if(s.push({file:l,start:t+1,end:e,tf:c,len:i.length,text:n.join("\n")}),r+=i.length,s.length>=6e3)break t;if(e>=a.length)break}}return{chunks:s,df:o,N:s.length,avgLen:s.length?r/s.length:1}}export function getIndex(t){const e=o.get(t);if(e&&Date.now()-e.builtAt<3e5)return e.index;const n=buildIndex(t);return o.set(t,{index:n,builtAt:Date.now()}),n}export function invalidate(t){o.delete(t)}export function search(t,e,n=8){const s=[...new Set(tokenize(e))];if(!s.length||!t.N)return[];const{chunks:o,df:i,N:r,avgLen:c}=t,l=[];for(const t of o){let e=0;for(const n of s){const s=t.tf.get(n);if(!s)continue;const o=i.get(n)||1;e+=Math.log(1+(r-o+.5)/(o+.5))*(2.5*s/(s+1.5*(.25+t.len/c*.75)))}e>0&&l.push({file:t.file,start:t.start,end:t.end,text:t.text,score:e})}return l.sort((t,e)=>e.score-t.score),l.slice(0,n)}export function cosine(t,e){if(!t||!e)return 0;let n=0,s=0,o=0;const i=Math.min(t.length,e.length);for(let r=0;r<i;r++)n+=t[r]*e[r],s+=t[r]*t[r],o+=e[r]*e[r];return s&&o?n/(Math.sqrt(s)*Math.sqrt(o)):0}export async function searchCode(t,e,s){const o=String(e?.query||"").trim();if(!o)return"缺少 query(要检索的概念/功能描述)";const i=Number.isFinite(e?.k)&&e.k>0?Math.min(e.k,20):8,r=getIndex(t);if(!r.N)return"(当前目录未索引到代码文件)";const c=!!(s&&s.embeddingModel&&s.baseUrl&&s.apiKey);let l=search(r,o,c?Math.min(30,Math.max(3*i,15)):i);if(!l.length)return`(未找到与「${o}」明显相关的代码;可换关键词,或用 grep 精确匹配)`;let a=!1;if(c&&l.length>1)try{const t=l.map(t=>t.text.slice(0,2e3)),e=await n(s.baseUrl,s.apiKey,s.embeddingModel,[o,...t]);if(e&&e.length===t.length+1&&e[0]){const t=e[0];l=l.map((n,s)=>({...n,score:cosine(t,e[s+1])})).sort((t,e)=>e.score-t.score),a=!0}}catch{}return(a?"(向量语义重排)\n":"")+l.slice(0,i).map(t=>{const e=t.text.length>1200?t.text.slice(0,1200)+"\n…(本块过长已截断)":t.text;return`${t.file}:${t.start}-${t.end}\n${e}`}).join("\n\n─────\n\n")}function r(t){const e=new Set,n=[/(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_]\w*)/g,/\bclass\s+([A-Za-z_]\w*)/g,/\bdef\s+([A-Za-z_]\w*)/g,/\b(?:export\s+)?const\s+([A-Za-z_]\w*)\s*=\s*(?:async\s*)?(?:function|\([^)]*\)\s*=>)/g,/@(?:Get|Post|Put|Delete|Request)Mapping\(\s*"([^"]+)"/g,/\bpublic\s+[\w<>[\]]+\s+([A-Za-z_]\w*)\s*\(/g];for(const s of n){let n;for(;(n=s.exec(t))&&e.size<12;)e.add(n[1])}return[...e]}export function buildRepoMap(n,{maxFiles:s=40,maxChars:o=2500}={}){const c=[];for(const s of i(n))try{const o=t.statSync(s);if(o.size>524288)continue;c.push({abs:s,size:o.size,rel:e.relative(n,s).split(e.sep).join("/")})}catch{}if(!c.length)return"";c.sort((t,e)=>e.size-t.size);const l=[];let a=0;for(const e of c.slice(0,s)){let n=[];try{n=r(t.readFileSync(e.abs,"utf8").slice(0,4e4))}catch{}const s=`- ${e.rel}${n.length?":"+n.slice(0,8).join(", "):""}`;if(a+s.length>o)break;l.push(s),a+=s.length+1}return l.join("\n")}
|
|
1
|
+
import e from"node:fs";import t from"node:path";import n from"node:zlib";import s from"node:crypto";import{execFileSync as r}from"node:child_process";import{CONFIG_DIR as o}from"./config.js";import{embed as c}from"./api.js";export const IGNORE=new Set([".git","node_modules","dist","build","target","out",".next",".nuxt",".venv","venv","__pycache__",".idea",".gradle",".mvn","coverage",".cache","vendor","bower_components","Pods","DerivedData",".turbo",".parcel-cache",".terraform",".pytest_cache",".mypy_cache",".tox",".angular",".svelte-kit",".output",".serverless",".svn",".hg"]);const i=new Set([".js",".mjs",".cjs",".ts",".tsx",".jsx",".java",".kt",".py",".go",".rs",".rb",".php",".c",".h",".cpp",".hpp",".cs",".swift",".scala",".sql",".vue",".svelte",".sh",".css",".scss",".html",".md",".yaml",".yml",".json",".toml",".xml",".properties",".gradle",".groovy",".proto",".graphql",".gql",".tf",".ini",".cfg",".conf",".dart",".lua",".clj",".ex",".exs",".erl",".hs",".jl",".pl",".pm",".r",".dockerfile"]),a=/(?:\.min\.(?:js|css)$)|(?:\.bundle\.js$)|(?:\.map$)|(?:(?:^|\/)(?:package-lock\.json|pnpm-lock\.yaml|npm-shrinkwrap\.json)$)/i;export const MAX_FILE_BYTES=524288;const l=t.join(o,"cache","index"),u=new Map;export function tokenize(e){const t=String(e),n=t.replace(/([a-z0-9])([A-Z])/g,"$1 $2").toLowerCase().split(/[^a-z0-9_]+/).filter(e=>e.length>=2&&e.length<=40),s=t.match(/[一-鿿]/g);if(s)for(const e of s)n.push(e);const r=t.match(/[一-鿿]{2,}/g);if(r)for(const e of r)for(let t=0;t<e.length-1;t++)n.push(e.slice(t,t+2));return n}function*f(n,s=0,r=14){let o;try{o=e.readdirSync(n,{withFileTypes:!0})}catch{return}for(const e of o){if(IGNORE.has(e.name)||e.name.startsWith("."))continue;const o=t.join(n,e.name);e.isDirectory()?s<r&&(yield*f(o,s+1,r)):e.isFile()&&i.has(t.extname(e.name).toLowerCase())&&(yield o)}}let p=null;export function ignoreGlobs(){return[...IGNORE].flatMap(e=>["-g",`!**/${e}/**`,"-g",`!${e}/**`])}export function listSourceFiles(e){let n=null;if(function(){if(null!==p)return p;try{r("rg",["--version"],{stdio:"ignore"}),p=!0}catch{p=!1}return p}())try{n=r("rg",["--files",...ignoreGlobs()],{cwd:e,encoding:"utf8",maxBuffer:64<<20,stdio:["ignore","pipe","ignore"]}).split("\n").map(e=>e.replace(/\\/g,"/")).filter(Boolean)}catch{n=null}if(!n){n=[];for(const s of f(e))n.push(t.relative(e,s).split(t.sep).join("/"))}const s=[];for(const r of n)i.has(t.extname(r).toLowerCase())&&(a.test(r)||s.push({rel:r,abs:t.join(e,r)}));return s}const h=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".java",".kt",".py",".go",".rs",".rb",".php",".c",".h",".cpp",".hpp",".cs",".swift",".scala",".vue",".svelte",".dart",".lua",".clj",".ex",".exs",".erl",".hs",".jl",".groovy"]),d=/(?:^|\/)(?:tests?|__tests__|spec|specs|fixtures?|mocks?|__mocks__|generated|gen|snapshots?|migrations?|node_modules|vendor)(?:\/|$)/i;export function indexPriority(e){const n=e.toLowerCase();let s=0;return h.has(t.extname(n))?s-=100:s+=25,d.test(n)&&(s+=60),s+=2*(n.split("/").length-1),s}const m=new Set([".js",".mjs",".cjs",".ts",".tsx",".jsx",".java",".kt",".go",".rs",".c",".h",".cpp",".hpp",".cc",".cs",".swift",".scala",".php",".dart",".groovy"]);const g=/^(?:(?:export|default|public|private|protected|internal|static|final|abstract|sealed|open|override|async|suspend|inline|const|unsafe|extern|virtual|partial|pub|data|companion)\s+)*(?:function|func|fn|def|fun|class|interface|enum|record|struct|trait|impl|module|mod|namespace|protocol|extension|object|actor|type)\b(?:\s+([A-Za-z_]\w*))?/,y=/^(?:@[\w.]+(?:\([^)]*\))?\s+)*(?:(?:public|private|protected|internal|static|final|abstract|synchronized|native|override|open|suspend|inline|virtual|sealed|async|unsafe|extern)\s+)+[\w<>\[\],.?&:*\s]*?\b([A-Za-z_]\w*)\s*\([^;{]*\)[^;]*?(?:\{|$)/,x=/^(?!(?:if|for|while|switch|catch|return|do|else|try|with|function|class|new|await|yield|throw|case|default|synchronized)\b)(?:static\s+|async\s+|get\s+|set\s+|readonly\s+|public\s+|private\s+|protected\s+|\*\s*)*([A-Za-z_$][\w$]*)\s*\([^;]*\)\s*(?::\s*[\w<>\[\].|&, ]+)?\s*\{/,w=/^(?:export\s+)?(?:default\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:function\b|\([^)]*\)\s*(?::\s*[\w<>\[\].|&, ]+)?\s*=>|[A-Za-z_$][\w$]*\s*=>)/,b=/^(?:async\s+)?(?:def|class)\s+([A-Za-z_]\w*)/,z=/^(?:def|class|module)\s+([A-Za-z_][\w:]*)/;function j(e,t){if("py"===e){const e=b.exec(t);return e?{name:e[1]}:null}if("ruby"===e){const e=z.exec(t);return e?{name:e[1]}:null}let n=g.exec(t);return n?{name:n[1]||""}:(n=y.exec(t),n?{name:n[1]}:(n=x.exec(t),n?{name:n[1]}:(n=w.exec(t),n?{name:n[1]}:null)))}function v(e,t=0){const n=[];for(let s=0;s<e.length;s+=40){const r=Math.min(e.length,s+50),o=e.slice(s,r).join("\n"),c=tokenize(o);if(c.length){const e=new Map;for(const t of c)e.set(t,(e.get(t)||0)+1);n.push({start:t+s+1,end:t+r,len:c.length,text:o,tf:[...e]})}if(r>=e.length)break}return n}function k(e,t){const n=function(e){return m.has(e)?"brace":".py"===e?"py":".rb"===e?"ruby":null}(e);if(!n)return null;const s=[];for(let e=0;e<t.length;e++){const r=t[e].trim();if(!r)continue;const o=j(n,r);o&&s.push({line:e,sym:o.name||""})}if(!s.length)return null;for(let e=0;e<s.length;e++){let n=s[e].line;const r=e>0?s[e-1].line+1:0;for(;n>r&&/^@[\w.]/.test(t[n-1].trim());)n--;s[e].start=n}const r=[];s[0].start>0&&r.push({start:0,sym:""});for(const e of s)r.push({start:e.start,sym:e.sym});const o=[];for(let e=0;e<r.length;e++){const n=r[e].start,s=e+1<r.length?r[e+1].start:t.length;if(s<=n)continue;const c=t.slice(n,s);if(c.length>160){const t=v(c,n);t.length&&r[e].sym&&(t[0].sym=r[e].sym),o.push(...t);continue}const i=c.join("\n"),a=tokenize(i);if(!a.length)continue;const l=new Map;for(const e of a)l.set(e,(l.get(e)||0)+1);const u={start:n+1,end:s,len:a.length,text:i,tf:[...l]};r[e].sym&&(u.sym=r[e].sym),o.push(u)}return o.length?o:null}export function chunkText(e,n=""){const s=e.split("\n"),r=n?t.extname(n).toLowerCase():"";return r&&k(r,s)||v(s,0)}export function buildIndexCore(t,n){const s=Date.now()+2500,r=listSourceFiles(t).map(t=>{let n=1/0,s=0;try{const r=e.statSync(t.abs);n=r.size,s=r.mtimeMs}catch{}return{...t,size:n,mtimeMs:s,prio:indexPriority(t.rel)}}).filter(e=>e.size<=524288).sort((e,t)=>e.prio-t.prio||e.size-t.size),o=new Set(r.map(e=>e.rel)),c=[],i=new Map,a={};let l=0,u=0,f=0,p=0,h=!1;for(const t of r){if(c.length>=12e3||Date.now()>s){h=!0;break}const r=n&&n[t.rel];let o;if(r&&r.mtimeMs===t.mtimeMs&&r.size===t.size&&Array.isArray(r.chunks))o=r.chunks,f++;else{let n;try{const s=e.readFileSync(t.abs);if(s.includes(0))continue;n=s.toString("utf8")}catch{continue}o=chunkText(n,t.rel),p++}u++,a[t.rel]={mtimeMs:t.mtimeMs,size:t.size,chunks:o};const d=new Set(tokenize(t.rel));for(const e of o){const n=new Map(e.tf);for(const[t]of e.tf)i.set(t,(i.get(t)||0)+1);c.push({file:t.rel,start:e.start,end:e.end,len:e.len,text:e.text,tf:n,pathTokens:d,sym:e.sym}),l+=e.len}}if(h&&n)for(const e in n)!a[e]&&o.has(e)&&(a[e]=n[e]);const d=n?Object.keys(n).length:-1,m=p>0||Object.keys(a).length!==d;return{index:{chunks:c,df:i,N:c.length,avgLen:c.length?l/c.length:1,truncated:h,indexedFiles:u,totalFiles:r.length,reused:f,rechunked:p},filesMap:a,changed:m}}export function buildIndex(e){return buildIndexCore(e,null).index}function _(e){const n=s.createHash("sha1").update(t.resolve(e)).digest("hex").slice(0,16);return t.join(l,n+".json.gz")}function S(s,r){try{e.mkdirSync(l,{recursive:!0});const o=_(s),c=n.gzipSync(JSON.stringify({v:2,cwd:t.resolve(s),at:Date.now(),files:r}));e.writeFileSync(o+".tmp",c),e.renameSync(o+".tmp",o),function(){try{const n=e.readdirSync(l).filter(e=>e.endsWith(".json.gz"));if(n.length<=24)return;const s=n.map(n=>{try{return{f:n,t:e.statSync(t.join(l,n)).mtimeMs}}catch{return null}}).filter(Boolean).sort((e,t)=>e.t-t.t);for(const{f:n}of s.slice(0,s.length-24))try{e.unlinkSync(t.join(l,n))}catch{}}catch{}}()}catch{}}export function getIndex(s){const r=u.get(s);if(r&&Date.now()-r.builtAt<3e5)return r.index;const{index:o,filesMap:c,changed:i}=buildIndexCore(s,function(s){try{const r=_(s),o=e.statSync(r);if(Date.now()-o.mtimeMs>12096e5)return null;const c=JSON.parse(n.gunzipSync(e.readFileSync(r)).toString("utf8"));if(!c||2!==c.v||c.cwd!==t.resolve(s)||!c.files)return null;const i=new Date;try{e.utimesSync(r,i,i)}catch{}return c.files}catch{return null}}(s));return i&&S(s,c),u.set(s,{index:o,builtAt:Date.now()}),o}export function invalidate(e){u.delete(e)}export function clearIndexCache(){u.clear();try{e.rmSync(l,{recursive:!0,force:!0})}catch{}}export function search(e,t,n=8){const s=[...new Set(tokenize(t))];if(!s.length||!e.N)return[];const{chunks:r,df:o,N:c,avgLen:i}=e,a=new Map;for(const e of s){const t=o.get(e)||1;a.set(e,Math.log(1+(c-t+.5)/(t+.5)))}const l=[];for(const e of r){let t=0;for(const n of s){const s=e.tf.get(n);s&&(t+=a.get(n)*(2.5*s/(s+1.5*(.25+e.len/i*.75))))}if(t>0){if(e.pathTokens)for(const n of s)e.pathTokens.has(n)&&(t+=.6*a.get(n));l.push({file:e.file,start:e.start,end:e.end,text:e.text,score:t,sym:e.sym})}}return l.sort((e,t)=>t.score-e.score),l.slice(0,n)}export function cosine(e,t){if(!e||!t)return 0;let n=0,s=0,r=0;const o=Math.min(e.length,t.length);for(let c=0;c<o;c++)n+=e[c]*t[c],s+=e[c]*e[c],r+=t[c]*t[c];return s&&r?n/(Math.sqrt(s)*Math.sqrt(r)):0}export async function searchCode(e,t,n){const s=String(t?.query||"").trim();if(!s)return"缺少 query(要检索的概念/功能描述)";const r=Number.isFinite(t?.k)&&t.k>0?Math.min(t.k,20):8,o=getIndex(e);if(!o.N)return"(当前目录未索引到代码文件)";const i=!!(n&&n.embeddingModel&&n.baseUrl&&n.apiKey);let a=search(o,s,i?Math.min(30,Math.max(3*r,15)):r);if(!a.length)return`(未找到与「${s}」明显相关的代码;可换关键词,或用 grep 精确匹配)`;let l=!1;if(i&&a.length>1)try{const e=a.map(e=>e.text.slice(0,2e3)),t=await c(n.baseUrl,n.apiKey,n.embeddingModel,[s,...e]);if(t&&t.length===e.length+1&&t[0]){const e=t[0];a=a.map((n,s)=>({...n,score:cosine(e,t[s+1])})).sort((e,t)=>t.score-e.score),l=!0}}catch{}let u=l?"(向量语义重排)\n":"";return o.truncated&&(u+=`(大型仓库:已索引最相关的约 ${o.indexedFiles}/${o.totalFiles} 个文件;如需穷尽匹配请配合 grep)\n`),u+a.slice(0,r).map(e=>{const t=e.text.length>1200?e.text.slice(0,1200)+"\n…(本块过长已截断)":e.text;return`${`${e.file}:${e.start}-${e.end}${e.sym?` ▸ ${e.sym}`:""}`}\n${t}`}).join("\n\n─────\n\n")}function M(e){const t=new Set,n=[/(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_]\w*)/g,/\bclass\s+([A-Za-z_]\w*)/g,/\bdef\s+([A-Za-z_]\w*)/g,/\b(?:export\s+)?const\s+([A-Za-z_]\w*)\s*=\s*(?:async\s*)?(?:function|\([^)]*\)\s*=>)/g,/@(?:Get|Post|Put|Delete|Request)Mapping\(\s*"([^"]+)"/g,/\bpublic\s+[\w<>[\]]+\s+([A-Za-z_]\w*)\s*\(/g,/\b(?:export\s+)?(?:interface|type|enum)\s+([A-Za-z_]\w*)/g,/\b(?:func|fn|fun)\s+([A-Za-z_]\w*)/g,/\b(?:struct|trait|impl)\s+([A-Za-z_]\w*)/g];for(const s of n){let n;for(;(n=s.exec(e))&&t.size<12;)t.add(n[1])}return[...t]}export function buildRepoMap(t,{maxFiles:n=40,maxChars:s=2500}={}){const r=[];for(const n of listSourceFiles(t))try{const t=e.statSync(n.abs);if(t.size>524288)continue;r.push({abs:n.abs,size:t.size,rel:n.rel,prio:indexPriority(n.rel)})}catch{}if(!r.length)return"";r.sort((e,t)=>e.prio-t.prio||t.size-e.size);const o=[];let c=0;for(const t of r.slice(0,n)){let n=[];try{n=M(e.readFileSync(t.abs,"utf8").slice(0,4e4))}catch{}const r=`- ${t.rel}${n.length?":"+n.slice(0,8).join(", "):""}`;if(c+r.length>s)break;o.push(r),c+=r.length+1}return o.join("\n")}
|