Merge remote-tracking branch 'refs/remotes/origin/worktree/web-multimodal-image-input' into worktree/pr555-simplify
# Conflicts: # .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.i18n.yaml # .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.md # .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md # apps/cli/README.i18n.yaml # apps/cli/README.md # apps/cli/README.zh.md # apps/cli/src/app-cli-entry.ts # packages/client/connection/src/client/fixture.ts # packages/client/ui-conversation/src/client/service.ts # packages/host/apiproxy/src/api-proxy.ts # packages/host/apiproxy/src/api/host.schema.ts # packages/host/apiproxy/src/api/host.ts # packages/host/apiproxy/tests/fetch-carrier.spec.ts
This commit is contained in:
@@ -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 apps/cli/README.md
|
||||
README.md: 467c525361d003fb1638f5812a6524eaae84b7a8
|
||||
README.zh.md: 579cafd04e67aba42ad7d0ffa2105d33554fe1cb
|
||||
README.md: 93c36d18abd06bbd7a80c918f520b92489180395
|
||||
README.zh.md: 85f4624a592eaf2ae44dc31fb4e18fb5657e62fd
|
||||
|
||||
@@ -14,7 +14,7 @@ The TUI surface:
|
||||
- tells the agent where its own source lives: after boot it adds a prompt section naming this harness checkout, resolved from the launcher's real path so it holds under a PATH symlink and an arbitrary cwd, so the self-referential `cordis` toolset can read and modify it;
|
||||
- applies the personal overlay from `~/.dsh` (see [app-boot's Personal config](../../packages/ui/app-boot/README.md#personal-config)): `.env` fills environment gaps (ambient > project `.env` > personal `.env`), `config.yaml` patches the booted tree.
|
||||
|
||||
The Web and headless surfaces boot one shared composition (`cordis.yml`): both treat the invoking directory as the default project and Workspace root, create named Workspaces beneath that root unless `--workspace-root <path>` overrides it, load applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget, and opt into first-message model titles. `dsh web --provider <name> --model <id>` selects a route already registered by that composition; the shipped yml pi-ai row provides openai/anthropic with provider-native ambient credentials, while the default DeepSeek route remains text-only. Headless differs only in listening on an OS-assigned port (parallel `dsh -p` runs never collide; the stderr-printed URL opens the live session in a browser). Both need the frontend dist and client bundles built (`pnpm run build && pnpm run build:web`).
|
||||
The Web and headless surfaces boot one shared composition (`cordis.yml`): both treat the invoking directory as the default project and Workspace root, create named Workspaces beneath that root unless `--workspace-root <path>` overrides it, load applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget, and opt into first-message model titles. Headless differs only in listening on an OS-assigned port (parallel `dsh -p` runs never collide; the stderr-printed URL opens the live session in a browser). Both need the frontend dist and client bundles built (`pnpm run build && pnpm run build:web`).
|
||||
|
||||
The shipped TUI and Web compositions register the native DeepSeek adapter plus pi-ai OpenAI and Anthropic profiles. Credentials and endpoint overrides come from the provider-standard `DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL`, `OPENAI_API_KEY` / `OPENAI_BASE_URL`, and `ANTHROPIC_API_KEY` / `ANTHROPIC_BASE_URL` pairs in the boot's layered environment.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ TUI 界面:
|
||||
- 告知 agent 自身源码所在位置:启动后添加一个命名此 harness checkout 的提示词段。该路径从启动器的真实路径解析,因此在 PATH 符号链接和任意 cwd 下仍然有效,使自指的 `cordis` 工具集可以读取并修改它;
|
||||
- 应用 `~/.dsh` 中的个人覆盖(参见 [app-boot 的个人配置](../../packages/ui/app-boot/README.md#personal-config)):`.env` 填补环境缺口(环境中已有的值 > 项目 `.env` > 个人 `.env`),`config.yaml` 则修补已启动的树。
|
||||
|
||||
Web 和无头界面启动同一个共享组合(`cordis.yml`):两者都将调用目录视为默认项目和 Workspace 根目录,除非通过 `--workspace-root <path>` 覆盖,否则会在该根目录下创建具名 Workspace;它们会把适用的 `AGENTS.md`/`CLAUDE.md` 指令加载到每个 agent-loop 请求前缀中,渲染预算为 65,536 字节,并选用首条消息模型标题。`dsh web --provider <name> --model <id>` 会选择该组合已经注册的路由;交付的 yml pi-ai row 使用提供方原生环境凭据提供 openai/anthropic 路由,而默认 DeepSeek 路由仍仅支持文本。无头界面唯一的差异是监听操作系统分配的端口(并行 `dsh -p` 运行绝不冲突;stderr 打印的 URL 会在浏览器中打开实时会话)。两者都需要先构建前端 dist 和客户端 bundle(`pnpm run build && pnpm run build:web`)。
|
||||
Web 和无头界面启动同一个共享组合(`cordis.yml`):两者都将调用目录视为默认项目和 Workspace 根目录,除非通过 `--workspace-root <path>` 覆盖,否则会在该根目录下创建具名 Workspace;它们会把适用的 `AGENTS.md`/`CLAUDE.md` 指令加载到每个 agent-loop 请求前缀中,渲染预算为 65,536 字节,并选用首条消息模型标题。无头界面唯一的差异是监听操作系统分配的端口(并行 `dsh -p` 运行绝不冲突;stderr 打印的 URL 会在浏览器中打开实时会话)。两者都需要先构建前端 dist 和客户端 bundle(`pnpm run build && pnpm run build:web`)。
|
||||
|
||||
已交付的 TUI 和 Web 组合会注册原生 DeepSeek 适配器,以及 pi-ai 的 OpenAI 和 Anthropic 提供方配置。凭据和端点覆盖来自启动分层环境中的提供方标准变量对:`DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL`、`OPENAI_API_KEY` / `OPENAI_BASE_URL` 和 `ANTHROPIC_API_KEY` / `ANTHROPIC_BASE_URL`。
|
||||
|
||||
|
||||
@@ -115,10 +115,6 @@ export interface AppCLIEntryOptions {
|
||||
port?: number
|
||||
/** Parent directory for name-created Workspaces; undefined uses the gateway's cwd fallback. */
|
||||
workspaceRoot?: string
|
||||
/** Host default provider override; the shipped tree must register the route. */
|
||||
provider?: string
|
||||
/** Host default model override. */
|
||||
model?: string
|
||||
/** Extra authorities for the /api browser-trust fence (`host` or `host:port`), appended to the derived LAN IP literals. */
|
||||
trustedHosts?: string[]
|
||||
}
|
||||
@@ -201,8 +197,6 @@ export class AppCLIEntry {
|
||||
if (this.options.host !== undefined) put('webserver', 'host', this.options.host)
|
||||
if (this.options.port !== undefined) put('webserver', 'port', this.options.port)
|
||||
if (this.options.workspaceRoot !== undefined) put('api-gateway', 'workspaceRoot', this.options.workspaceRoot)
|
||||
if (this.options.provider !== undefined) put('api-gateway', 'provider', this.options.provider)
|
||||
if (this.options.model !== undefined) put('api-gateway', 'model', this.options.model)
|
||||
|
||||
// Source 2b: authorities for the /api browser-trust fence (rationale on
|
||||
// resolveLanTrust).
|
||||
|
||||
@@ -33,7 +33,6 @@ interface HeadlessInvocation {
|
||||
* value fails loud at boot). `port` is `Number`-coerced only because the schema
|
||||
* wants a number, not a string. `dev` mounts the client HMR driver;
|
||||
* `workspaceRoot` is the parent directory for name-created workspaces.
|
||||
* `provider` and `model` override the host's default model route.
|
||||
*/
|
||||
interface WebInvocation {
|
||||
mode: 'web'
|
||||
@@ -41,8 +40,6 @@ interface WebInvocation {
|
||||
port?: number
|
||||
dev: boolean
|
||||
workspaceRoot?: string
|
||||
provider?: string
|
||||
model?: string
|
||||
/** Extra authorities for the /api browser-trust fence (`host` or `host:port`); LAN IP literals are derived, not listed here. */
|
||||
trustedHosts?: string[]
|
||||
}
|
||||
@@ -56,8 +53,6 @@ interface WebOptions {
|
||||
port?: string
|
||||
dev?: boolean
|
||||
workspaceRoot?: string
|
||||
provider?: string
|
||||
model?: string
|
||||
trustedHost?: string[]
|
||||
}
|
||||
|
||||
@@ -74,8 +69,6 @@ function resolveWeb(options: WebOptions): WebInvocation {
|
||||
...options.port !== undefined && { port: Number(options.port) },
|
||||
dev: options.dev === true,
|
||||
...options.workspaceRoot !== undefined && { workspaceRoot: options.workspaceRoot },
|
||||
...options.provider !== undefined && { provider: options.provider },
|
||||
...options.model !== undefined && { model: options.model },
|
||||
...options.trustedHost !== undefined && { trustedHosts: options.trustedHost },
|
||||
}
|
||||
}
|
||||
@@ -128,8 +121,6 @@ export function parseDshArgs(argv: readonly string[], version: string): DshInvoc
|
||||
.option('--port <port>', 'override the config listen port (0 requests an OS-assigned port)')
|
||||
.option('--dev', 'mount the client HMR driver and watch plugin bundles for rebuilds')
|
||||
.option('--workspace-root <path>', 'parent directory for name-created workspaces')
|
||||
.option('--provider <name>', 'override the host default provider')
|
||||
.option('--model <id>', 'override the host default model')
|
||||
.option('--trusted-host <authority...>', 'extra authority the /api browser-trust fence accepts (host or host:port; repeatable)')
|
||||
.action((options: WebOptions) => {
|
||||
// Commander parses the parent (default-surface) options on either side of
|
||||
|
||||
@@ -35,8 +35,6 @@ switch (invocation.mode) {
|
||||
invocation.port,
|
||||
invocation.dev,
|
||||
invocation.workspaceRoot,
|
||||
invocation.provider,
|
||||
invocation.model,
|
||||
invocation.trustedHosts,
|
||||
)
|
||||
break
|
||||
|
||||
@@ -22,8 +22,6 @@ const LOOPBACK_HOST = '127.0.0.1'
|
||||
* @param port - the listen port (`0` requests an OS-assigned port), or `undefined` to keep the config default.
|
||||
* @param dev - mount the client HMR driver and watch plugin bundles for rebuilds.
|
||||
* @param workspaceRoot - parent directory for name-created workspaces, or `undefined` for the gateway's cwd fallback.
|
||||
* @param provider - provider override, or `undefined` to keep the profile/config route.
|
||||
* @param model - model override, or `undefined` to keep the profile/config route.
|
||||
* @param trustedHosts - extra authorities for the /api browser-trust fence, or `undefined` for the derived LAN literals alone.
|
||||
*/
|
||||
export async function runWeb(
|
||||
@@ -31,8 +29,6 @@ export async function runWeb(
|
||||
port: number | undefined,
|
||||
dev: boolean,
|
||||
workspaceRoot: string | undefined,
|
||||
provider: string | undefined,
|
||||
model: string | undefined,
|
||||
trustedHosts: string[] | undefined,
|
||||
): Promise<void> {
|
||||
const entry = new AppCLIEntry({
|
||||
@@ -41,8 +37,6 @@ export async function runWeb(
|
||||
...host !== undefined && { host },
|
||||
...port !== undefined && { port },
|
||||
...workspaceRoot !== undefined && { workspaceRoot },
|
||||
...provider !== undefined && { provider },
|
||||
...model !== undefined && { model },
|
||||
...trustedHosts !== undefined && { trustedHosts },
|
||||
})
|
||||
const { ctx, port: boundPort } = await entry.run()
|
||||
|
||||
@@ -35,15 +35,12 @@ describe('parseDshArgs', () => {
|
||||
// at boot); the adapter only coerces the port string to a number.
|
||||
expect(parse([
|
||||
'web', '--host', '0.0.0.0', '--port', '8080', '--dev', '--workspace-root', '/w',
|
||||
'--provider', 'anthropic', '--model', 'claude-opus-4-8',
|
||||
])).toEqual({
|
||||
mode: 'web',
|
||||
host: '0.0.0.0',
|
||||
port: 8080,
|
||||
dev: true,
|
||||
workspaceRoot: '/w',
|
||||
provider: 'anthropic',
|
||||
model: 'claude-opus-4-8',
|
||||
})
|
||||
// --trusted-host is variadic and repeatable; authorities pass through unvalidated.
|
||||
expect(parse(['web', '--trusted-host', 'harness.internal:3080', 'lab.internal', '--trusted-host', '10.0.0.9']))
|
||||
@@ -65,6 +62,8 @@ describe('parseDshArgs', () => {
|
||||
expect(exitCode(['web', '-p', 'task'])).toBe(1)
|
||||
expect(exitCode(['web', '--resume', 's'])).toBe(1)
|
||||
expect(exitCode(['--config', 'c.yml', 'web'])).toBe(1)
|
||||
expect(exitCode(['web', '--provider', 'anthropic'])).toBe(1)
|
||||
expect(exitCode(['web', '--model', 'claude-opus-4-8'])).toBe(1)
|
||||
})
|
||||
|
||||
it('exits 0 for --help (disclosing web) and --version', () => {
|
||||
|
||||
Reference in New Issue
Block a user