refactor(cli)!: complete app-owned profile startup

This commit is contained in:
Turtle
2026-08-09 18:23:25 +08:00
parent 7e3a82eacc
commit d4ccfbd80f
97 changed files with 260 additions and 224 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/config-catalog.md
config-catalog.md: 64c7722a3cc3788d110eab8d9be161129684b942
config-catalog.zh.md: 4cc146a38b408d9f1a7e5067419fead48520ae11
config-catalog.md: 836a7f6a81f8c77be12fd10be5b8204be8c1acc0
config-catalog.zh.md: 22dca1252d93ea9ce223464079f3c51c35eeb89d

View File

@@ -572,7 +572,7 @@ Source: [`packages/goal/goal/src/index.ts:116`](../packages/goal/goal/src/index.
Requires: `agentDefaultModel` · `agents` · `sessions`
```ts config-catalog
/** Plugin config: the task, patched in by the launcher. */
/** Plugin config: the task resolved from this app's injected startup service. */
export interface Config {
/** The prompt text for the single run. */
task: string
@@ -2520,21 +2520,21 @@ Source: [`packages/web/web/src/index.ts:55`](../packages/web/web/src/index.ts)
Requires: `httpServer`
```ts config-catalog
/** Plugin config: the surface facts the launcher patches over this bundle's defaults. */
/** Plugin config: composed deployment settings plus per-invocation startup values. */
export interface Config {
/** Whether this process mounted the client-plugin HMR receiver (`dsh web --dev`). */
mode: WebMode
/** Print the URL line on activation; a headless layer over this bundle turns it off. */
/** Print the URL line on activation; a non-interactive layer can turn it off. */
printUrl: boolean
/**
* Register the model-visible surface context (the `app:web-surface` prompt
* section and the `DSH_WEB_URL`/`DSH_WEB_MODE` bash variables). A one-shot
* layer turns it off: its user is not interacting through the GUI, so the
* non-interactive layer can turn it off when its user is not in the GUI, so the
* orientation text would be false.
*/
surfaceContext: boolean
/**
* LAN IPv4 addresses sampled once by the launcher when the effective bind
* LAN IPv4 addresses sampled once by the app startup row when the effective bind
* is all-interfaces — the exact snapshot the /api trust fence was
* configured with, so the printed LAN URL can never name an address the
* fence rejects. Empty on a loopback bind.
@@ -2546,7 +2546,7 @@ export interface Config {
export type WebMode = 'production' | 'development'
```
Source: [`packages/bundle/web-app/src/index.ts:36`](../packages/bundle/web-app/src/index.ts)
Source: [`packages/bundle/web-app/src/index.ts:41`](../packages/bundle/web-app/src/index.ts)
## `@deepseek-ai/dsh-web-fetch-local`
@@ -2792,6 +2792,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
- `@deepseek-ai/dsh-client-ui-slots` ([`packages/client/ui-slots/src/index.ts`](../packages/client/ui-slots/src/index.ts))
- `@deepseek-ai/dsh-client-web` ([`packages/client/web/src/index.ts`](../packages/client/web/src/index.ts))
- `@deepseek-ai/dsh-client-web-react` ([`packages/client/web-react/src/index.ts`](../packages/client/web-react/src/index.ts))
- `@deepseek-ai/dsh-cmdline` ([`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts))
- `@deepseek-ai/dsh-environment` ([`packages/util/environment/src/index.ts`](../packages/util/environment/src/index.ts))
- `@deepseek-ai/dsh-helper` ([`packages/scaffold/helper/src/index.ts`](../packages/scaffold/helper/src/index.ts))
- `@deepseek-ai/dsh-hook-protocol` ([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts))

View File

@@ -574,7 +574,7 @@ export interface Config {
需要:`agentDefaultModel` · `agents` · `sessions`
```ts config-catalog
/** Plugin config: the task, patched in by the launcher. */
/** Plugin config: the task resolved from this app's injected startup service. */
export interface Config {
/** The prompt text for the single run. */
task: string
@@ -2521,21 +2521,21 @@ export interface WebServiceConfig {
需要:`httpServer`
```ts config-catalog
/** Plugin config: the surface facts the launcher patches over this bundle's defaults. */
/** Plugin config: composed deployment settings plus per-invocation startup values. */
export interface Config {
/** Whether this process mounted the client-plugin HMR receiver (`dsh web --dev`). */
mode: WebMode
/** Print the URL line on activation; a headless layer over this bundle turns it off. */
/** Print the URL line on activation; a non-interactive layer can turn it off. */
printUrl: boolean
/**
* Register the model-visible surface context (the `app:web-surface` prompt
* section and the `DSH_WEB_URL`/`DSH_WEB_MODE` bash variables). A one-shot
* layer turns it off: its user is not interacting through the GUI, so the
* non-interactive layer can turn it off when its user is not in the GUI, so the
* orientation text would be false.
*/
surfaceContext: boolean
/**
* LAN IPv4 addresses sampled once by the launcher when the effective bind
* LAN IPv4 addresses sampled once by the app startup row when the effective bind
* is all-interfaces — the exact snapshot the /api trust fence was
* configured with, so the printed LAN URL can never name an address the
* fence rejects. Empty on a loopback bind.
@@ -2547,7 +2547,7 @@ export interface Config {
export type WebMode = 'production' | 'development'
```
来源:[`packages/bundle/web-app/src/index.ts:32`](../packages/bundle/web-app/src/index.ts)
来源:[`packages/bundle/web-app/src/index.ts:41`](../packages/bundle/web-app/src/index.ts)
## `@deepseek-ai/dsh-web-fetch-local`
@@ -2792,6 +2792,7 @@ export interface Config {
- `@deepseek-ai/dsh-client-ui-slots`[`packages/client/ui-slots/src/index.ts`](../packages/client/ui-slots/src/index.ts)
- `@deepseek-ai/dsh-client-web`[`packages/client/web/src/index.ts`](../packages/client/web/src/index.ts)
- `@deepseek-ai/dsh-client-web-react`[`packages/client/web-react/src/index.ts`](../packages/client/web-react/src/index.ts)
- `@deepseek-ai/dsh-cmdline`[`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts)
- `@deepseek-ai/dsh-environment`[`packages/util/environment/src/index.ts`](../packages/util/environment/src/index.ts)
- `@deepseek-ai/dsh-helper`[`packages/scaffold/helper/src/index.ts`](../packages/scaffold/helper/src/index.ts)
- `@deepseek-ai/dsh-hook-protocol`[`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts)

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/testing.md
testing.md: f5e8a478ec86c29c52f4127c51682c1c44fd23a7
testing.zh.md: bd1fa7d23263d7c6e3bed65ef4ed09576ca47cc1
testing.md: f330bb1e02f3613c63f3989a8f9128f737bf5c52
testing.zh.md: db6facb4fa4bf07eda0a6ee7e558c8c60d4c331e

View File

@@ -9,7 +9,7 @@ How this repo tests, tier by tier, and the rules that keep a green suite meaning
- **Unit** (`pnpm run test`): vitest over package and example specs under their `tests/**` directories plus repository script specs under `scripts/**/*.spec.ts`; tests stay with the code area they exercise. Every registry gets an HMR-safety test (dispose the contributing fiber, assert cleanup). Prefer edge cases, error paths, event ordering, concurrency races, and permanent tests for contract regressions (see `packages/core/agent-loop/tests/contract-regressions.spec.ts`).
- **Coverage gate** (`pnpm run test:coverage`): the gating run, per-file 100% on `packages/*/*/src`. An uncovered line is often dead code the gate is correctly flagging for deletion, not a missing test to bolt on. Line coverage is necessary, never sufficient — it proves lines ran, not that the feature works as shipped. Per-file 100% on `packages/bash/pwsh-local/src` needs a real `pwsh`: without one its executor suites self-skip and `vitest.config.ts` exempts the file so pwsh-less hosts stay green, while CI runners ship pwsh and enforce the full bar.
- **Real-API e2e** (`pnpm run test:e2e`): with-key tests against live provider APIs — the DeepSeek model plus provider-specific smokes that gate on their own keys (`EXA_API_KEY`, `PERPLEXITY_API_KEY`, …); each suite self-skips without its key so keyless CI stays green ([real-API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md)).
- **Snapshot** (`pnpm run test:snapshot`): keyless expected outputs cover external behavior — transport contracts and presentation, while persisted logs pin assembled backend behavior. ACP boots the real automation-server example, replays a recorded session, and diffs normalized JSON-RPC plus the re-persisted log ([ACP snapshot Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md)); headless backend scenarios boot their explicit example composition through an unexported JSONL test driver, while `apps/cli` separately owns product `dsh run` acceptance. Use `pnpm run test:snapshot:record` when a model transcript changes and `pnpm run test:snapshot:refresh` when replay input remains valid; review every JSONL and expected-output diff. One ACP scenario (`text-turn`) pins full system-prompt/tool-schema content; other fixtures tokenize it so an edit churns one line ([pinned-header Agent Note](../.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)).
- **Snapshot** (`pnpm run test:snapshot`): keyless expected outputs cover external behavior — transport contracts and presentation, while persisted logs pin assembled backend behavior. ACP boots the real automation-server example, replays a recorded session, and diffs normalized JSON-RPC plus the re-persisted log ([ACP snapshot Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md)); headless backend scenarios boot their explicit example composition through an unexported JSONL test driver, while `apps/cli` separately owns product `dsh --profile headless` acceptance. Use `pnpm run test:snapshot:record` when a model transcript changes and `pnpm run test:snapshot:refresh` when replay input remains valid; review every JSONL and expected-output diff. One ACP scenario (`text-turn`) pins full system-prompt/tool-schema content; other fixtures tokenize it so an edit churns one line ([pinned-header Agent Note](../.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)).
- **Web browser snapshot** (`pnpm run test:web`; required Linux PR gate): Chromium compares replayed browser output with `apps/web/tests/snapshots/`. CI forces read-only `DSH_SNAPSHOT=replay`, never writing expected outputs; record/refresh stay local and every diff is reviewed ([web e2e lane](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md), [CI gate decision](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md)). `test:web` [builds first](../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md) for plugin CSS.
Committed session-format JSONL uses the canonical packed-row layout, and the keyless snapshot gate discovers every such fixture by its `session` header; the [temporary migrator](../scripts/migrate-packed-session-fixtures.ts) rewrites older fixture layouts.

View File

@@ -9,7 +9,7 @@
- **单元测试**`pnpm run test`vitest 运行包package和示例各自的 `tests/**` 目录下的测试,以及匹配 `scripts/**/*.spec.ts` 的仓库脚本测试;测试文件与其所覆盖的代码区域放在一起。每个注册表都有一个 HMR热模块替换安全测试dispose资源释放贡献的 fiber断言清理完成。优先覆盖边界情况、错误路径、事件顺序、并发竞态以及针对约定回归的永久测试`packages/core/agent-loop/tests/contract-regressions.spec.ts`)。
- **覆盖率门禁**`pnpm run test:coverage`):门禁级运行,对 `packages/*/*/src` 按文件 100% 覆盖。未覆盖的行往往是门禁正确标记出的死代码(应删除),而非需要补写的测试。行覆盖率是必要条件,但永远不是充分条件:它证明行被执行过,不证明功能按交付预期工作。`packages/bash/pwsh-local/src` 的按文件 100% 覆盖需要真实的 `pwsh`:缺少它时其 executor 套件会自动跳过,`vitest.config.ts` 会豁免该文件以使无 pwsh 的主机保持绿色,而 CI runner 自带 pwsh仍按完整标准执行门禁。
- **真实 API e2e**`pnpm run test:e2e`):带密钥测试调用真实提供方 API包括 DeepSeek 模型以及各提供方特有的冒烟测试;这些测试各自由自己的密钥控制(`EXA_API_KEY``PERPLEXITY_API_KEY` 等),缺少密钥时套件会自动跳过,使 keyless CI 保持绿色([真实 API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md))。
- **快照**`pnpm run test:snapshot`无密钥预期输出覆盖对外行为传输约定与呈现持久化日志则固定组装后的后端行为。ACP 启动真实的自动化服务器示例、回放录制会话,并对归一化 JSON-RPC 与重新持久化的日志执行 diff[ACP 快照 Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md)headless 后端场景通过未导出的 JSONL 测试 driver 启动各自显式的示例组装,而 `apps/cli` 则单独负责产品 CLI命令行界面`dsh run` 的验收。当模型 transcript文本记录发生变化时使用 `pnpm run test:snapshot:record`,回放输入仍然有效时使用 `pnpm run test:snapshot:refresh`;请审查每一处 JSONL 与预期输出差异。一个 ACP 场景(`text-turn`)固定完整的系统提示词与工具 schema 内容;其他 fixture测试前置数据将其 token 化,因此修改只会扰动一行([pinned-header Agent Note](../.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md))。
- **快照**`pnpm run test:snapshot`无密钥预期输出覆盖对外行为传输约定与呈现持久化日志则固定组装后的后端行为。ACP 启动真实的自动化服务器示例、回放录制会话,并对归一化 JSON-RPC 与重新持久化的日志执行 diff[ACP 快照 Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md)headless 后端场景通过未导出的 JSONL 测试 driver 启动各自显式的示例组装,而 `apps/cli` 则单独负责产品 CLI命令行界面`dsh --profile headless` 的验收。当模型 transcript文本记录发生变化时使用 `pnpm run test:snapshot:record`,回放输入仍然有效时使用 `pnpm run test:snapshot:refresh`;请审查每一处 JSONL 与预期输出差异。一个 ACP 场景(`text-turn`)固定完整的系统提示词与工具 schema 内容;其他 fixture测试前置数据将其 token 化,因此修改只会扰动一行([pinned-header Agent Note](../.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md))。
- **Web 浏览器快照**`pnpm run test:web`;必需的 Linux PRPull Request门禁Chromium 将回放后的浏览器输出与 `apps/web/tests/snapshots/` 比较。CI 强制只读的 `DSH_SNAPSHOT=replay`绝不写入预期输出record/refresh 留在本地,每处 diff 都须评审([web e2e 车道](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md)、[CI 门禁决策](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md))。`test:web` 会[先构建](../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md)以交付插件 CSS。
签入仓库的会话格式 JSONL 使用规范打包行布局,无密钥快照门禁会通过 `session` header 发现每一份此类 fixture[临时迁移器](../scripts/migrate-packed-session-fixtures.ts)会改写旧版 fixture 布局。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/user/develop/basic/publish.md
publish.md: c81e53d75ecccd31c9051f33252854dbe156c566
publish.zh.md: c5a15be00bea838eb534dbf608c29d3832c2c0e1
publish.md: 04520b0fb7d30c716e3c87761bd38f0c25824739
publish.zh.md: 7b0e0141dc0522bb5ec356aa8cba1618c9517f09

View File

@@ -118,9 +118,19 @@ A bundle that defines a runnable app marks its startup row through the injection
inject: [cmdlineArgs]
```
That row calls `runStartup` from [`@deepseek-ai/dsh-cmdline`](../../../../packages/ui/cmdline/README.md) with the app's own commander program. The launcher hands it every argument after the launcher flags, so app-specific flags need no launcher change. Loader mounts the composition once, waits for each row's injections, and only then evaluates that row's `!!js` config against its injected context.
That row calls `runStartup` from [`@deepseek-ai/dsh-cmdline`](../../../../packages/boot/cmdline/README.md) with the app's own commander program. The launcher hands it every argument after the launcher flags, so app-specific flags need no launcher change. Loader mounts the composition once, waits for each row's injections, and only then evaluates that row's `!!js` config against its injected context.
Rows configured by those arguments inject the startup service and read it from their own `!!js` options, with the deployment value beside it as the fallback. On `--help`, the service is not provided, so those rows never activate. An app layered over another app disables the lower startup row, because one composition has one command-line owner.
Rows configured by those arguments inject the startup service and read it from their own `!!js` options, with the deployment value beside it as the fallback:
```yaml
- id: my-app
name: '@example/my-app'
inject: [myAppStartup]
config:
port: !!js ctx.myAppStartup.port ?? 8080
```
On `--help`, the service is not provided, so those rows never activate. An app layered over another app disables the lower startup row, because one composition has one command-line owner.
## Installing from GitHub: the build-script catch

View File

@@ -118,9 +118,19 @@ dsh --profile demo
inject: [cmdlineArgs]
```
该行使用应用自己的 commander program 调用 [`@deepseek-ai/dsh-cmdline`](../../../../packages/ui/cmdline/README.md) 中的 `runStartup`。启动器把自身 flag 之后的所有参数交给它,因此添加应用专属 flag 无需修改启动器。Loader 只挂载一次组合,等待每一行的注入,再基于其已注入的上下文求值该行的 `!!js` 配置。
该行使用应用自己的 commander program 调用 [`@deepseek-ai/dsh-cmdline`](../../../../packages/boot/cmdline/README.md) 中的 `runStartup`。启动器把自身 flag 之后的所有参数交给它,因此添加应用专属 flag 无需修改启动器。Loader 只挂载一次组合,等待每一行的注入,再基于其已注入的上下文求值该行的 `!!js` 配置。
受这些参数配置的行会注入启动服务,并在自己的 `!!js` 选项中读取它,同时把部署取值写在旁边作为回退。遇到 `--help` 时,该服务不会被提供,所以这些行不会激活。叠加在另一应用之上的应用会禁用下层启动行,因为一套组合只能有一个命令行所有者。
受这些参数配置的行会注入启动服务,并在自己的 `!!js` 选项中读取它,同时把部署取值写在旁边作为回退
```yaml
- id: my-app
name: '@example/my-app'
inject: [myAppStartup]
config:
port: !!js ctx.myAppStartup.port ?? 8080
```
遇到 `--help` 时,该服务不会被提供,所以这些行不会激活。叠加在另一应用之上的应用会禁用下层启动行,因为一套组合只能有一个命令行所有者。
## 从 GitHub 安装:构建脚本这道坎

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/user/guide/quickstart.md
quickstart.md: 6a0b292ce12b32b7993b7de56b35f1df2e7a7153
quickstart.zh.md: 008245f136e28630c7e8368eeec536e11112a885
quickstart.md: 8e883efd470fef329a7308f1017cab0b7afcdd67
quickstart.zh.md: e0e3f4e4754bca588831b6d204f77d1c97a140b3

View File

@@ -36,10 +36,10 @@ DEEPSEEK_API_KEY=sk-your-key-here
Run a non-interactive task and print its final answer:
```sh
pnpm run dsh run "summarize the architecture of this workspace"
pnpm run dsh --profile headless "summarize the architecture of this workspace"
```
`dsh run` creates and persists a fresh session, prints the final assistant answer, and exits. It starts no Web server or listening port, and a successful run leaves stderr empty.
`dsh --profile headless` creates and persists a fresh session, prints the final assistant answer, and exits. It starts no Web server or listening port, and a successful run leaves stderr empty.
## Step 3: use the Web UI
@@ -53,7 +53,7 @@ Open `http://127.0.0.1:3080`. The agent can read and write files, run commands,
## What happened
`dsh run` boots the `headless` profile: [`dsh-base`](../../../packages/bundle/base/cordis.patch.yml) and [`dsh-headless`](../../../packages/bundle/headless/cordis.patch.yml) compose over an empty root, then the runner drives the core Agent and Session services directly. `dsh web` instead composes `dsh-base` with [`dsh-web-app`](../../../packages/bundle/web-app/cordis.patch.yml), which owns the Host, HTTP, and browser layers. Both read the same default DeepSeek model route from `dsh-base`.
`dsh --profile headless` boots the `headless` profile: [`dsh-base`](../../../packages/bundle/base/cordis.patch.yml) and [`dsh-headless`](../../../packages/bundle/headless/cordis.patch.yml) compose over an empty root, then the runner drives the core Agent and Session services directly. `dsh web` instead composes `dsh-base` with [`dsh-web-app`](../../../packages/bundle/web-app/cordis.patch.yml), which owns the Host, HTTP, and browser layers. Both read the same default DeepSeek model route from `dsh-base`.
## Next steps

View File

@@ -36,10 +36,10 @@ DEEPSEEK_API_KEY=sk-your-key-here
运行一个非交互式任务并打印最终回答:
```sh
pnpm run dsh run "summarize the architecture of this workspace"
pnpm run dsh --profile headless "summarize the architecture of this workspace"
```
`dsh run` 创建并持久化一个新会话,打印最终助手回答,然后退出。它不会启动 Web 服务器或监听端口;成功运行时 stderr 为空。
`dsh --profile headless` 创建并持久化一个新会话,打印最终助手回答,然后退出。它不会启动 Web 服务器或监听端口;成功运行时 stderr 为空。
## 第三步:使用 Web UI
@@ -53,7 +53,7 @@ pnpm run dsh web
## 运行原理
`dsh run` 启动 `headless` profile[`dsh-base`](../../../packages/bundle/base/cordis.patch.yml) 和 [`dsh-headless`](../../../packages/bundle/headless/cordis.patch.yml) 在空根之上组合,随后 runner 直接驱动 core Agent 与 Session 服务。`dsh web` 则由 `dsh-base` 与 [`dsh-web-app`](../../../packages/bundle/web-app/cordis.patch.yml) 组合,后者拥有 Host、HTTP 与浏览器层。二者都从 `dsh-base` 读取同一个默认 DeepSeek 模型路由。
`dsh --profile headless` 启动 `headless` profile[`dsh-base`](../../../packages/bundle/base/cordis.patch.yml) 和 [`dsh-headless`](../../../packages/bundle/headless/cordis.patch.yml) 在空根之上组合,随后 runner 直接驱动 core Agent 与 Session 服务。`dsh web` 则由 `dsh-base` 与 [`dsh-web-app`](../../../packages/bundle/web-app/cordis.patch.yml) 组合,后者拥有 Host、HTTP 与浏览器层。二者都从 `dsh-base` 读取同一个默认 DeepSeek 模型路由。
## 下一步