svharness 0.14.9 → 0.14.12
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 +126 -20
- package/bin/launch-codechat-cli.js +18 -0
- package/dist/commands/convert.js +129 -22
- package/dist/commands/doctor/check-requirements-coverage.js +82 -59
- package/dist/commands/doctor/check-requirements-fidelity.js +2 -2
- package/dist/commands/doctor/check-source-inventory.js +133 -0
- package/dist/commands/doctor/check-state-phases.js +33 -1
- package/dist/commands/doctor/index.js +33 -19
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/init.js +24 -1
- package/dist/commands/requirements.js +96 -0
- package/dist/commands/shell-integration.js +150 -0
- package/dist/commands/start-agent.js +38 -0
- package/dist/config/merge-options.js +3 -0
- package/dist/core/cli-input-declarations.js +187 -0
- package/dist/core/markitdown-client.js +80 -33
- package/dist/core/next-steps.js +19 -0
- package/dist/core/scaffold.js +30 -0
- package/dist/core/state.js +9 -1
- package/dist/index.js +122 -5
- package/dist/lib/agent-launcher.js +220 -0
- package/dist/lib/requirements/parse-blocks.js +137 -0
- package/dist/lib/requirements/paths.js +71 -0
- package/dist/lib/requirements/structure-scan.js +154 -0
- package/dist/lib/requirements/types.js +7 -0
- package/dist/lib/requirements/verify.js +341 -0
- package/dist/lib/win-registry.js +52 -0
- package/dist/utils/validate-args.js +32 -10
- package/docs/agent-launcher-design.md +296 -0
- package/package.json +11 -2
- package/scripts/postinstall.js +28 -0
- package/scripts/preuninstall.js +17 -0
- package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +19 -16
- package/templates/_shared/build-rules/harness-build-rule-requirements-extraction.md +7 -26
- package/templates/_shared/build-rules/harness-build-rule-user-interaction.md +38 -4
- package/templates/_shared/build-skills/harness-build-skill-agent-env-merge.md +6 -0
- package/templates/_shared/build-skills/harness-build-skill-knowledge-builder.md +5 -4
- package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +22 -15
- package/templates/_shared/build-skills/harness-build-skill-references-intake.md +4 -0
- package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +28 -38
- package/templates/_shared/meta/task_list.md.ejs +4 -0
- package/templates/_shared/skeleton/references/md/README.md +5 -0
- package/templates/_shared/skeleton/references/raw/README.md +5 -0
- package/templates/_shared/skeleton/references/yaml/README.md +5 -0
- package/templates/_shared/skeleton/requirements/_work/.gitkeep +0 -0
- package/templates/_shared/skeleton/requirements/_work/source-inventory.schema.json +89 -0
- package/templates/_shared/skeleton/requirements/coverage-report.schema.json +36 -0
- package/templates/_shared/skeleton/requirements/yaml/schema.json +15 -0
- package/templates/_shared/skeleton/tasks/base_tasks/README.md +22 -0
- package/templates/_shared/skeleton/tasks/base_tasks/_pack-template/00-AGENT-ENTRY.md +52 -0
- package/templates/_shared/skeleton/tasks/base_tasks/_pack-template/EXECUTION-STATUS.yaml +56 -0
- package/templates/_shared/skeleton/tasks/base_tasks/_pack-template/VERIFY-CASES.yaml +19 -0
- package/templates/_shared/skeleton/tasks/base_tasks/_pack-template/evidence/README.md +5 -0
- package/templates/_shared/skeleton/tasks/base_tasks/index.yaml +6 -0
- package/templates/_shared/tasks/templates/README.md +3 -1
- package/templates/_shared/tasks/templates/TEMPLATE-GUIDE.md +3 -3
- package/templates/default-claude.env +60 -0
- package/templates/svharness.config.example.yaml +2 -0
package/README.md
CHANGED
|
@@ -40,6 +40,10 @@ svharness build --harness-name my-app --arch android-compose --agent qoder --yes
|
|
|
40
40
|
|
|
41
41
|
# 把构建完成的 harness 应用到其它项目(绑定式,不拷贝)
|
|
42
42
|
svharness apply --harness ./my-app-harness --target ../other-project --yes
|
|
43
|
+
|
|
44
|
+
# build 完成后启动 CodeChat Agent 填充 harness(当前目录)
|
|
45
|
+
svharness start-agent --work-dir .
|
|
46
|
+
# 或全局快捷命令: launch_codechat_cli
|
|
43
47
|
```
|
|
44
48
|
|
|
45
49
|
### 配置文件与向导(v0.10+)
|
|
@@ -179,7 +183,7 @@ harness-build-{skill|rule}-<semantic-name>
|
|
|
179
183
|
|
|
180
184
|
### 构建阶段
|
|
181
185
|
|
|
182
|
-
`build`
|
|
186
|
+
`build` 生成目录骨架,并在 **S00** 预置默认 `tasks/templates/`;其余各阶段的**项目相关内容**由 Agent 借助注入的 skill 逐步填充:
|
|
183
187
|
|
|
184
188
|
| 阶段 | 动作 | 产物 |
|
|
185
189
|
|------|------|------|
|
|
@@ -191,17 +195,15 @@ harness-build-{skill|rule}-<semantic-name>
|
|
|
191
195
|
| **S60_process_references** | references 处理(`svharness convert` + 结构化索引 + 用户确认落地) | `references/md/` |
|
|
192
196
|
| **S61_confirm_baseline_extraction** | 确认是否自动从 baseline 提取 skills/rules(表单确认) | `.harness-build-state.yaml`(`phases.S61_confirm_baseline_extraction.baseline_auto_extract`) |
|
|
193
197
|
| **S65_customize_agent_env** | agent-env 定制(extra-skills/extra-rules 冲突建议与重命名建议 → 用户确认 → 写入) | `agent-env/rules/` + `agent-env/skills/` |
|
|
194
|
-
| **S70_runtime_assets** | 运行期 Skills
|
|
198
|
+
| **S70_runtime_assets** | 运行期 Skills 定制 + tasks 模板按项目裁剪/增补(build 已预置默认模板) | `agent-env/skills/` + `tasks/templates/` |
|
|
195
199
|
| **S80_seed_memory** | Memory 初始化 | `agent-env/memory/categories/` |
|
|
196
200
|
| **S85_pre_seal_validation** | 封存前校验 | `svharness doctor` + 全面审查报告 |
|
|
197
201
|
| **S90_finalize** | 封板 | 版本 bump + CHANGELOG + `bootstrap_mode: false` |
|
|
198
202
|
|
|
199
203
|
> 反复调用 orchestrator 总是从"第一个非 DONE 的阶段"继续。
|
|
200
204
|
> **S10_wiki 仅在有 baseline 时出现;无 baseline 则不构建 wiki,流程从 S20_collect_inputs 开始。**
|
|
201
|
-
> **S20_collect_inputs
|
|
202
|
-
> **
|
|
203
|
-
> **S61_confirm_baseline_extraction 必须显式确认是否启用 baseline 自动提取 skills/rules;未经确认不得在 S65 自动提取。**
|
|
204
|
-
> **S65_customize_agent_env 专注 extra-skills/extra-rules:来源可来自 `--extra-skills` 导入后的 `_incoming/manifest.yaml`,先冲突识别与重命名建议,再表单确认写入;推荐命名:`harness-apply-skills-<topic>` 与 `harness-apply-rules-<topic>.md/.mdc`。**
|
|
205
|
+
> **S20_collect_inputs**:`requirements/raw/` 须有真实文档。未传 `--references` / `--extra-skills` 时 CLI 预声明 `input_declarations` 并可能预 DONE S20(当 `--requirements` 已注入);Agent 不得再对已为 `none` 的可选项弹表单。`--strict-input-confirm` 可恢复旧行为。
|
|
206
|
+
> **S60/S61/S65/S80**:未提供对应可选输入时,build 可预标记 `source: cli-default`、`outcome: none` 并 DONE;增量添加时说「reopen S60」等。S61 默认 `baseline_auto_extract: DISABLED`;需提取时传 `--enable-baseline-extract`。
|
|
205
207
|
> **S40/S50/S85 的 DONE 都带覆盖率门禁:未闭环 gap(或未备案 waiver)不得推进。**
|
|
206
208
|
|
|
207
209
|
---
|
|
@@ -276,13 +278,15 @@ svharness build \
|
|
|
276
278
|
| `--baseline-branch <name>` | 可选 | git 基线的分支名(仅 git 模式有效) | `main` |
|
|
277
279
|
| `--baseline-max-file-kb <kb>` | 可选 | 基线拷贝单文件大小上限(KB) | `1024` |
|
|
278
280
|
| `--repomix` | 可选 flag | 将 `baseline/code` 打成**单文件** Repomix XML;**须同时提供 `--baseline`**。适用场景见下文 [Repomix](#repomix-repomix) | `false` |
|
|
279
|
-
| `--convert-endpoint <url>` | 可选 | build 自动 convert 使用的 markitdown
|
|
281
|
+
| `--convert-endpoint <url>` | 可选 | build 自动 convert 使用的 markitdown 服务基址(支持逗号分隔 failover);省略时读 `SVHARNESS_MARKITDOWN_ENDPOINT` | `http://markitdown.desaysz.site` |
|
|
280
282
|
| `--convert-concurrency <n>` | 可选 | build 自动 convert 并发上传数 | `3` |
|
|
281
283
|
| `--convert-max-file-mb <n>` | 可选 | build 自动 convert 单文件大小上限(MB) | `50` |
|
|
282
284
|
| `--convert-timeout-sec <n>` | 可选 | build 自动 convert 请求超时秒数 | `120` |
|
|
283
285
|
| `--convert-force` | 可选 flag | build 自动 convert 覆盖同名 `.md`(否则按 `-1/-2` 追加) | `false` |
|
|
284
286
|
| `--force` | 可选 flag | 覆写已存在的 harness 目录;同时允许覆盖已注入的 build skills/rules 与项目根 `AGENTS.md` / `CLAUDE.md`(默认遇到已存在文件会跳过) | `false` |
|
|
285
287
|
| `-y, --yes` | 可选 flag | 跳过所有提示,采用默认值 | `false` |
|
|
288
|
+
| `--enable-baseline-extract` | 可选 flag | 启用 baseline 自动提取 skills/rules(S61 ENABLED) | `false` |
|
|
289
|
+
| `--strict-input-confirm` | 可选 flag | 禁用 CLI 预声明,恢复 S60/S61/S65 等 Agent 表单确认 | `false` |
|
|
286
290
|
| `--verbose` | 可选 flag | 打印每个生成文件 | `false` |
|
|
287
291
|
|
|
288
292
|
#### Wiki 参数
|
|
@@ -446,7 +450,7 @@ svharness doctor --harness ./my-app-harness --mode pre-seal --format json --repo
|
|
|
446
450
|
| 选项 | 说明 |
|
|
447
451
|
|------|------|
|
|
448
452
|
| `--harness` | harness 根目录(必填;可写在 `svharness.config.yaml` 的 `doctor.harness`) |
|
|
449
|
-
| `--mode` | `pre-seal
|
|
453
|
+
| `--mode` | `pre-seal`(默认)、`health` 或 **`requirements`**(S40 专用) |
|
|
450
454
|
| `--format` | `text`(默认)或 `json` |
|
|
451
455
|
| `--report` | JSON 报告路径(默认 `<harness>/doctor-report.json`) |
|
|
452
456
|
| `--strict` | 将 warning 视为 error |
|
|
@@ -459,15 +463,34 @@ svharness doctor --harness ./my-app-harness --mode pre-seal --format json --repo
|
|
|
459
463
|
- **Part B**:按 `specs.layers` 与 `agent-env/review-profiles/<arch>.yaml` 做深度评分
|
|
460
464
|
- **门禁**:`overall_score >= 5.0`(C 级)、`critical_count == 0`、报告 frontmatter `gate_pass: true`、用户表单确认
|
|
461
465
|
|
|
462
|
-
`doctor` 另含 `check-review-report`(warning):S85 已 DONE 时校验报告 frontmatter 与 state 中 `review_gate_pass`
|
|
466
|
+
`doctor` 另含 `check-review-report`(warning):S85 已 DONE 时校验报告 frontmatter 与 state 中 `review_gate_pass` 一致。**S40 数量/结构覆盖率**由 `svharness requirements verify` + `doctor --mode requirements` 负责(非表单门禁)。
|
|
463
467
|
同时新增:
|
|
464
468
|
|
|
465
469
|
- `check-requirements-fidelity`:检查 `source_excerpt` 缺失、`description` 缩略与占位语义。
|
|
466
470
|
- `check-specs-depth`:检查 index-only 信号、enum 缺失 `enum_values`、behavior 空 `guard/action`。
|
|
467
471
|
|
|
468
|
-
|
|
472
|
+
- `check-structure-coverage` / `check-source-inventory`:结构块与 Locator 摘录校验(`requirements` 模式)。
|
|
473
|
+
|
|
474
|
+
### `requirements` —— S40 条目化结构扫描与 verify 门禁
|
|
475
|
+
|
|
476
|
+
`structure-scan` 在 **`svharness convert`(requirements)与 `svharness build` 注入需求后自动执行**;也可手动增量重扫。
|
|
469
477
|
|
|
470
|
-
|
|
478
|
+
```bash
|
|
479
|
+
# 增量重扫(docx→md 与 raw/*.md 双路径)
|
|
480
|
+
svharness requirements structure-scan --harness ./my-app-harness
|
|
481
|
+
|
|
482
|
+
# S40 机器门禁(生成 coverage-report + verify-gaps.json)
|
|
483
|
+
svharness requirements verify --harness ./my-app-harness --strict
|
|
484
|
+
|
|
485
|
+
# S40 专用 doctor
|
|
486
|
+
svharness doctor --harness ./my-app-harness --mode requirements --strict
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
`svharness convert ... --no-structure-scan` 可跳过 convert 后的自动 structure-scan。
|
|
490
|
+
|
|
491
|
+
### `convert` —— 文档 → Markdown 预处理(对接 S30_convert_docs)
|
|
492
|
+
|
|
493
|
+
把本地原始需求文档(`.pdf / .docx / .pptx / .xlsx / .html / .epub / .txt / .csv / .json / ...`)通过**云端部署**的 `markitdown_serve`(FastAPI + Microsoft MarkItDown + **多引擎 fallback chain**)批量转为 Markdown,产物统一落到 `<harness>/<type>/md/`(`type` 为 `requirements` 或 `references`),直接喂给下游 `S40_extract_requirements` 条目化流程。
|
|
471
494
|
|
|
472
495
|
> **表格清洗(xlsx/xls/csv)**:源文件为 `.xlsx`、`.xls` 或 `.csv` 时,转换完成后自动清理 Markdown 表格中的 `NaN`、全空行、以及数据全空且表头为 `Unnamed: N` 的列(CLI 与服务端双端生效)。对已存在的产物 md 可执行:`node scripts/cleanup-spreadsheet-md.js <dirOrFile>`。
|
|
473
496
|
|
|
@@ -475,6 +498,11 @@ svharness doctor --harness ./my-app-harness --mode pre-seal --format json --repo
|
|
|
475
498
|
|
|
476
499
|
> **架构约束**:CLI 只是 HTTP 客户端,**不 spawn / 不装 Python / 不管进程**。服务端代码集中在 `svharnessbuild/markitdown_serve/` 便于仓内维护,但**不随 npm 包分发**,部署方式详见 [markitdown_serve/README.md](./markitdown_serve/README.md)。
|
|
477
500
|
|
|
501
|
+
> **高可靠转换(2026-06)**:
|
|
502
|
+
> - **服务端 fallback chain**:MarkItDown 失败或输出过短时,按扩展名尝试后备(PDF:pymupdf → pypdf → tesseract OCR;xlsx:openpyxl → pandas)。详见 [markitdown_serve/README.md §详细设计](./markitdown_serve/README.md#详细设计) 与 [详细设计 §4.10](../tmp/svharnessbuild-detail-design.md#410-文档转换convert-命令--markitdown_serve-远端服务)。
|
|
503
|
+
> - **CLI endpoint failover**:`--endpoint` / `SVHARNESS_MARKITDOWN_ENDPOINT` 支持逗号分隔多 URL。
|
|
504
|
+
> - **失败追溯**:有失败时写入 `<outputDir>/convert-failures.json`(含 `trace_id`)。
|
|
505
|
+
|
|
478
506
|
```bash
|
|
479
507
|
# 最简 —— 所有参数均可省略,默认扫描当前目录
|
|
480
508
|
svharness convert
|
|
@@ -485,8 +513,9 @@ svharness convert --input ./docs/*.pdf --output ./docs/md --yes
|
|
|
485
513
|
# harness 模式:md 写入 ./my-app-harness/requirements/md/
|
|
486
514
|
svharness convert --input ./my-app-harness/requirements/raw --output ./my-app-harness --yes
|
|
487
515
|
|
|
488
|
-
#
|
|
489
|
-
|
|
516
|
+
# 多节点 failover + 详细引擎日志
|
|
517
|
+
export SVHARNESS_MARKITDOWN_ENDPOINT=https://md-a.example.com,https://md-b.example.com
|
|
518
|
+
svharness convert --verbose --yes
|
|
490
519
|
```
|
|
491
520
|
|
|
492
521
|
#### 全部参数
|
|
@@ -496,7 +525,7 @@ svharness convert --verbose
|
|
|
496
525
|
| `--input <path...>` | 一个或多个源文件 / 目录 / glob,支持 `./a/*.pdf`、`./docs/**/*.docx`、`./{a,b}/*.md` | `.`(当前目录) |
|
|
497
526
|
| `--harness <path>` | harness 根目录(需 `harness.yaml`);单独使用时写入 `<harness>/<type>/md/`;与 `--output` 同用时 `--output` 为最终 md 目录 | — |
|
|
498
527
|
| `--output <path>` | **独立模式**:最终 md 输出目录;**harness 模式**:`path` 含 `harness.yaml` 时写入 `<path>/<type>/md/` | `.`(当前目录) |
|
|
499
|
-
| `--endpoint <url>` | 云端 `markitdown_serve`
|
|
528
|
+
| `--endpoint <url>` | 云端 `markitdown_serve` 基址;**逗号分隔**可配置 failover;省略时读 `SVHARNESS_MARKITDOWN_ENDPOINT` | `http://markitdown.desaysz.site` |
|
|
500
529
|
| `--concurrency <n>` | 并发上传数 | `3` |
|
|
501
530
|
| `--max-file-mb <n>` | 单文件大小上限(MB),本地 + 服务端双重限流 | `50` |
|
|
502
531
|
| `--timeout-sec <n>` | 单请求超时秒数 | `120`(2 分钟) |
|
|
@@ -505,12 +534,13 @@ svharness convert --verbose
|
|
|
505
534
|
| `--split-sheets-suffix <suffix>` | xlsx/xls 按 sheet 拆分的子目录后缀 | `_split` |
|
|
506
535
|
| `--no-split-sheets` | flag,不将 xlsx/xls 合并 md 按 `##` 拆分为多文件 | `false`(默认拆分) |
|
|
507
536
|
| `-y, --yes` | flag,跳过交互确认 | `false` |
|
|
508
|
-
| `--verbose` | flag
|
|
537
|
+
| `--verbose` | flag,显示详细日志(含每文件 `converter` / `fallback_used` / endpoint) | `false` |
|
|
509
538
|
|
|
510
539
|
#### 行为要点
|
|
511
540
|
|
|
512
|
-
- **健康探针前置**:上传前先 `GET /healthz
|
|
513
|
-
-
|
|
541
|
+
- **健康探针前置**:上传前先 `GET /healthz`(多 endpoint 时过滤不健康节点),不通直接退出。
|
|
542
|
+
- **单文件失败不中断**:失败计入汇总;有失败时写入 `convert-failures.json` 并 `exit 1`。
|
|
543
|
+
- **Endpoint failover**:5xx / 网络错误在 comma-separated 列表中切换下一节点;415 等 4xx 不切换。
|
|
514
544
|
- **本地白名单预过滤**:客户端与服务端的扩展名白名单保持一致,非白名单文件本地直接跳过,避免无意义往返。
|
|
515
545
|
- **两种输出模式**:
|
|
516
546
|
- **独立**:`--output` 即最终 md 目录(路径下无 `harness.yaml`),例如 `--output ./docs/md`。
|
|
@@ -530,16 +560,86 @@ svharness convert --verbose
|
|
|
530
560
|
└── md/
|
|
531
561
|
├── 参考文档.md
|
|
532
562
|
└── ...
|
|
563
|
+
|
|
564
|
+
# 有失败时额外产出(与 md 同目录):
|
|
565
|
+
convert-failures.json
|
|
533
566
|
```
|
|
534
567
|
|
|
535
568
|
#### 接口契约(CLI ⇄ markitdown_serve)
|
|
536
569
|
|
|
537
570
|
| 端点 | 方法 | 约定 |
|
|
538
571
|
|---|---|---|
|
|
539
|
-
| `/healthz` | GET | 200 `{status, markitdown_version}` |
|
|
540
|
-
| `/convert` | POST | multipart/form-data,字段 `file`;200 返回 `{markdown, source_name, mime}`;413/415/500
|
|
572
|
+
| `/healthz` | GET | 200 `{status, markitdown_version, converters?}` |
|
|
573
|
+
| `/convert` | POST | multipart/form-data,字段 `file`;200 返回 `{markdown, source_name, mime, converter?, fallback_used?}`;413/415/500 带 `error` + `trace_id` |
|
|
574
|
+
|
|
575
|
+
完整契约、fallback chain、Docker 部署见 [markitdown_serve/README.md](./markitdown_serve/README.md);模块设计见 [tmp/svharnessbuild-detail-design.md §4.10](../tmp/svharnessbuild-detail-design.md#410-文档转换convert-命令--markitdown_serve-远端服务)。
|
|
576
|
+
|
|
577
|
+
### `start-agent` —— 启动 Agent 填充 harness(v0.16+)
|
|
578
|
+
|
|
579
|
+
`build` 生成骨架后,用本命令在**项目根(workdir)**启动 CodeChat CLI,驱动 S10–S90 构建阶段;`apply` 之后亦可在目标项目根启动 Agent 做业务开发。
|
|
580
|
+
|
|
581
|
+
详细设计见 [`docs/agent-launcher-design.md`](docs/agent-launcher-design.md)。
|
|
582
|
+
|
|
583
|
+
**env 策略(单向,只写用户目录)**:可**读取**项目 `<workdir>/.claude/.env` 并同步到 `~/.claude/.env`;**绝不**在项目内创建或修改 `.claude/.env`。无 env 时内置模板**仅**生成 `~/.claude/.env`。
|
|
584
|
+
|
|
585
|
+
#### 快速示例
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
# 当前目录
|
|
589
|
+
svharness start-agent
|
|
590
|
+
|
|
591
|
+
# 显式 workdir
|
|
592
|
+
svharness start-agent --work-dir /path/to/project
|
|
593
|
+
svharness start-agent codechat ./my-app
|
|
594
|
+
svharness start-agent /path/to/project
|
|
541
595
|
|
|
542
|
-
|
|
596
|
+
# 不同步项目 env、保留 CodeChat 权限确认
|
|
597
|
+
svharness start-agent --work-dir . --no-sync-env --no-skip-permissions
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
#### 全部参数
|
|
601
|
+
|
|
602
|
+
| 参数 | 说明 | 默认 |
|
|
603
|
+
|------|------|------|
|
|
604
|
+
| `[agentOrWorkdir]` | 已知 agent 名 **或** workdir 路径 | — |
|
|
605
|
+
| `[workdir]` | 工作目录(前一参数为 agent 时使用) | — |
|
|
606
|
+
| `--work-dir <path>` | Agent 工作目录(harness 项目根) | 当前目录 |
|
|
607
|
+
| `--agent <agent>` | 目标 Agent(**当前仅 codechat 可实际启动**) | `codechat` |
|
|
608
|
+
| `--no-sync-env` | 不读取项目 `.claude/.env` 同步到用户级(仍可用已有 `~/.claude/.env`;若无则仅写入用户级内置模板) | false |
|
|
609
|
+
| `--no-skip-permissions` | 不传 `--dangerously-skip-permissions` | 默认传 |
|
|
610
|
+
|
|
611
|
+
#### 平台说明
|
|
612
|
+
|
|
613
|
+
| 平台 | CodeChat runner |
|
|
614
|
+
|------|-----------------|
|
|
615
|
+
| Windows | `~/.codechat/cli_app/run.bat` |
|
|
616
|
+
| Linux | `~/.codechat/cli_app/run.sh` |
|
|
617
|
+
|
|
618
|
+
### `launch_codechat_cli` —— 全局快捷命令(v0.16+)
|
|
619
|
+
|
|
620
|
+
`npm install -g svharness` 后可用,等价于 `svharness start-agent codechat`(不支持额外 flag)。
|
|
621
|
+
|
|
622
|
+
```bash
|
|
623
|
+
launch_codechat_cli
|
|
624
|
+
launch_codechat_cli D:\projects\my-app # Windows
|
|
625
|
+
launch_codechat_cli ~/projects/my-app # Linux
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### `shell` —— Windows 右键菜单(v0.16+,仅 win32)
|
|
629
|
+
|
|
630
|
+
在资源管理器**文件夹空白处**与**文件夹图标**右键增加「在此启动 CodeChat Agent」,点击后打开 **PowerShell** 窗口并启动 Agent。
|
|
631
|
+
|
|
632
|
+
```bash
|
|
633
|
+
svharness shell install # 注册(幂等)
|
|
634
|
+
svharness shell uninstall # 移除
|
|
635
|
+
svharness shell status # 查看 stub / 注册表状态
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
`npm install -g svharness` 在 Windows 上 **默认自动** `shell install`。跳过:
|
|
639
|
+
|
|
640
|
+
```bash
|
|
641
|
+
SVHARNESS_SKIP_SHELL=1 npm install -g svharness
|
|
642
|
+
```
|
|
543
643
|
|
|
544
644
|
---
|
|
545
645
|
|
|
@@ -666,10 +766,16 @@ svharness build \
|
|
|
666
766
|
```
|
|
667
767
|
svharnessbuild/ # 源码目录名沿用旧名(历史包名),CLI 名称已迁至 svharness
|
|
668
768
|
├── bin/cli.js # npm bin 入口 → dist/index.js
|
|
769
|
+
├── bin/launch-codechat-cli.js # 全局快捷命令 launch_codechat_cli
|
|
770
|
+
├── docs/agent-launcher-design.md # Agent 启动器详细设计
|
|
669
771
|
├── src/
|
|
670
772
|
│ ├── index.ts # CLI 入口(commander)
|
|
671
773
|
│ ├── types.ts # SupportedArch 枚举
|
|
672
774
|
│ ├── commands/init.ts # 4 步主管线(双模板根:_shared + <arch>)
|
|
775
|
+
│ ├── commands/start-agent.ts # start-agent 子命令
|
|
776
|
+
│ ├── commands/shell-integration.ts # Windows 右键菜单
|
|
777
|
+
│ ├── lib/agent-launcher.ts # CodeChat 启动核心
|
|
778
|
+
│ ├── lib/win-registry.ts # HKCU 注册表封装
|
|
673
779
|
│ ├── core/
|
|
674
780
|
│ │ ├── scaffold.ts # scaffoldLayered: 先 _shared 再 <arch>
|
|
675
781
|
│ │ ├── render-meta.ts # renderMetaLayered: 同名 .ejs 以 <arch> 为准
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { runStartAgent } = require('../dist/lib/agent-launcher');
|
|
5
|
+
|
|
6
|
+
const positionalWorkdir = process.argv[2];
|
|
7
|
+
|
|
8
|
+
runStartAgent({
|
|
9
|
+
agent: 'codechat',
|
|
10
|
+
positionalWorkdir,
|
|
11
|
+
})
|
|
12
|
+
.then((code) => {
|
|
13
|
+
process.exitCode = code;
|
|
14
|
+
})
|
|
15
|
+
.catch((err) => {
|
|
16
|
+
console.error('[err] ' + err.message);
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
});
|
package/dist/commands/convert.js
CHANGED
|
@@ -27,6 +27,7 @@ const logger_1 = require("../utils/logger");
|
|
|
27
27
|
const validate_args_1 = require("../utils/validate-args");
|
|
28
28
|
const markitdown_client_1 = require("../core/markitdown-client");
|
|
29
29
|
const doc_intake_paths_1 = require("../core/doc-intake-paths");
|
|
30
|
+
const structure_scan_1 = require("../lib/requirements/structure-scan");
|
|
30
31
|
const markdown_table_cleanup_1 = require("../core/markdown-table-cleanup");
|
|
31
32
|
const markdown_sheet_split_1 = require("../core/markdown-sheet-split");
|
|
32
33
|
/**
|
|
@@ -87,7 +88,12 @@ async function runConvert(opts) {
|
|
|
87
88
|
configRows.push({ label: 'harness ', value: v.harnessRoot });
|
|
88
89
|
configRows.push({ label: 'type ', value: v.type });
|
|
89
90
|
}
|
|
90
|
-
configRows.push({ label: 'output dir ', value: outDir }, {
|
|
91
|
+
configRows.push({ label: 'output dir ', value: outDir }, {
|
|
92
|
+
label: 'endpoint ',
|
|
93
|
+
value: v.endpoints.length > 1
|
|
94
|
+
? `${v.endpoint} (+${v.endpoints.length - 1} failover)`
|
|
95
|
+
: v.endpoint,
|
|
96
|
+
}, { label: 'concurrency', value: String(v.concurrency) }, { label: 'max file ', value: `${v.maxFileMB} MB` }, { label: 'timeout ', value: `${v.timeoutSec}s` }, { label: 'force ', value: opts.force ? 'yes' : 'no' }, {
|
|
91
97
|
label: 'split sheets',
|
|
92
98
|
value: opts.splitSheets === false
|
|
93
99
|
? 'no'
|
|
@@ -130,20 +136,60 @@ async function runConvert(opts) {
|
|
|
130
136
|
return;
|
|
131
137
|
}
|
|
132
138
|
}
|
|
133
|
-
// 4. Probe health endpoint before any upload.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
// 4. Probe health endpoint(s) before any upload.
|
|
140
|
+
let activeEndpoints = v.endpoints;
|
|
141
|
+
if (v.endpoints.length > 1) {
|
|
142
|
+
activeEndpoints = await (0, markitdown_client_1.filterHealthyEndpoints)(v.endpoints);
|
|
143
|
+
if (activeEndpoints.length === 0) {
|
|
144
|
+
logger_1.logger.error(`all ${v.endpoints.length} markitdown_serve endpoint(s) unreachable`);
|
|
145
|
+
for (const ep of v.endpoints) {
|
|
146
|
+
logger_1.logger.plain(` - ${ep}`);
|
|
147
|
+
}
|
|
148
|
+
process.exitCode = 1;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (activeEndpoints.length < v.endpoints.length) {
|
|
152
|
+
logger_1.logger.warn(`using ${activeEndpoints.length}/${v.endpoints.length} healthy endpoint(s)`);
|
|
153
|
+
}
|
|
154
|
+
if (opts.verbose) {
|
|
155
|
+
try {
|
|
156
|
+
const h = await (0, markitdown_client_1.ping)(activeEndpoints[0]);
|
|
157
|
+
if (h.converters) {
|
|
158
|
+
const ready = Object.entries(h.converters)
|
|
159
|
+
.filter(([, ok]) => ok)
|
|
160
|
+
.map(([name]) => name)
|
|
161
|
+
.join(', ');
|
|
162
|
+
logger_1.logger.debug(`converters ready: ${ready}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
/* ignore */
|
|
167
|
+
}
|
|
168
|
+
}
|
|
137
169
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
170
|
+
else {
|
|
171
|
+
try {
|
|
172
|
+
const h = await (0, markitdown_client_1.ping)(v.endpoint);
|
|
173
|
+
logger_1.logger.debug(`healthz ok: status=${h.status} markitdown=${h.markitdown_version ?? 'n/a'}`);
|
|
174
|
+
if (opts.verbose && h.converters) {
|
|
175
|
+
const ready = Object.entries(h.converters)
|
|
176
|
+
.filter(([, ok]) => ok)
|
|
177
|
+
.map(([name]) => name)
|
|
178
|
+
.join(', ');
|
|
179
|
+
logger_1.logger.debug(`converters ready: ${ready}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
const e = err;
|
|
184
|
+
logger_1.logger.error(`markitdown_serve unreachable at ${v.endpoint}: ${e.message}`);
|
|
185
|
+
logger_1.logger.plain('');
|
|
186
|
+
logger_1.logger.plain(' - confirm the service is deployed and reachable from this machine');
|
|
187
|
+
logger_1.logger.plain(' - pass --endpoint <url> or comma-separated URLs for failover');
|
|
188
|
+
logger_1.logger.plain(' - set env SVHARNESS_MARKITDOWN_ENDPOINT (supports comma-separated list)');
|
|
189
|
+
logger_1.logger.plain(' - check the server code under svharnessbuild/markitdown_serve/');
|
|
190
|
+
process.exitCode = 1;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
147
193
|
}
|
|
148
194
|
// 5. Bounded-concurrency upload.
|
|
149
195
|
const results = skipped.map(planToResult);
|
|
@@ -155,7 +201,7 @@ async function runConvert(opts) {
|
|
|
155
201
|
const item = queue.shift();
|
|
156
202
|
if (!item)
|
|
157
203
|
break;
|
|
158
|
-
results.push(await uploadOne(item.source, outDir, used, v, opts));
|
|
204
|
+
results.push(await uploadOne(item.source, outDir, used, v, opts, activeEndpoints));
|
|
159
205
|
}
|
|
160
206
|
};
|
|
161
207
|
for (let i = 0; i < Math.min(v.concurrency, toUpload.length); i++) {
|
|
@@ -173,6 +219,18 @@ async function runConvert(opts) {
|
|
|
173
219
|
const failed = results.filter((r) => r.status === 'failed');
|
|
174
220
|
if (failed.length > 0) {
|
|
175
221
|
process.exitCode = 1;
|
|
222
|
+
await writeFailuresReport(outDir, activeEndpoints, failed, opts.failuresReport);
|
|
223
|
+
}
|
|
224
|
+
if (v.harnessMode && v.harnessRoot && v.type === 'requirements') {
|
|
225
|
+
try {
|
|
226
|
+
await (0, structure_scan_1.maybeRunStructureScanForRequirements)(v.harnessRoot, 'convert', {
|
|
227
|
+
noStructureScan: opts.noStructureScan,
|
|
228
|
+
verbose: opts.verbose,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
logger_1.logger.warn(`structure-scan 失败(不中断 convert):${err.message}`);
|
|
233
|
+
}
|
|
176
234
|
}
|
|
177
235
|
}
|
|
178
236
|
/** Walk glob patterns / explicit paths / directories into an absolute file list. */
|
|
@@ -324,7 +382,7 @@ async function buildPlan(files, limitBytes) {
|
|
|
324
382
|
}
|
|
325
383
|
return out;
|
|
326
384
|
}
|
|
327
|
-
async function uploadOne(source, outDir, used, v, opts) {
|
|
385
|
+
async function uploadOne(source, outDir, used, v, opts, activeEndpoints) {
|
|
328
386
|
const basename = node_path_1.default.basename(source, node_path_1.default.extname(source));
|
|
329
387
|
const outPath = await resolveOutputPath(outDir, basename, used, !!opts.force);
|
|
330
388
|
used.add(outPath);
|
|
@@ -332,7 +390,11 @@ async function uploadOne(source, outDir, used, v, opts) {
|
|
|
332
390
|
const splitSuffix = opts.splitSheetsSuffix ?? '_split';
|
|
333
391
|
logger_1.logger.debug(`upload ${source} -> ${outPath}`);
|
|
334
392
|
try {
|
|
335
|
-
const resp = await (0, markitdown_client_1.
|
|
393
|
+
const resp = await (0, markitdown_client_1.postConvertWithEndpointFailover)(activeEndpoints, source, v.timeoutSec * 1000);
|
|
394
|
+
if (opts.verbose && (resp.converter || resp.fallback_used)) {
|
|
395
|
+
logger_1.logger.debug(`converter ${node_path_1.default.basename(source)}: ${resp.converter ?? 'markitdown'} ` +
|
|
396
|
+
`fallback=${resp.fallback_used ? 'yes' : 'no'} endpoint=${resp.endpointUsed}`);
|
|
397
|
+
}
|
|
336
398
|
const ext = node_path_1.default.extname(source).toLowerCase();
|
|
337
399
|
let markdown = resp.markdown;
|
|
338
400
|
if (markdown_table_cleanup_1.XLSX_CONVERT_EXTS.has(ext)) {
|
|
@@ -358,17 +420,37 @@ async function uploadOne(source, outDir, used, v, opts) {
|
|
|
358
420
|
outputs.push(...splitWritten);
|
|
359
421
|
logger_1.logger.debug(`sheet split ${node_path_1.default.basename(source)}: ${split.sections.length} sections -> ${splitDir}`);
|
|
360
422
|
logger_1.logger.success(`${node_path_1.default.basename(source)} -> ${node_path_1.default.basename(outPath)} + ${split.sections.length} sheets in ${node_path_1.default.basename(splitDir)}/ (${bytes}B)`);
|
|
361
|
-
return {
|
|
423
|
+
return {
|
|
424
|
+
source,
|
|
425
|
+
output: outPath,
|
|
426
|
+
outputs,
|
|
427
|
+
status: 'ok',
|
|
428
|
+
bytes,
|
|
429
|
+
converter: resp.converter,
|
|
430
|
+
fallbackUsed: resp.fallback_used,
|
|
431
|
+
endpoint: resp.endpointUsed,
|
|
432
|
+
};
|
|
362
433
|
}
|
|
363
434
|
}
|
|
364
|
-
|
|
365
|
-
|
|
435
|
+
const convNote = resp.fallback_used && resp.converter ? ` [${resp.converter}]` : '';
|
|
436
|
+
logger_1.logger.success(`${node_path_1.default.basename(source)} -> ${node_path_1.default.basename(outPath)} (${bytes}B)${convNote}`);
|
|
437
|
+
return {
|
|
438
|
+
source,
|
|
439
|
+
output: outPath,
|
|
440
|
+
outputs,
|
|
441
|
+
status: 'ok',
|
|
442
|
+
bytes,
|
|
443
|
+
converter: resp.converter,
|
|
444
|
+
fallbackUsed: resp.fallback_used,
|
|
445
|
+
endpoint: resp.endpointUsed,
|
|
446
|
+
};
|
|
366
447
|
}
|
|
367
448
|
catch (err) {
|
|
368
449
|
const e = err;
|
|
369
450
|
const reason = e.message;
|
|
451
|
+
const traceId = e instanceof markitdown_client_1.MarkItDownClientError ? (0, markitdown_client_1.extractTraceId)(e.body) : undefined;
|
|
370
452
|
logger_1.logger.error(`failed: ${node_path_1.default.basename(source)} — ${reason}`);
|
|
371
|
-
return { source, status: 'failed', reason };
|
|
453
|
+
return { source, status: 'failed', reason, traceId };
|
|
372
454
|
}
|
|
373
455
|
}
|
|
374
456
|
/**
|
|
@@ -393,14 +475,39 @@ function printSummary(results) {
|
|
|
393
475
|
const ok = results.filter((r) => r.status === 'ok').length;
|
|
394
476
|
const failed = results.filter((r) => r.status === 'failed').length;
|
|
395
477
|
const skipped = results.filter((r) => r.status === 'skipped').length;
|
|
478
|
+
const fallbackOk = results.filter((r) => r.status === 'ok' && r.fallbackUsed).length;
|
|
396
479
|
logger_1.logger.section('convert summary');
|
|
397
480
|
logger_1.logger.plain(` ok : ${ok}`);
|
|
398
481
|
logger_1.logger.plain(` skipped : ${skipped}`);
|
|
399
482
|
logger_1.logger.plain(` failed : ${failed}`);
|
|
483
|
+
if (fallbackOk > 0) {
|
|
484
|
+
logger_1.logger.plain(` fallback: ${fallbackOk} file(s) used backup converter`);
|
|
485
|
+
}
|
|
400
486
|
if (failed > 0) {
|
|
401
487
|
logger_1.logger.plain('');
|
|
402
488
|
for (const r of results.filter((x) => x.status === 'failed')) {
|
|
403
|
-
|
|
489
|
+
const trace = r.traceId ? ` (trace_id=${r.traceId})` : '';
|
|
490
|
+
logger_1.logger.plain(` - ${node_path_1.default.basename(r.source)}: ${r.reason}${trace}`);
|
|
404
491
|
}
|
|
405
492
|
}
|
|
406
493
|
}
|
|
494
|
+
async function writeFailuresReport(outDir, endpoints, failed, customPath) {
|
|
495
|
+
if (customPath === '')
|
|
496
|
+
return;
|
|
497
|
+
const reportPath = customPath && customPath.trim().length > 0
|
|
498
|
+
? node_path_1.default.isAbsolute(customPath)
|
|
499
|
+
? customPath
|
|
500
|
+
: node_path_1.default.resolve(outDir, customPath)
|
|
501
|
+
: node_path_1.default.join(outDir, 'convert-failures.json');
|
|
502
|
+
const payload = {
|
|
503
|
+
generated_at: new Date().toISOString(),
|
|
504
|
+
endpoints,
|
|
505
|
+
failures: failed.map((r) => ({
|
|
506
|
+
source: r.source,
|
|
507
|
+
reason: r.reason,
|
|
508
|
+
trace_id: r.traceId,
|
|
509
|
+
})),
|
|
510
|
+
};
|
|
511
|
+
await fs_extra_1.default.writeFile(reportPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
512
|
+
logger_1.logger.warn(`wrote failure manifest: ${reportPath}`);
|
|
513
|
+
}
|