Merge remote-tracking branch 'origin/master' into worktree/code-diff-card
This commit is contained in:
commit
3c632ebf66
318 files changed
+9061
-3253
No files matched your search
@@ -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 packages/bundle/web-app/README.md
|
||||
README.md: aea694942173a856861d00a69f15b451cc930976
|
||||
README.zh.md: 4dab93ff03071bff6783ae89b0397216b86a8c05
|
||||
README.md: 84630f1d9142b1e2300baa0cde0513acdb602da9
|
||||
README.zh.md: 0488c74f433b0ef1d7e6b1677b7f50fd21a198a2
|
||||
@@ -36,6 +36,10 @@ dsh --profile web --no-open --port 8080
|
||||
|
||||
After startup you see a `dsh web:` line whose root URL carries a fresh process token. Unless `--no-open` or an SSH session suppresses it, the default browser opens that URL, receives a signed cookie, and redirects to the clean root page. You know it worked when the page loads and you can chat with the agent. Two failures to expect: if the frontend is not built, startup stops with a build hint (`pnpm run build` in a checkout); if the browser cannot be opened, a credential-free diagnostic prints to stderr while the server keeps running — open the printed startup URL yourself.
|
||||
|
||||
**Settings → Models** displays **DeepSeek**, using `DEEPSEEK_API_KEY`. The default is `deepseek-official` / `deepseek-flash` (DeepSeek-V41-Flash). The [DeepSeek plugin](../../llm/llm-deepseek/README.md#choose-a-protocol) defaults to Chat Completions; set `protocol: messages` in Cordis YAML to switch. Web has no protocol selector.
|
||||
|
||||
Saved model selections override the composition default. Both protocols share `deepseek-official` and `llm-deepseek` settings, so switching preserves model selections and credential references. Endpoint overrides retain their values; the settings card lets users supply a compatible API address.
|
||||
|
||||
### Configuration
|
||||
|
||||
Most users never set these; the command-line flags feed the four settings below — `--host`, `--port`, and `--trusted-host` come from the invocation, and `--no-open` turns the browser handoff off for that invocation:
|
||||
@@ -77,7 +81,7 @@ A patch replaces the targeted row's whole `config`, so each web row restates eve
|
||||
|
||||
### Readiness
|
||||
|
||||
The URL line and browser handoff are readiness signals: supervisors RPC as soon as they observe the line, and a browser requests the page as soon as it opens, so both run only after the Loader tree settles and Connection authentication is available — or immediately in a hand-built tree without a Loader. A tree disposed mid-boot announces nothing.
|
||||
The URL line and browser handoff are readiness signals: supervisors RPC as soon as they observe the line, and a browser requests the page as soon as it opens, so both run only after the Loader tree settles, the required-startup audit passes, and Connection authentication is available — or immediately in a hand-built tree without a Loader. Optional plugin failures do not suppress readiness; a required startup failure or a tree disposed mid-boot announces nothing.
|
||||
|
||||
### LAN trust sampling
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@ dsh --profile web --no-open --port 8080
|
||||
|
||||
启动后你会看到 `dsh web:` 行,其根 URL 携带新的进程 token。除非 `--no-open` 或 SSH 会话抑制,否则默认浏览器会打开该 URL、取得签名 cookie,再重定向到不含认证参数的根页面。页面加载且你可以与 agent 对话,就说明成功了。两种可预期的失败:前端未构建时,启动会以构建提示停止(checkout 中运行 `pnpm run build`);浏览器无法打开时,stderr 会打印不含凭据的诊断,但服务器会继续运行——请自行打开已打印的启动 URL。
|
||||
|
||||
**设置 → 模型**显示 **DeepSeek**,使用 `DEEPSEEK_API_KEY`。默认模型为 `deepseek-official` / `deepseek-flash`(DeepSeek-V41-Flash)。[DeepSeek 插件](../../llm/llm-deepseek/README.zh.md#choose-a-protocol)默认使用 Chat Completions;在 Cordis YAML 中设置 `protocol: messages` 可切换协议。Web 不提供协议选择器。
|
||||
|
||||
已保存的模型选择优先于组合默认值。两种协议共用 `deepseek-official` 与 `llm-deepseek` 设置,因此切换协议不改变模型选择或复制凭据。端点覆盖保持原值;设置卡片允许用户填写兼容的 API 地址。
|
||||
|
||||
### 配置
|
||||
|
||||
大多数用户不需要设置这些;命令行 flag 会提供给下面四个设置——`--host`、`--port` 与 `--trusted-host` 来自本次调用,`--no-open` 仅对本次调用关闭浏览器交接:
|
||||
@@ -77,7 +81,7 @@ patch 会替换目标行的整个 `config`,因此每个 Web 行都重述自己
|
||||
|
||||
### 就绪宣告
|
||||
|
||||
URL 行与浏览器交接都是就绪信号:监督方一观察到该行就发起 RPC,浏览器一打开就请求页面,因此两者只在 Loader 配置树结算且 Connection 认证可用后运行——在没有 Loader 的手工构建树中则立即运行。启动中途被释放的树不会宣告任何内容。
|
||||
URL 行与浏览器交接都是就绪信号:监督方一观察到该行就发起 RPC,浏览器一打开就请求页面,因此两者只在 Loader 配置树结算、通过 required 启动检查且 Connection 认证可用后运行——在没有 Loader 的手工构建树中则立即运行。可选插件失败不会阻止就绪宣告;required 启动失败或启动中途被释放的树不会宣告任何内容。
|
||||
|
||||
### LAN 信任采样
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { networkInterfaces } from 'node:os'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import { addHarnessSourceSection } from '@deepseek-ai/dsh-app-boot'
|
||||
import { addHarnessSourceSection, auditStartupEntries } from '@deepseek-ai/dsh-app-boot'
|
||||
import type {} from '@deepseek-ai/dsh-client-connection'
|
||||
import * as FrontendStatic from '@deepseek-ai/dsh-host-frontend-static'
|
||||
import { launchedThroughSsh, launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
|
||||
@@ -284,15 +284,17 @@ export function apply(ctx: Context, config: Config): void {
|
||||
const settled = connectionCtx.get('loader')?.await()
|
||||
if (settled === undefined) announceReady()
|
||||
else {
|
||||
void settled.then(() => {
|
||||
void settled.then(async () => {
|
||||
await auditStartupEntries(connectionCtx.root, 'dsh web', () => {})
|
||||
// The tree can be disposed while the boot was in flight (early
|
||||
// SIGTERM); a URL line or browser tab for a dead server would only
|
||||
// mislead, and reading torn-down services would turn a clean shutdown
|
||||
// into a crash.
|
||||
if (connectionCtx.get('webServer') !== undefined
|
||||
&& connectionCtx.get('connection') !== undefined) announceReady()
|
||||
// Loader reports a failed boot; this row only stays quiet.
|
||||
}, () => {})
|
||||
}).catch(() => {
|
||||
// Boot owns the failure diagnostic; readiness remains unpublished.
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,8 +11,10 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { PassThrough } from 'node:stream'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
||||
import * as AppBoot from '@deepseek-ai/dsh-app-boot'
|
||||
import { createLaunchEnvironmentSnapshot, DSH_LAUNCH_ENVIRONMENT_KEY } from '@deepseek-ai/dsh-launch-environment'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { WebServer } from '@deepseek-ai/dsh-host-webserver'
|
||||
@@ -100,7 +102,7 @@ function provideConnection(ctx: Context): void {
|
||||
|
||||
/** A fake Loader whose settlement the test controls (the URL line waits on it). */
|
||||
function provideLoader(ctx: Context, settle: () => Promise<void> = async () => {}): void {
|
||||
ctx.provide('loader', { await: settle } as never)
|
||||
ctx.provide('loader', { await: settle, entries: () => [] } as never)
|
||||
}
|
||||
|
||||
interface BashContribution {
|
||||
@@ -313,6 +315,39 @@ describe('web-app runtime glue', () => {
|
||||
await torn.fiber.dispose()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ id: 'webserver', announces: false },
|
||||
{ id: 'modules', announces: false },
|
||||
{ id: 'connection', announces: false },
|
||||
{ id: 'optional-tool', announces: true },
|
||||
])('announces readiness=$announces after the $id sibling fails', async ({ id, announces }) => {
|
||||
stageDist()
|
||||
const ctx = new Context()
|
||||
onTestFinished(() => ctx.fiber.dispose())
|
||||
ctx.baseUrl = 'file:///'
|
||||
ctx.provide('webServer', fakeHttpServer().server)
|
||||
provideConnection(ctx)
|
||||
await ctx.plugin(Loader)
|
||||
ctx.loader.builtins.failure = () => { throw new Error('sibling rejected') }
|
||||
await ctx.loader.root.update([{ id, name: 'cordis:failure' }])
|
||||
await ctx.loader.await()
|
||||
await expect(ctx.loader.resolve(id).fiber?.await()).rejects.toThrow('sibling rejected')
|
||||
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
const openBrowser = vi.fn(async () => {})
|
||||
internals.openBrowser = openBrowser
|
||||
const audit = vi.spyOn(AppBoot, 'auditStartupEntries')
|
||||
apply(ctx, new Config({ openBrowser: true, printUrl: true, surfaceContext: false, trustedHosts: [] }))
|
||||
await vi.waitFor(() => { expect(audit).toHaveBeenCalledOnce() })
|
||||
await Promise.allSettled(audit.mock.results.map(result => result.value as Promise<void>))
|
||||
if (announces) {
|
||||
expect(log).toHaveBeenCalledWith('dsh web: http://127.0.0.1:4567/?token=test-token')
|
||||
expect(openBrowser).toHaveBeenCalledWith('http://127.0.0.1:4567/?token=test-token')
|
||||
} else {
|
||||
expect(log).not.toHaveBeenCalled()
|
||||
expect(openBrowser).not.toHaveBeenCalled()
|
||||
}
|
||||
})
|
||||
|
||||
it('fails loud when the prompt section resolves against a portless webserver', async () => {
|
||||
stageDist()
|
||||
const ctx = new Context()
|
||||
|
||||
Reference in new issue
Block a user