merge: update the MCP manager with master

This commit is contained in:
gengruilin committed 2026-10-20 15:45:33 +08:00
commit 4e796f736e
569 files changed
+13876 -2216

No files matched your search

+3
View File
@@ -40,6 +40,8 @@ flowchart LR
cfg --> plugin_dsh_base_user_questions
plugin_dsh_base_agent["agent<br/>@deepseek-ai/dsh-agent"]
cfg --> plugin_dsh_base_agent
plugin_dsh_base_agent_controller["agent-controller<br/>@deepseek-ai/dsh-agent-controller"]
cfg --> plugin_dsh_base_agent_controller
plugin_dsh_base_plugin_package_inventory_deepseek["plugin-package-inventory-deepseek<br/>@deepseek-ai/dsh-plugin-package-inventory-deepseek"]
cfg --> plugin_dsh_base_plugin_package_inventory_deepseek
plugin_dsh_base_agent_default_model["agent-default-model<br/>@deepseek-ai/dsh-agent-default-model"]
@@ -218,6 +220,7 @@ flowchart LR
| `session-title-llm` | `@deepseek-ai/dsh-session-title-first-prompt-llm` |
| `user-questions` | `@deepseek-ai/dsh-user-questions` |
| `agent` | `@deepseek-ai/dsh-agent` |
| `agent-controller` | `@deepseek-ai/dsh-agent-controller` |
| `plugin-package-inventory-deepseek` | `@deepseek-ai/dsh-plugin-package-inventory-deepseek` |
| `agent-default-model` | `@deepseek-ai/dsh-agent-default-model` |
| `jobs` | `@deepseek-ai/dsh-jobs-local` |
+2 -2
View File
@@ -24,8 +24,8 @@
en: 850434e8ca162221
zh: 4893613f97a21eb8
/dsh-cli-behavior-reference/shared-deployment-behavior:
en: 6a7abc9339e8f37f
zh: 133adb834c8e195d
en: f40edcf903a51f3f
zh: 8a9a915451047c8b
/dsh-cli-behavior-reference/source-execution:
en: 05c2b23790f7b296
zh: 51f809020fb230a1
+1 -1
View File
@@ -134,7 +134,7 @@ New sessions in base-backed profiles default to the `workspace-write` permission
## Shared deployment behavior
The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search` and `web_fetch`, the public-only HTTP fetch provider, default-on DeepSeek session-log upload, and feedback-gated OTel upload for all users. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`. Enabled fetch calls run in every sandbox and approval mode without per-call confirmation; the provider rejects non-public destinations before connecting. The Web app disables the base tool row and exposes the same tools through its `cordis`, `ptc`, and `standard` agent presets.
The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search` and `web_fetch`, the HTTP fetch provider, default-on DeepSeek session-log upload, and feedback-gated OTel upload for all users. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`. Enabled fetch calls run in every sandbox and approval mode without per-call confirmation and can reach loopback and private-network hosts. The Web app disables the base tool row and exposes the same tools through its `cordis`, `ptc`, and `standard` agent presets.
Feedback is recorded in the Session log without starting model work. The [DeepSeek session-log contributor](../../../packages/session/session-log-deepseek/README.md) sends complete unaccepted log suffixes with subsequent DeepSeek requests by default, including requests sent through configured gateways; turn off **Settings → General → Upload Session Log when using the official model API** in Web, or set its `enabled` configuration to `false` to opt out. [OTel session upload](../../../packages/session/session-telemetry-otel/README.md) applies to all users and providers, including `deepseek-official`, without requiring a request header. The base defaults to `FEEDBACK_ONLY`: new own text feedback, message ratings, edits, and withdrawals release the complete canonical prefix through that event, including stored context; later records wait for the next explicit feedback. Inherited parent feedback does not authorize a fork. Requests, restoration, mount, and HMR do not trigger capture. SDK batching may finish an authorized upload without further interaction or model work. `DSH_TELEMETRY_MODE=DISABLED` disables OTel delivery; `FULL` is rejected, and any non-empty `DSH_TELEMETRY_DISABLED` disables its row. `DSH_TELEMETRY_OTLP_URL` selects the collector. Handoff is best-effort, not collector acceptance; no durable outbox or retry guarantee is provided. These OTel settings do not enable or disable the DeepSeek contribution. Neither path changes model input, but exports can include message text, tool arguments and results, and workspace paths.
+1 -1
View File
@@ -136,7 +136,7 @@ dsh web --help
## 共享部署行为
基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search` 和 `web_fetch`、仅限公网的 HTTP fetch 提供方,默认开启的 DeepSeek 会话日志上传,以及面向所有用户的反馈门控 OTel 上传。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`。已启用的抓取调用会在所有 sandbox 与审批模式下执行,无需逐次确认;提供方会在连接前拒绝非公开目的地址。Web app 会禁用 base 工具配置项,再通过 `cordis`、`ptc` 与 `standard` agent preset 暴露相同工具。
基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search` 和 `web_fetch`、HTTP fetch 提供方,默认开启的 DeepSeek 会话日志上传,以及面向所有用户的反馈门控 OTel 上传。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`。已启用的抓取调用会在所有 sandbox 与审批模式下执行,无需逐次确认,并且可以访问 loopback 与内网主机。Web app 会禁用 base 工具配置项,再通过 `cordis`、`ptc` 与 `standard` agent preset 暴露相同工具。
反馈记录在会话日志中,不会启动模型工作。[DeepSeek 会话日志贡献器](../../../packages/session/session-log-deepseek/README.zh.md)默认随后续 DeepSeek 请求发送尚未确认接收的完整日志后缀,包括经已配置网关发送的请求;可在 Web 的**设置 → 通用 → 在使用官方模型 API 时上传 Session Log**中关闭上传,或将其 `enabled` 配置设为 `false`。[OTel 会话上传](../../../packages/session/session-telemetry-otel/README.zh.md)适用于所有用户和提供方,包括 `deepseek-official`,无需请求头。基础配置默认使用 `FEEDBACK_ONLY`:新的自身文本反馈、消息评分、编辑与撤回会释放截至该事件的完整规范日志前缀,包含存储的上下文;后续记录等待下一次显式反馈。继承的父级反馈不构成 fork 的授权。请求、恢复、挂载和 HMR 不触发捕获。SDK 批处理可完成已授权上传,无需进一步交互或模型工作。`DSH_TELEMETRY_MODE=DISABLED` 禁止 OTel 投递;`FULL` 被拒绝,任何非空的 `DSH_TELEMETRY_DISABLED` 都会禁用其配置行。`DSH_TELEMETRY_OTLP_URL` 选择采集端。交接尽力而为,不代表采集端接受;不提供持久化 outbox 或重试保证。这些 OTel 设置不会开启或关闭 DeepSeek 贡献。两条路径都不改变模型输入,但导出可能包含消息文本、工具参数和结果,以及工作区路径。
@@ -0,0 +1,10 @@
/** Frame one text or tool response from the local Messages endpoint. */
export function messagesResponse(content: Record<string, unknown>, stopReason: 'end_turn' | 'max_tokens' | 'tool_use'): string {
return [
{ type: 'message_start', message: { id: 'sdk-smoke-response', model: 'deepseek-v4-pro', usage: { input_tokens: 3, output_tokens: 0 } } },
{ type: 'content_block_start', index: 0, content_block: content },
{ type: 'content_block_stop', index: 0 },
{ type: 'message_delta', delta: { stop_reason: stopReason }, usage: { output_tokens: 1 } },
{ type: 'message_stop' },
].map(event => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join('')
}
@@ -0,0 +1,10 @@
/** Freeze only the isolated runtime's wall clock before reminder activation starts. */
export function apply(ctx) {
const original = Date.now
const now = Number(process.env.DSH_TEST_SCHEDULE_NOW)
ctx.effect(() => {
Date.now = () => now
return () => { Date.now = original }
})
ctx.provide('sdkTestClock', { ready: true })
}
@@ -10,6 +10,8 @@ import { execa } from 'execa'
import { describe, expect, it, onTestFinished } from 'vitest'
import { workspaceDependencyPaths, type PrimaryRuntimeManifest } from '@deepseek-ai/dsh-tool-workspace-dependencies'
import { messagesResponse } from './fixtures/messages-response.ts'
const repoRoot = fileURLToPath(new URL('../../../../../', import.meta.url))
const launch = resolveExampleLaunch({
srcBin: fileURLToPath(new URL('../../../src/bin.ts', import.meta.url)),
@@ -17,16 +19,6 @@ const launch = resolveExampleLaunch({
})
const decompress = promisify(zstdDecompress)
/** Frame one text or tool response from the local Messages endpoint. */
function messagesResponse(content: Record<string, unknown>, stopReason: 'end_turn' | 'max_tokens' | 'tool_use'): string {
return [
{ type: 'message_start', message: { id: 'sdk-smoke-response', model: 'deepseek-v4-pro', usage: { input_tokens: 3, output_tokens: 0 } } },
{ type: 'content_block_start', index: 0, content_block: content },
{ type: 'content_block_stop', index: 0 },
{ type: 'message_delta', delta: { stop_reason: stopReason }, usage: { output_tokens: 1 } },
{ type: 'message_stop' },
].map(event => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join('')
}
function waitForLine(
lines: string[],
@@ -66,7 +58,11 @@ describe('Python SDK dsh profile keyless smoke', () => {
{ label: 'reports max-token turns with mapping enabled through env', envValue: 'true', editorEnabled: false },
{ label: 'reports max-token turns with mapping disabled through env', envValue: 'false', editorEnabled: false },
{ label: 'allows an explicit patch to enable str_replace_editor', envValue: undefined, editorEnabled: true },
])('$label', async ({ envValue, editorEnabled }) => {
{ label: 'uses desktop tools through the PTC environment switch', envValue: undefined, editorEnabled: false, ptc: true },
])('$label', async (scenario) => {
const { envValue, editorEnabled } = scenario
const ptc = 'ptc' in scenario && scenario.ptc
const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-runtime-smoke-'))
const editorPatch = join(root, 'editor.patch.yml')
if (editorEnabled) await writeFile(editorPatch, [
@@ -103,6 +99,7 @@ describe('Python SDK dsh profile keyless smoke', () => {
DSH_HOME: join(root, '.dsh'),
DSH_PERMISSION_MODE: 'danger-full-access',
DSH_TELEMETRY_DISABLED: '1',
DSH_TOOLS_MODE: ptc ? 'ptc' : 'native',
DEEPSEEK_API_KEY: 'keyless-smoke-no-call',
DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`,
...(envValue === undefined ? {} : { DSH_MAX_TOKENS_AS_SUCCESS: envValue }),
@@ -176,9 +173,23 @@ describe('Python SDK dsh profile keyless smoke', () => {
const toolNames = tools.map(tool => tool.name)
expect(modelRequests[0]?.output_config).toEqual({ effort: 'max' })
expect(modelRequests[0]?.max_tokens).toBe(1234)
expect(toolNames).toEqual(expect.arrayContaining(['read', 'write', 'edit', 'web_fetch', 'web_search']))
expect(toolNames.includes('str_replace_editor')).toBe(editorEnabled)
expect(toolNames).not.toContain('list_subagent_models')
const expectedTools = ['read', 'write', 'edit', 'web_fetch', 'web_search', 'subagent', 'subagent_fork',
'present', 'ask_user_question', 'schedule_create', 'schedule_delete', 'schedule_list', 'schedule_update']
const requestText = JSON.stringify(modelRequests[0])
if (ptc) {
expect(toolNames).toEqual(['run_code'])
for (const tool of expectedTools) expect(requestText.includes(`${tool}:`), tool).toBe(true)
} else {
expect(toolNames).toEqual(expect.arrayContaining(expectedTools))
expect(toolNames.includes('str_replace_editor')).toBe(editorEnabled)
expect(toolNames).not.toContain('list_subagent_models')
}
for (const text of ['Tokens prefixed with @', 'Prefer showing the primary results',
'Time sampled while preparing']) {
expect(requestText.includes(text), text).toBe(true)
}
expect(requestText).not.toContain('Current DeepSeek Harness implementation checkout:')
expect(requestText).not.toContain('Current DeepSeek Harness Web GUI URL:')
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`)
const shutdown = await waitForLine(lines, value => value.id === 3, () => stderr)
@@ -0,0 +1,72 @@
/** Reminder activation must share SDK ownership and initialized model limits after process restart. */
import { createServer } from 'node:http'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
import { clearAmbientProxyEnv } from '../../../../../scripts/test-proxy-environment.ts'
import { expect, it, onTestFinished, vi } from 'vitest'
import { messagesResponse } from './fixtures/messages-response.ts'
it('delivers an overdue reminder with the SDK route and lets the client resume that same Session', async () => {
const root = await mkdtemp(join(tmpdir(), 'sdk-schedule-resume-'))
const patch = join(root, 'clock.patch.yml')
const fixture = fileURLToPath(new URL('./fixtures/schedule-clock.mjs', import.meta.url))
await writeFile(patch, [
'- insert:', ' - id: sdk-test-clock', ` name: ${JSON.stringify(fixture)}`,
'- id: sdk-jsonrpc-server', ' inject: [sdkAppStartup, loader, sdkTestClock]', '',
].join('\n'))
const requests: Record<string, unknown>[] = []
const restored: Record<string, unknown>[] = []
let restarting = false
const server = createServer((incoming, response) => {
let body = ''
incoming.setEncoding('utf8')
incoming.on('data', (chunk: string) => { body += chunk })
incoming.on('end', () => {
const request = JSON.parse(body) as Record<string, unknown>
requests.push(request)
if (restarting) restored.push(request)
const content = requests.length === 1 ? {
type: 'tool_use', id: 'create-reminder', name: 'schedule_create',
input: { title: 'Check SDK', prompt: 'Report the SDK reminder', after_seconds: 86400 },
} : { type: 'text', text: 'ready' }
response.writeHead(200, { 'content-type': 'text/event-stream' })
response.end(messagesResponse(content, requests.length === 1 ? 'tool_use' : 'end_turn'))
})
})
const clients: DeepSeekHarness[] = []
onTestFinished(async () => {
try { await Promise.all(clients.map(client => client.close())) } finally {
try {
await new Promise<void>((resolve, reject) => {
server.close((error) => { if (error === undefined) resolve(); else reject(error) })
})
} finally { await rm(root, { recursive: true, force: true }) }
}
})
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
const address = server.address()
if (address === null || typeof address === 'string') throw new Error('model fixture has no port')
const env = { ...process.env, DSH_TOOLS_MODE: 'native', DSH_PERMISSION_MODE: 'danger-full-access', DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: 'keyless-schedule-fixture', DEEPSEEK_BASE_URL: `http://127.0.0.1:${String(address.port)}` }
clearAmbientProxyEnv(env)
const first = new DeepSeekHarness({ cwd: root, dshHome: join(root, '.dsh'), patches: [patch],
model: 'deepseek-v4-pro', maxTokens: 5678, initializeTimeoutMs: 30_000,
env: { ...env, DSH_TEST_SCHEDULE_NOW: String(Date.parse('2030-01-01T00:00:00Z')) } })
clients.push(first)
expect((await first.session('saved').run('Set a reminder')).finalResponse).toBe('ready')
expect(requests).toHaveLength(2)
await first.close()
restarting = true
const second = new DeepSeekHarness({ cwd: root, dshHome: join(root, '.dsh'), patches: [patch],
model: 'deepseek-v4-flash', maxTokens: 1234, initializeTimeoutMs: 30_000,
env: { ...env, DSH_TEST_SCHEDULE_NOW: String(Date.parse('2030-01-03T00:00:00Z')) } })
clients.push(second)
await second.start()
await vi.waitFor(() => { expect(restored).toHaveLength(1) }, { timeout: 30_000 })
expect(restored[0]).toMatchObject({ model: 'deepseek-v4-flash', max_tokens: 1234 })
const session = await second.resumeSession('saved')
expect((await session.run('Continue after the reminder')).finalResponse).toBe('ready')
})
@@ -0,0 +1,84 @@
/** Keyless SDK questions through the shipped profile, native tools, and both tool-call modes. */
import { createServer } from 'node:http'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { DeepSeekHarness, type SdkUserQuestionRequest } from '@deepseek-ai/dsh-sdk-client'
import { clearAmbientProxyEnv } from '../../../../../scripts/test-proxy-environment.ts'
import { expect, it, onTestFinished } from 'vitest'
import { messagesResponse } from './fixtures/messages-response.ts'
it.each([
{ mode: 'native', cancel: false }, { mode: 'ptc', cancel: false }, { mode: 'native', cancel: true },
])('handles a built-in question through $mode (cancel: $cancel)', async ({ mode, cancel }) => {
const root = await mkdtemp(join(tmpdir(), 'sdk-question-profile-'))
const requests: Record<string, unknown>[] = []
const questions = [{ id: 'format', question: 'Which format?', options: [{ label: 'CSV' }, { label: 'JSON' }] }]
const server = createServer((incoming, response) => {
let body = ''
incoming.setEncoding('utf8')
incoming.on('data', (chunk: string) => { body += chunk })
incoming.on('end', () => {
requests.push(JSON.parse(body) as Record<string, unknown>)
const tool = mode === 'ptc' ? {
type: 'tool_use', id: 'question-call', name: 'run_code', input: {
code: `return await tools.ask_user_question(${JSON.stringify({ questions })});`, description: 'Ask which output format to use',
},
} : { type: 'tool_use', id: 'question-call', name: 'ask_user_question', input: { questions } }
response.writeHead(200, { 'content-type': 'text/event-stream' })
response.end(messagesResponse(requests.length === 1 ? tool : { type: 'text', text: 'answer received' },
requests.length === 1 ? 'tool_use' : 'end_turn'))
})
})
const state: { harness?: DeepSeekHarness } = {}
onTestFinished(async () => {
try { await state.harness?.close() } finally {
try {
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error === undefined) resolve()
else reject(error)
})
})
}
finally { await rm(root, { recursive: true, force: true }) }
}
})
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
const address = server.address()
if (address === null || typeof address === 'string') throw new Error('model fixture has no port')
const env = { ...process.env, DSH_TOOLS_MODE: mode, DSH_PERMISSION_MODE: 'danger-full-access', DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: 'keyless-question-fixture', DEEPSEEK_BASE_URL: `http://127.0.0.1:${String(address.port)}` }
clearAmbientProxyEnv(env)
const seen: SdkUserQuestionRequest[] = []
const entered = Promise.withResolvers<AbortSignal>()
const harness = state.harness = new DeepSeekHarness({
cwd: root, dshHome: join(root, '.dsh'), env, model: 'deepseek-v4-pro', maxTokens: 1234, initializeTimeoutMs: 30_000,
onUserQuestion: async (request, { signal }) => {
expect(signal.aborted).toBe(false)
seen.push(request)
entered.resolve(signal)
if (cancel) return new Promise((_resolve, reject) => {
signal.addEventListener('abort', () => { reject(new Error('question cancelled')) }, { once: true })
})
return { answers: [{ id: 'format', selected: ['CSV'] }] }
},
})
const pending = harness.session('question-session').run('Ask which output format to use')
if (cancel) {
const settled = pending.catch(() => undefined)
const signal = await entered.promise
await harness.close()
await settled
expect(signal.aborted).toBe(true)
expect(requests).toHaveLength(1)
return
}
const result = await pending
expect(seen).toEqual([{ requestId: expect.any(String) as string, sessionId: 'question-session', questions }])
expect(result.finalResponse).toBe('answer received')
expect(requests).toHaveLength(2)
expect(requests[1]?.max_tokens).toBe(1234)
expect(JSON.stringify(requests[1]?.messages)).toContain('CSV')
expect(result.events.some(event => event.type === 'tool/result' && event.data.error !== undefined)).toBe(false)
})