diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e0e635d..13acb1d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and [中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md) +## [2.8.3] - 2026-11-06 + +### Added + +- **Model-based web search**: `bl search web` adds a model search mode and now uses it as the default implementation. +- **Web search in chat**: `bl text chat` supports built-in web search via the `enable_search` parameter. + +### Changed + +- **Profile credentials**: Profiles can now declare credentials per search mode (`model` / `mcp`), and the CLI picks the matching profile credential when switching search modes. + ## [2.8.2] - 2026-11-04 ### Internal diff --git a/CHANGELOG.zh.md b/CHANGELOG.zh.md index 23d4e16e..d34887a8 100644 --- a/CHANGELOG.zh.md +++ b/CHANGELOG.zh.md @@ -6,6 +6,17 @@ [English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md) +## [2.8.3] - 2026-11-06 + +### 新增 + +- **模型联网搜索**:`bl search web` 新增模型搜索模式(`--mode model`),并改为默认实现。 +- **对话内置联网搜索**:`bl text chat` 新增 `enable_search` 参数,支持内置联网搜索。 + +### 变更 + +- **Profile 凭证**:Profile 可按搜索模式(`model` / `mcp`)声明凭证,切换搜索模式时自动选择对应 Profile 凭证。 + ## [2.8.2] - 2026-11-04 ### 内部维护 diff --git a/packages/bailian-kb-dsh/package.json b/packages/bailian-kb-dsh/package.json index 10d54760..7e1b1734 100644 --- a/packages/bailian-kb-dsh/package.json +++ b/packages/bailian-kb-dsh/package.json @@ -1,6 +1,6 @@ { "name": "bailian-kb-dsh", - "version": "2.8.2", + "version": "2.8.3", "description": "Bailian knowledge-base tools for DeepSeek Harness: kb_search and kb_chat over the DashScope RAG API, plus the bl CLI management skill.", "keywords": [ "alibaba-cloud", diff --git a/packages/bailian-memo-dsh/package.json b/packages/bailian-memo-dsh/package.json index 2c72f8a5..63e538fa 100644 --- a/packages/bailian-memo-dsh/package.json +++ b/packages/bailian-memo-dsh/package.json @@ -1,6 +1,6 @@ { "name": "bailian-memo-dsh", - "version": "2.8.2", + "version": "2.8.3", "description": "Bailian personal memory for DeepSeek Harness: automatic recall, silent curation, and user-controlled remember/forget.", "keywords": [ "alibaba-cloud", diff --git a/packages/cli/package.json b/packages/cli/package.json index 6af93671..df104826 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli", - "version": "2.8.2", + "version": "2.8.3", "description": "CLI for Aliyun Model Studio (DashScope) AI Platform.", "keywords": [ "agent", diff --git a/packages/commands/package.json b/packages/commands/package.json index 5b405a40..48029c74 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-commands", - "version": "2.8.2", + "version": "2.8.3", "description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.", "homepage": "https://bailian.console.aliyun.com/cli", "bugs": { diff --git a/packages/commands/src/commands/search/web.ts b/packages/commands/src/commands/search/web.ts index 70083d48..9ec70f60 100644 --- a/packages/commands/src/commands/search/web.ts +++ b/packages/commands/src/commands/search/web.ts @@ -2,13 +2,32 @@ import { defineCommand, formatOutput, BailianError, + chatPath, mcpWebSearchPath, + type ChatRequest, + type ChatResponse, type FlagsDef, type McpConnectedClient, + type McpToolResult, } from "bailian-cli-core"; import { rethrowWithWebSearchActivateHint } from "./web-activate-hint.ts"; +/** Fallback model for --mode model, mirroring the text-chat default chain. */ +const DEFAULT_SEARCH_MODEL = "qwen3.8-max"; + +type WebSearchMode = "mcp" | "model"; + +/** + * Resolve the effective search mode. `--list-tools` is an MCP-only concept and + * locks the mode to mcp; otherwise default to model (Chat Completions) unless + * `--mode mcp` is given. + */ +function resolveSearchMode(flags: { mode?: string; listTools?: boolean }): WebSearchMode { + if (flags.listTools) return "mcp"; + return flags.mode === "mcp" ? "mcp" : "model"; +} + const WEB_SEARCH_FLAGS = { query: { type: "string", @@ -30,14 +49,39 @@ const WEB_SEARCH_FLAGS = { "zh-CN": "列出可用的 MCP 工具并退出", }, }, + mode: { + type: "string", + valueHint: "", + choices: ["mcp", "model"] as const, + description: { + "en-US": + "Search mode: model answers via Chat Completions with enable_search, mcp calls the WebSearch MCP service (default: model)", + "zh-CN": + "搜索模式:model 通过 Chat Completions(enable_search)由模型联网作答,mcp 调用 WebSearch MCP 服务(默认:model)", + }, + }, + model: { + type: "string", + valueHint: "", + description: { + "en-US": "Model ID for --mode model (default: qwen3.8-max)", + "zh-CN": "--mode model 使用的模型 ID(默认:qwen3.8-max)", + }, + }, } satisfies FlagsDef; export default defineCommand({ description: { - "en-US": "Search the web using DashScope MCP WebSearch service", - "zh-CN": "使用 DashScope MCP WebSearch 服务搜索互联网", + "en-US": + "Search the web via a chat model with built-in web search (default) or the DashScope MCP WebSearch service", + "zh-CN": "搜索互联网:默认走模型内置联网搜索,也可切换为 DashScope MCP WebSearch 服务", }, auth: "apiKey", + // Model mode talks Chat Completions (the `text.chat` capability domain), so + // Profile API-key selection must follow the mode; MCP mode keeps the + // `search.web` capability, which Token Plan profiles deliberately do not + // list — plan keys cannot authorize Bailian MCP search. + authCapability: (flags) => (resolveSearchMode(flags) === "model" ? "text.chat" : "search.web"), usageArgs: "--query [flags]", flags: WEB_SEARCH_FLAGS, exampleArgs: [ @@ -45,21 +89,79 @@ export default defineCommand({ "en-US": '--query "Alibaba Cloud Bailian latest features"', "zh-CN": '--query "阿里云百炼最新功能"', }, - { - "en-US": '--query "TypeScript 5.9 new features" --count 5', - "zh-CN": '--query "TypeScript 5.9 新功能" --count 5', - }, { "en-US": '--query "Today\'s news"', "zh-CN": '--query "今日新闻"', }, + { + "en-US": '--mode mcp --query "TypeScript 5.9 new features" --count 5', + "zh-CN": '--mode mcp --query "TypeScript 5.9 新功能" --count 5', + }, + { + "en-US": '--mode mcp --query "Alibaba Cloud Bailian latest features"', + "zh-CN": '--mode mcp --query "阿里云百炼最新功能"', + }, "--list-tools", ], - validate: (f) => (!f.listTools && !f.query ? "Missing required flag: --query" : undefined), + notes: [ + { + "en-US": + "Defaults to --mode model: one non-streaming Chat Completions request with enable_search: true, returning the model answer as a single content[].text block (no pages list). Use --mode mcp for the WebSearch MCP service (pages list); --count and --list-tools are MCP-mode only. The model resolves from --model, default_text_model, then qwen3.8-max.", + "zh-CN": + "默认 --mode model:发送单次非流式 Chat Completions 请求(enable_search: true),模型回答以单个 content[].text 块返回(无 pages 列表)。--mode mcp 走 WebSearch MCP 服务(返回 pages);--count 与 --list-tools 仅限 MCP 模式。模型按 --model、default_text_model、qwen3.8-max 依次解析。", + }, + ], + validate: (flags) => { + if (flags.mode === "model" && flags.listTools) { + return "--list-tools is only available in MCP mode (--mode mcp). / --list-tools 仅支持 MCP 模式(--mode mcp)。"; + } + if (resolveSearchMode(flags) === "model" && flags.count !== undefined) { + return "--count is only available in MCP mode (--mode mcp). / --count 仅支持 MCP 模式(--mode mcp)。"; + } + if (!flags.listTools && !flags.query) return "Missing required flag: --query"; + return undefined; + }, async run(ctx) { const { settings, flags } = ctx; const format = settings.output; + // --- Model mode (default): Chat Completions with enable_search --- + if (resolveSearchMode(flags) === "model") { + const model = flags.model || settings.defaultTextModel || DEFAULT_SEARCH_MODEL; + const request: ChatRequest = { + model, + messages: [{ role: "user", content: flags.query! }], + enable_search: true, + stream: false, + }; + + if (settings.dryRun) { + ctx.output.result({ endpoint: ctx.client.url(chatPath()), request }); + return; + } + + ctx.output.progress({ "en-US": "Searching...", "zh-CN": "正在搜索……" }); + try { + const response = await ctx.client.requestJson({ + path: chatPath(), + method: "POST", + body: request, + }); + const answer = response.choices?.[0]?.message?.content ?? ""; + ctx.output.progress({ "en-US": "Done.", "zh-CN": "已完成。" }); + // Keep the MCP result envelope so downstream parsers work for both modes. + const result: McpToolResult = { + content: [{ type: "text", text: answer }], + isError: false, + }; + ctx.output.result(result, format === "text" ? { text: answer } : undefined); + } catch (error) { + ctx.output.progress({ "en-US": "Failed.", "zh-CN": "失败。" }); + throw error; + } + return; + } + // --- List tools mode --- if (flags.listTools) { if (settings.dryRun) { diff --git a/packages/commands/src/commands/text/chat.ts b/packages/commands/src/commands/text/chat.ts index d4937c9c..457e6618 100644 --- a/packages/commands/src/commands/text/chat.ts +++ b/packages/commands/src/commands/text/chat.ts @@ -25,6 +25,9 @@ import { } from "./responses.ts"; import { PRIME_FLAGS, resolvePrimeEndpoint, validatePrimeFlags } from "../shared/prime.ts"; +/** Models that enable built-in web search by default on the Chat Completions API. */ +const SEARCH_BY_DEFAULT_MODELS = new Set(["qwen3.8-max"]); + const CHAT_FLAGS = { api: { type: "string", @@ -79,6 +82,15 @@ const CHAT_FLAGS = { valueHint: "", description: { "en-US": "Nucleus sampling threshold", "zh-CN": "核采样阈值" }, }, + enableSearch: { + type: "boolean", + valueHint: "", + description: { + "en-US": + "Enable built-in web search (Chat Completions API only; default: true for qwen3.8-max)", + "zh-CN": "启用模型内置联网搜索(仅 Chat Completions API;qwen3.8-max 默认开启)", + }, + }, stream: { type: "switch", description: { @@ -202,6 +214,10 @@ export default defineCommand({ "en-US": '--model qwq-plus --message "Solve 1+1" --enable-thinking', "zh-CN": '--model qwq-plus --message "计算 1+1" --enable-thinking', }, + { + "en-US": '--enable-search true --message "Latest news about Alibaba Cloud"', + "zh-CN": '--enable-search true --message "阿里云最新新闻"', + }, { "en-US": '--prime --workspace-id --model glm-5.3-prime --message "Explain this code"', "zh-CN": '--prime --workspace-id --model glm-5.3-prime --message "解释这段代码"', @@ -229,6 +245,12 @@ export default defineCommand({ "zh-CN": "Prime Endpoint:已配置的 --base-url、DASHSCOPE_BASE_URL 或 Profile base_url 优先;否则按 --workspace-id、BAILIAN_WORKSPACE_ID、配置项 workspace_id 解析工作空间。", }, + { + "en-US": + '--enable-search maps to the Chat Completions enable_search parameter and requires a model with built-in web search support; whitelisted models (qwen3.8-max) enable it by default unless --enable-search false is passed; for the Responses API use --tool \'{"type":"web_search"}\' instead.', + "zh-CN": + '--enable-search 映射为 Chat Completions 的 enable_search 参数,需要模型支持内置联网搜索;白名单模型(qwen3.8-max)默认开启,可传 --enable-search false 显式关闭;Responses API 请改用 --tool \'{"type":"web_search"}\'。', + }, ], validate: (flags) => { if (!flags.message && !flags.messagesFile) return "Provide --message or --messages-file."; @@ -243,6 +265,9 @@ export default defineCommand({ if (flags.api === "responses" && flags.thinkingBudget !== undefined) { return "--thinking-budget is not supported by the Responses API."; } + if (flags.api === "responses" && flags.enableSearch !== undefined) { + return '--enable-search is not supported by the Responses API; use --tool \'{"type":"web_search"}\' instead. / --enable-search 不支持 Responses API,请改用 --tool \'{"type":"web_search"}\'。'; + } return undefined; }, async run(ctx) { @@ -295,6 +320,15 @@ export default defineCommand({ } } + if (api === "chat" && "messages" in body) { + if (flags.enableSearch !== undefined) { + body.enable_search = flags.enableSearch; + } else if (SEARCH_BY_DEFAULT_MODELS.has(model)) { + // Whitelisted models get built-in web search unless explicitly opted out. + body.enable_search = true; + } + } + const requestPath = flags.batch ? resolveBatchChatEndpoint(ctx.client) : flags.prime diff --git a/packages/commands/tests/e2e/search-web.e2e.test.ts b/packages/commands/tests/e2e/search-web.e2e.test.ts index c924e10e..c5c9ba83 100644 --- a/packages/commands/tests/e2e/search-web.e2e.test.ts +++ b/packages/commands/tests/e2e/search-web.e2e.test.ts @@ -1,5 +1,13 @@ +import { writeFileSync } from "node:fs"; +import { join } from "node:path"; import { describe, expect, test } from "vite-plus/test"; -import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts"; +import { + isDashScopeE2EReady, + makeE2eOutputDir, + parseStdoutJson, + runCommandHelp, + runCommandE2e, +} from "./helpers.ts"; import { SEARCH_WEB_ROUTES } from "./topic-routes.ts"; function pagesFromSearchWebStdout(stdout: string): Array<{ title?: string; url?: string }> { @@ -23,7 +31,7 @@ describe("e2e: search web", () => { "--help", ]); expect(exitCode, stderr).toBe(0); - expect(stderr).toMatch(/web|--query|list-tools|count/i); + expect(stderr).toMatch(/web|--query|list-tools|count|--mode/i); }); test("search web --dry-run --list-tools 无需 --query 也无需凭证即可干跑", async () => { @@ -39,6 +47,177 @@ describe("e2e: search web", () => { const data = parseStdoutJson<{ action?: string }>(stdout); expect(data.action).toBe("tools/list"); }); + + test("search web --mode model --dry-run 生成 enable_search 对话请求", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e( + SEARCH_WEB_ROUTES, + ["search", "web", "--mode", "model", "--dry-run", "--output", "json", "--query", "干跑校验"], + { + DASHSCOPE_API_KEY: undefined, + DASHSCOPE_ACCESS_TOKEN: undefined, + }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ + endpoint?: string; + request?: { + model?: string; + messages?: Array<{ role?: string; content?: string }>; + enable_search?: boolean; + stream?: boolean; + }; + }>(stdout); + expect(data.endpoint).toMatch(/\/chat\/completions$/); + expect(data.request?.model).toBe("qwen3.8-max"); + expect(data.request?.messages).toEqual([{ role: "user", content: "干跑校验" }]); + expect(data.request?.enable_search).toBe(true); + expect(data.request?.stream).toBe(false); + }); + + test("search web --mode model 可用 --model 指定模型", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e( + SEARCH_WEB_ROUTES, + [ + "search", + "web", + "--mode", + "model", + "--model", + "qwen-max", + "--dry-run", + "--output", + "json", + "--query", + "干跑校验", + ], + { + DASHSCOPE_API_KEY: undefined, + DASHSCOPE_ACCESS_TOKEN: undefined, + }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ request?: { model?: string } }>(stdout); + expect(data.request?.model).toBe("qwen-max"); + }); + + test("search web --mode model 拒绝 --list-tools 与 --count", async () => { + const listTools = await runCommandE2e(SEARCH_WEB_ROUTES, [ + "search", + "web", + "--mode", + "model", + "--list-tools", + ]); + expect(listTools.exitCode).toBe(2); + expect(listTools.stderr).toMatch(/--list-tools.*MCP/i); + + const count = await runCommandE2e(SEARCH_WEB_ROUTES, [ + "search", + "web", + "--mode", + "model", + "--query", + "hello", + "--count", + "3", + ]); + expect(count.exitCode).toBe(2); + expect(count.stderr).toMatch(/--count.*MCP/i); + }); + + test("search web 拒绝未知的 --mode 值", async () => { + const { stderr, exitCode } = await runCommandE2e(SEARCH_WEB_ROUTES, [ + "search", + "web", + "--mode", + "legacy", + "--query", + "hello", + ]); + expect(exitCode).toBe(2); + expect(stderr).toMatch(/--mode|mcp.*model/i); + }); + + test("search web 默认走 model:--dry-run 生成 enable_search 对话请求", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e( + SEARCH_WEB_ROUTES, + ["search", "web", "--dry-run", "--output", "json", "--query", "干跑校验"], + { + DASHSCOPE_API_KEY: undefined, + DASHSCOPE_ACCESS_TOKEN: undefined, + }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ + endpoint?: string; + request?: { model?: string; enable_search?: boolean }; + }>(stdout); + expect(data.endpoint).toMatch(/\/chat\/completions$/); + expect(data.request?.model).toBe("qwen3.8-max"); + expect(data.request?.enable_search).toBe(true); + }); + + test("search web 默认 model 模式拒绝 --count", async () => { + const { stderr, exitCode } = await runCommandE2e(SEARCH_WEB_ROUTES, [ + "search", + "web", + "--query", + "hello", + "--count", + "3", + ]); + expect(exitCode).toBe(2); + expect(stderr).toMatch(/--count.*MCP/i); + }); + + test("search web 按模式选择 Profile 凭证:model 保留 token-plan,mcp 回退 default", async () => { + const configDir = makeE2eOutputDir("search-web-mode-capability"); + writeFileSync( + join(configDir, "config.json"), + JSON.stringify( + { + api_key: "sk-default", + base_url: "https://default.example.com", + active_config: "token-plan", + "token-plan": { + api_key: "sk-token-plan", + base_url: "https://token-plan.example.com", + api_key_capabilities: ["text.chat"], + }, + }, + null, + 2, + ) + "\n", + ); + const env = { + BAILIAN_CONFIG_DIR: configDir, + DASHSCOPE_API_KEY: undefined, + DASHSCOPE_BASE_URL: undefined, + DASHSCOPE_ACCESS_TOKEN: undefined, + }; + + // Model mode authenticates as `text.chat`, which the token-plan Profile + // lists — its connection must survive capability selection. + const model = await runCommandE2e( + SEARCH_WEB_ROUTES, + ["search", "web", "--dry-run", "--output", "json", "--query", "凭证选择"], + env, + ); + expect(model.exitCode, model.stderr).toBe(0); + const modelData = parseStdoutJson<{ endpoint?: string }>(model.stdout); + expect(modelData.endpoint).toContain("https://token-plan.example.com"); + + // MCP mode keeps the `search.web` capability, absent from token-plan — + // the run must fall back to the default Profile connection. + const mcp = await runCommandE2e( + SEARCH_WEB_ROUTES, + ["search", "web", "--mode", "mcp", "--dry-run", "--output", "json", "--query", "凭证选择"], + env, + ); + expect(mcp.exitCode, mcp.stderr).toBe(0); + const mcpData = parseStdoutJson<{ endpoint?: string }>(mcp.stdout); + expect(mcpData.endpoint).toContain("https://default.example.com"); + }); }); describe.skipIf(!isDashScopeE2EReady())("e2e: search web", () => { @@ -52,10 +231,12 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: search web", () => { expect(stderr).toMatch(/--query|Usage:/i); }); - test("search web --dry-run 仅输出计划且不调 MCP", async () => { + test("search web --mode mcp --dry-run 仅输出 tools/call 计划", async () => { const { stdout, stderr, exitCode } = await runCommandE2e(SEARCH_WEB_ROUTES, [ "search", "web", + "--mode", + "mcp", "--dry-run", "--output", "json", @@ -76,10 +257,12 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: search web", () => { expect(data.arguments?.count).toBe(5); }); - test("联网搜索返回 JSON 且含搜索结果", async () => { + test("MCP 联网搜索返回 JSON 且含 pages", async () => { const { stdout, stderr, exitCode } = await runCommandE2e(SEARCH_WEB_ROUTES, [ "search", "web", + "--mode", + "mcp", "--query", "阿里云百炼", "--count", @@ -107,4 +290,23 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: search web", () => { const data = parseStdoutJson<{ tools?: Array<{ name?: string }> }>(stdout); expect(data.tools?.some((tool) => tool.name === "bailian_web_search")).toBe(true); }, 60_000); + + test("--mode model 真实调用模型联网搜索并返回 content 信封", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e(SEARCH_WEB_ROUTES, [ + "search", + "web", + "--mode", + "model", + "--query", + "阿里云百炼是什么?一句话回答", + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + // 模型模式无 pages;content[].text 直接承载模型回答纯文本(非 JSON)。 + const envelope = parseStdoutJson<{ content?: Array<{ type?: string; text?: string }> }>(stdout); + const text = envelope.content?.find((block) => block.type === "text")?.text ?? ""; + expect(text.length).toBeGreaterThan(0); + expect(() => JSON.parse(text)).toThrow(); + }, 120_000); }); diff --git a/packages/commands/tests/e2e/text-chat.e2e.test.ts b/packages/commands/tests/e2e/text-chat.e2e.test.ts index 1fbf95dc..f9050413 100644 --- a/packages/commands/tests/e2e/text-chat.e2e.test.ts +++ b/packages/commands/tests/e2e/text-chat.e2e.test.ts @@ -2,7 +2,13 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, test } from "vite-plus/test"; -import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts"; +import { + isDashScopeE2EReady, + parseStdoutJson, + runCommandHelp, + runCommandE2e, + runCommandInProcess, +} from "./helpers.ts"; import { TEXT_CHAT_ROUTES } from "./topic-routes.ts"; let isolatedDirectory: string; @@ -40,6 +46,7 @@ describe("e2e: text chat", () => { const { stderr, exitCode } = await runCommandHelp(TEXT_CHAT_ROUTES, ["text", "chat", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--api\s+/i); + expect(stderr).toMatch(/--enable-search/i); expect(stderr).toMatch(/--prime/i); expect(stderr).toMatch(/--workspace-id/i); expect(stderr).toMatch(/chat|--message|model|stream/i); @@ -73,6 +80,140 @@ describe("e2e: text chat", () => { expect(stderr).toMatch(/thinking-budget.*Responses/i); }); + test("text chat 在 Responses 模式拒绝 --enable-search", async () => { + const { stderr, exitCode } = await runCommandInProcess(TEXT_CHAT_ROUTES, [ + "text", + "chat", + "--api", + "responses", + "--message", + "hello", + "--enable-search", + "true", + ]); + expect(exitCode).toBe(2); + expect(stderr).toMatch(/enable-search.*Responses/i); + }); + + test("text chat --dry-run 透传 --enable-search true/false 到请求体", async () => { + const { stdout, stderr, exitCode } = await runCommandInProcess( + TEXT_CHAT_ROUTES, + [ + "text", + "chat", + "--dry-run", + "--enable-search", + "true", + "--message", + "hello", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "" }, + ); + expect(exitCode, stderr).toBe(0); + const enabled = parseStdoutJson<{ request?: { enable_search?: boolean } }>(stdout); + expect(enabled.request?.enable_search).toBe(true); + + const { + stdout: disabledStdout, + stderr: disabledStderr, + exitCode: disabledExit, + } = await runCommandInProcess( + TEXT_CHAT_ROUTES, + [ + "text", + "chat", + "--dry-run", + "--enable-search", + "false", + "--message", + "hello", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "" }, + ); + expect(disabledExit, disabledStderr).toBe(0); + const disabled = parseStdoutJson<{ request?: { enable_search?: boolean } }>(disabledStdout); + expect(disabled.request?.enable_search).toBe(false); + }); + + test("text chat --dry-run 未传 --enable-search 时请求体不含 enable_search", async () => { + const { stdout, stderr, exitCode } = await runCommandInProcess( + TEXT_CHAT_ROUTES, + [ + "text", + "chat", + "--dry-run", + "--model", + "qwen-max", + "--message", + "hello", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ request?: Record }>(stdout); + expect(data.request).not.toHaveProperty("enable_search"); + }); + + test("text chat --dry-run 白名单模型 qwen3.8-max 默认开启 enable_search", async () => { + // 未显式传 --model 时兼容默认模型也是 qwen3.8-max,同样命中白名单。 + const { stdout, stderr, exitCode } = await runCommandInProcess( + TEXT_CHAT_ROUTES, + [ + "text", + "chat", + "--dry-run", + "--model", + "qwen3.8-max", + "--message", + "hello", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ request?: { enable_search?: boolean } }>(stdout); + expect(data.request?.enable_search).toBe(true); + + const { stdout: implicitStdout, exitCode: implicitExit } = await runCommandInProcess( + TEXT_CHAT_ROUTES, + ["text", "chat", "--dry-run", "--message", "hello", "--output", "json"], + { DASHSCOPE_API_KEY: "" }, + ); + expect(implicitExit).toBe(0); + const implicit = parseStdoutJson<{ request?: { enable_search?: boolean } }>(implicitStdout); + expect(implicit.request?.enable_search).toBe(true); + }); + + test("text chat --dry-run 白名单模型可用 --enable-search false 显式关闭", async () => { + const { stdout, stderr, exitCode } = await runCommandInProcess( + TEXT_CHAT_ROUTES, + [ + "text", + "chat", + "--dry-run", + "--model", + "qwen3.8-max", + "--enable-search", + "false", + "--message", + "hello", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ request?: { enable_search?: boolean } }>(stdout); + expect(data.request?.enable_search).toBe(false); + }); + test("Prime 模式要求显式 --model", async () => { const { stderr, exitCode } = await runIsolatedTextChat( ["text", "chat", "--prime", "--message", "hello"], diff --git a/packages/commands/tests/output/fixtures/inference-output.json b/packages/commands/tests/output/fixtures/inference-output.json index ffbfe234..ced89ba8 100644 --- a/packages/commands/tests/output/fixtures/inference-output.json +++ b/packages/commands/tests/output/fixtures/inference-output.json @@ -1669,221 +1669,99 @@ ] }, "search:text:false": { - "stdout": "pages:\n - title: Fixture\n url: https://example.com/fixture\n snippet: fixture snippet\n hostname: example.com\ntotal: 1\ntext: extra result\n", + "stdout": "Hello fixture\n", "requests": [ { - "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", - "method": "POST", - "body": { - "jsonrpc": "2.0", - "id": 0, - "method": "initialize", - "params": { - "protocolVersion": "2025-11-25", - "capabilities": {}, - "clientInfo": { - "name": "test", - "version": "0.0.0" - } - } - } - }, - { - "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", - "method": "POST", - "body": { - "jsonrpc": "2.0", - "method": "notifications/initialized" - } - }, - { - "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", + "url": "https://fixture.invalid/compatible-mode/v1/chat/completions", "method": "POST", "body": { - "jsonrpc": "2.0", - "id": 1, - "method": "tools/call", - "params": { - "name": "bailian_web_search", - "arguments": { - "query": "fixture query", - "count": 2 + "model": "qwen3.8-max", + "messages": [ + { + "role": "user", + "content": "fixture query" } - } + ], + "enable_search": true, + "stream": false } } ] }, "search:json:false": { - "stdout": "{\n \"isError\": false,\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"{\\\"pages\\\":[{\\\"title\\\":\\\"Fixture\\\",\\\"url\\\":\\\"https://example.com/fixture\\\",\\\"snippet\\\":\\\"fixture snippet\\\",\\\"hostname\\\":\\\"example.com\\\"}]}\"\n },\n {\n \"type\": \"text\",\n \"text\": \"extra result\"\n }\n ],\n \"structuredContent\": {\n \"future\": {\n \"values\": [\n false,\n 0,\n null,\n []\n ]\n }\n }\n}\n", + "stdout": "{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"Hello fixture\"\n }\n ],\n \"isError\": false\n}\n", "data": { - "isError": false, "content": [ { "type": "text", - "text": "{\"pages\":[{\"title\":\"Fixture\",\"url\":\"https://example.com/fixture\",\"snippet\":\"fixture snippet\",\"hostname\":\"example.com\"}]}" - }, - { - "type": "text", - "text": "extra result" + "text": "Hello fixture" } ], - "structuredContent": { - "future": { - "values": [false, 0, null, []] - } - } + "isError": false }, "requests": [ { - "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", - "method": "POST", - "body": { - "jsonrpc": "2.0", - "id": 0, - "method": "initialize", - "params": { - "protocolVersion": "2025-11-25", - "capabilities": {}, - "clientInfo": { - "name": "test", - "version": "0.0.0" - } - } - } - }, - { - "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", - "method": "POST", - "body": { - "jsonrpc": "2.0", - "method": "notifications/initialized" - } - }, - { - "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", + "url": "https://fixture.invalid/compatible-mode/v1/chat/completions", "method": "POST", "body": { - "jsonrpc": "2.0", - "id": 1, - "method": "tools/call", - "params": { - "name": "bailian_web_search", - "arguments": { - "query": "fixture query", - "count": 2 + "model": "qwen3.8-max", + "messages": [ + { + "role": "user", + "content": "fixture query" } - } + ], + "enable_search": true, + "stream": false } } ] }, "search:text:true": { - "stdout": "pages:\n - title: Fixture\n url: https://example.com/fixture\n snippet: fixture snippet\n hostname: example.com\ntotal: 1\ntext: extra result\n", + "stdout": "Hello fixture\n", "requests": [ { - "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", - "method": "POST", - "body": { - "jsonrpc": "2.0", - "id": 0, - "method": "initialize", - "params": { - "protocolVersion": "2025-11-25", - "capabilities": {}, - "clientInfo": { - "name": "test", - "version": "0.0.0" - } - } - } - }, - { - "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", - "method": "POST", - "body": { - "jsonrpc": "2.0", - "method": "notifications/initialized" - } - }, - { - "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", + "url": "https://fixture.invalid/compatible-mode/v1/chat/completions", "method": "POST", "body": { - "jsonrpc": "2.0", - "id": 1, - "method": "tools/call", - "params": { - "name": "bailian_web_search", - "arguments": { - "query": "fixture query", - "count": 2 + "model": "qwen3.8-max", + "messages": [ + { + "role": "user", + "content": "fixture query" } - } + ], + "enable_search": true, + "stream": false } } ] }, "search:json:true": { - "stdout": "{\n \"isError\": false,\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"{\\\"pages\\\":[{\\\"title\\\":\\\"Fixture\\\",\\\"url\\\":\\\"https://example.com/fixture\\\",\\\"snippet\\\":\\\"fixture snippet\\\",\\\"hostname\\\":\\\"example.com\\\"}]}\"\n },\n {\n \"type\": \"text\",\n \"text\": \"extra result\"\n }\n ],\n \"structuredContent\": {\n \"future\": {\n \"values\": [\n false,\n 0,\n null,\n []\n ]\n }\n }\n}\n", + "stdout": "{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"Hello fixture\"\n }\n ],\n \"isError\": false\n}\n", "data": { - "isError": false, "content": [ { "type": "text", - "text": "{\"pages\":[{\"title\":\"Fixture\",\"url\":\"https://example.com/fixture\",\"snippet\":\"fixture snippet\",\"hostname\":\"example.com\"}]}" - }, - { - "type": "text", - "text": "extra result" + "text": "Hello fixture" } ], - "structuredContent": { - "future": { - "values": [false, 0, null, []] - } - } + "isError": false }, "requests": [ { - "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", - "method": "POST", - "body": { - "jsonrpc": "2.0", - "id": 0, - "method": "initialize", - "params": { - "protocolVersion": "2025-11-25", - "capabilities": {}, - "clientInfo": { - "name": "test", - "version": "0.0.0" - } - } - } - }, - { - "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", - "method": "POST", - "body": { - "jsonrpc": "2.0", - "method": "notifications/initialized" - } - }, - { - "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", + "url": "https://fixture.invalid/compatible-mode/v1/chat/completions", "method": "POST", "body": { - "jsonrpc": "2.0", - "id": 1, - "method": "tools/call", - "params": { - "name": "bailian_web_search", - "arguments": { - "query": "fixture query", - "count": 2 + "model": "qwen3.8-max", + "messages": [ + { + "role": "user", + "content": "fixture query" } - } + ], + "enable_search": true, + "stream": false } } ] @@ -1895,8 +1773,6 @@ "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", "method": "POST", "body": { - "jsonrpc": "2.0", - "id": 0, "method": "initialize", "params": { "protocolVersion": "2025-11-25", @@ -1905,24 +1781,26 @@ "name": "test", "version": "0.0.0" } - } + }, + "jsonrpc": "2.0", + "id": 0 } }, { "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", "method": "POST", "body": { - "jsonrpc": "2.0", - "method": "notifications/initialized" + "method": "notifications/initialized", + "jsonrpc": "2.0" } }, { "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", "method": "POST", "body": { + "method": "tools/list", "jsonrpc": "2.0", - "id": 1, - "method": "tools/list" + "id": 1 } } ] @@ -1945,8 +1823,6 @@ "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", "method": "POST", "body": { - "jsonrpc": "2.0", - "id": 0, "method": "initialize", "params": { "protocolVersion": "2025-11-25", @@ -1955,24 +1831,26 @@ "name": "test", "version": "0.0.0" } - } + }, + "jsonrpc": "2.0", + "id": 0 } }, { "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", "method": "POST", "body": { - "jsonrpc": "2.0", - "method": "notifications/initialized" + "method": "notifications/initialized", + "jsonrpc": "2.0" } }, { "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", "method": "POST", "body": { + "method": "tools/list", "jsonrpc": "2.0", - "id": 1, - "method": "tools/list" + "id": 1 } } ] @@ -1984,8 +1862,6 @@ "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", "method": "POST", "body": { - "jsonrpc": "2.0", - "id": 0, "method": "initialize", "params": { "protocolVersion": "2025-11-25", @@ -1994,24 +1870,26 @@ "name": "test", "version": "0.0.0" } - } + }, + "jsonrpc": "2.0", + "id": 0 } }, { "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", "method": "POST", "body": { - "jsonrpc": "2.0", - "method": "notifications/initialized" + "method": "notifications/initialized", + "jsonrpc": "2.0" } }, { "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", "method": "POST", "body": { + "method": "tools/list", "jsonrpc": "2.0", - "id": 1, - "method": "tools/list" + "id": 1 } } ] @@ -2034,8 +1912,6 @@ "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", "method": "POST", "body": { - "jsonrpc": "2.0", - "id": 0, "method": "initialize", "params": { "protocolVersion": "2025-11-25", @@ -2044,24 +1920,26 @@ "name": "test", "version": "0.0.0" } - } + }, + "jsonrpc": "2.0", + "id": 0 } }, { "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", "method": "POST", "body": { - "jsonrpc": "2.0", - "method": "notifications/initialized" + "method": "notifications/initialized", + "jsonrpc": "2.0" } }, { "url": "https://fixture.invalid/api/v1/mcps/WebSearch/mcp", "method": "POST", "body": { + "method": "tools/list", "jsonrpc": "2.0", - "id": 1, - "method": "tools/list" + "id": 1 } } ] diff --git a/packages/commands/tests/output/inference-output.test.ts b/packages/commands/tests/output/inference-output.test.ts index d47ce48d..27e14947 100644 --- a/packages/commands/tests/output/inference-output.test.ts +++ b/packages/commands/tests/output/inference-output.test.ts @@ -304,6 +304,7 @@ async function invoke( return new Response("", { status: 200 }); } if (scenario.kind === "search") { + if (address.includes("/chat/completions")) return Response.json(chat); if (String(json.method).startsWith("notifications/")) return new Response(null, { status: 204 }); return Response.json({ diff --git a/packages/core/package.json b/packages/core/package.json index beb5c49b..fe12a150 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-core", - "version": "2.8.2", + "version": "2.8.3", "description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.", "homepage": "https://bailian.console.aliyun.com/cli", "bugs": { diff --git a/packages/core/src/types/api.ts b/packages/core/src/types/api.ts index b324fb92..2c7e94c1 100644 --- a/packages/core/src/types/api.ts +++ b/packages/core/src/types/api.ts @@ -42,6 +42,7 @@ export interface ChatRequest { tool_choice?: "auto" | "none" | { type: "function"; function: { name: string } }; enable_thinking?: boolean; thinking_budget?: number; + enable_search?: boolean; modalities?: string[]; audio?: { voice: string; format?: string }; stream_options?: { include_usage?: boolean }; diff --git a/packages/core/src/types/command.ts b/packages/core/src/types/command.ts index e5e6c131..4844c1e7 100644 --- a/packages/core/src/types/command.ts +++ b/packages/core/src/types/command.ts @@ -318,6 +318,15 @@ export interface Command { description: LocalizedText; /** Credential this command requires. See {@link AuthRequirement}. */ auth: AuthRequirement; + /** + * Capability key used for Profile API-key selection (see + * `selectApiKeyResolutionSources`). Defaults to the command path joined with + * ".". Commands whose network target switches with a flag supply a resolver + * so credential selection follows the actual connection: `search web` model + * mode talks Chat Completions and authenticates as `text.chat`, while + * `--mode mcp` keeps the MCP-only `search.web` capability. + */ + authCapability?: (flags: ParsedFlags) => string; /** * Best-effort secondary console credential for `auth: "apiKey"` commands. * When true, the runtime additionally resolves the console credential and diff --git a/packages/kscli/package.json b/packages/kscli/package.json index 799612b5..2d47e2d9 100644 --- a/packages/kscli/package.json +++ b/packages/kscli/package.json @@ -1,6 +1,6 @@ { "name": "knowledge-studio-cli", - "version": "2.8.2", + "version": "2.8.3", "description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.", "keywords": [ "alibaba-cloud", diff --git a/packages/runtime/package.json b/packages/runtime/package.json index f137b046..d0ef59ee 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-runtime", - "version": "2.8.2", + "version": "2.8.3", "description": "Runtime framework for bailian-cli (createCli, registry, args, output, pipeline). See https://www.npmjs.com/package/bailian-cli for usage.", "homepage": "https://bailian.console.aliyun.com/cli", "bugs": { diff --git a/packages/runtime/src/middleware.ts b/packages/runtime/src/middleware.ts index 0687a49b..22b8f3ae 100644 --- a/packages/runtime/src/middleware.ts +++ b/packages/runtime/src/middleware.ts @@ -195,7 +195,11 @@ export const authStage: Middleware = async (ctx, next) => { ...resolveModelBaseUrlState(sources), }; if (command.auth === "apiKey") { - const capability = ctx.path.join("."); + // Commands whose network target switches with a flag declare authCapability + // so Profile selection follows the actual connection; others key off path. + const capability = command.authCapability + ? command.authCapability(ctx.flags) + : ctx.path.join("."); const selection = selectApiKeyResolutionSources(sources, capability); const apiSources = selection.sources; if (selection.fallbackFrom) { diff --git a/packages/runtime/tests/auth-profile-fallback.test.ts b/packages/runtime/tests/auth-profile-fallback.test.ts index dc0b7774..de54b88a 100644 --- a/packages/runtime/tests/auth-profile-fallback.test.ts +++ b/packages/runtime/tests/auth-profile-fallback.test.ts @@ -12,6 +12,7 @@ import { makeConfigStore, resolveModelBaseUrl, writeConfigFile, + type AnyCommand, type CommandPackManager, type LocalizedText, type SourceFlags, @@ -39,15 +40,15 @@ function makeContext( path: string[], flags: Partial = { config: "company-plan" }, env: NodeJS.ProcessEnv = {}, + command: AnyCommand = defineCommand({ + description: "test command", + auth: "apiKey", + async run() {}, + }), ): RunContext { const sources = buildSources(flags); sources.env = env; const settings = { ...buildSettings(sources), quiet: true }; - const command = defineCommand({ - description: "test command", - auth: "apiKey", - async run() {}, - }); return { identity: { binName: "bl", @@ -189,6 +190,43 @@ test("authStage 不为缺少 capability 字段的旧 token-plan 注入内置 pre }); }); +test("authStage 按命令 authCapability 选 capability:model 模式保留 token-plan,mcp 模式回退 default", async () => { + useTempConfigDir(); + await writeConfigFile({ api_key: "sk-default", base_url: "https://default.example.com" }); + await writeConfigFile( + { + api_key: "sk-token-plan", + base_url: "https://token-plan.example.com", + api_key_capabilities: ["text.chat"], + }, + "token-plan", + ); + const command = defineCommand({ + description: "test search", + auth: "apiKey", + flags: { + mode: { type: "string", valueHint: "", description: "search mode" }, + }, + authCapability: (flags) => (flags.mode === "mcp" ? "search.web" : "text.chat"), + async run() {}, + }); + + const modelContext = makeContext(["search", "web"], { config: "token-plan" }, {}, command); + await runAuth(modelContext); + expect(modelContext.client.exportApiCredential()).toMatchObject({ + token: "sk-token-plan", + baseUrl: "https://token-plan.example.com", + }); + + const mcpContext = makeContext(["search", "web"], { config: "token-plan" }, {}, command); + mcpContext.flags = { mode: "mcp" }; + await runAuth(mcpContext); + expect(mcpContext.client.exportApiCredential()).toMatchObject({ + token: "sk-default", + baseUrl: "https://default.example.com", + }); +}); + test("authStage 在 --api-key 覆盖 Profile API Key 时跳过 capability fallback", async () => { useTempConfigDir(); await writeConfigFile({ api_key: "sk-default", base_url: "https://default.example.com" }); diff --git a/skills/bailian-cli/SKILL.md b/skills/bailian-cli/SKILL.md index 350f101b..22c4b3ac 100644 --- a/skills/bailian-cli/SKILL.md +++ b/skills/bailian-cli/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-cli metadata: - version: "2.8.2" + version: "2.8.3" requires: bins: ["bl"] description: >- @@ -14,7 +14,7 @@ description: >- 家族路由:生图/生视频/配音/语音合成/转写 → bailian-gen;精调/微调/训练/训练数据集 → bailian-finetune; agents.yaml 托管 Agent → bailian-managed-agent;Sandbox 实例与模版 → bailian-sandbox; 个人长期记忆与应用记忆资源 → bailian-memory;个人历史资料辅助任务、资料接入与知识库管理 → bailian-knowledge; - 联网搜索的模型路由(Token Plan 自带搜索 vs MCP 搜索 + 兜底)→ bailian-web-search。 + 联网搜索路由(默认模型搜索 vs 显式 `--mode mcp` + 兜底)→ bailian-web-search。 不要用于普通问答、编程、写作、翻译、摘要、泛搜索,或图片理解等宿主自己能做的任务(普通问答、编程、写作、翻译、摘要、泛搜索不触发)。 未命名用量/额度问题:先问用户使用哪个产品,再运行 `bl usage` / `bl quota` 查询。 --- @@ -101,7 +101,7 @@ Use this table only after the decision table in [`bailian-protocol`](../bailian- | Training dataset / fine-tune / deploy | → skill `bailian-finetune` | Fallback: `bl dataset\|finetune\|deploy --help` | | agents.yaml IaC / managed-agent sessions | → skill `bailian-managed-agent` | Fallback: `bl managed-agent --help`; `apply`/`destroy` need `--yes` after `plan` | | Bailian Sandbox instance / template lifecycle | → skill `bailian-sandbox` | Fallback: `bl sandbox --help` | -| Web search (model-aware routing) | → skill `bailian-web-search` | Token Plan vs MCP path + fallback; fallback: `bl search web --help` | +| Web search (mode-aware routing) | → skill `bailian-web-search` | Default model mode vs explicit `--mode mcp` + fallback; fallback: `bl search web --help` | Flags, usage, and examples: see hub [`reference/`](reference/index.md) or `bl --help` — do not guess flags. Domain command details live in the owning skill's `reference/`. Examples in `reference/` stay bare; wrap with Call attribution when running. @@ -149,7 +149,7 @@ schema-export commands. ## Routing reminders - Image/video/audio generation or editing → skill `bailian-gen` (class 3 consent from `bailian-protocol`). Fine-tuning / datasets / deployments → `bailian-finetune`. agents.yaml IaC → `bailian-managed-agent`. Soft hand-off: Read sibling skill if installed; else `bl … --help` or prompt `bl skill init`. Image understanding the host agent can do → host-first; use `bl vision` / `bl omni` only when the user names a Bailian model or the media (video/audio files) exceeds host capability. -- Web search inside a Bailian workflow → skill `bailian-web-search` (model-aware routing: Token Plan → model-native search; default → `bl search web`; MCP failure → fall back once). Generic web research the host can do stays host-first — do not bounce it through `bl`. +- Web search inside a Bailian workflow → skill `bailian-web-search` (routing: default `bl search web` model mode for every identity; explicit `--mode mcp` for regular keys; MCP failure → fall back once). Generic web research the host can do stays host-first — do not bounce it through `bl`. - Answer ordinary reasoning, coding, writing, translation, summarization, and generic research with the host agent's native capabilities; do not bounce them through `bl text chat` or `bl search web`. - Usage / quota / credits questions that do not name a product → ask which product (Bailian or another AI service) first; run `bl usage` / `bl quota` only after the user picks Bailian or Bailian context is already established. - Personal recall, new long-term personal facts, and "remember this" requests → read `bailian-memory` if installed to check enablement, including first-use onboarding. Once enabled, use it for authorized personal recall/extraction; installation alone does not authorize cloud uploads. If unavailable or declined, respect the user’s host-memory choice. Explicit Bailian memory resource operations also route to `bailian-memory`. diff --git a/skills/bailian-cli/reference/index.md b/skills/bailian-cli/reference/index.md index 1a215a16..2b4b9492 100644 --- a/skills/bailian-cli/reference/index.md +++ b/skills/bailian-cli/reference/index.md @@ -100,7 +100,7 @@ Use this index for the skill-scoped quick index and global flags. | `bl quota history` | Console | View quota change history | [quota.md](quota.md) | | `bl quota list` | API Key | View model rate limits (QPM/TPM, account and workspace level) | [quota.md](quota.md) | | `bl quota update` | API Key | Update model rate limits (QPM/TPM) | [quota.md](quota.md) | -| `bl search web` | API Key | Search the web using DashScope MCP WebSearch service | [search.md](search.md) | +| `bl search web` | API Key | Search the web via a chat model with built-in web search (default) or the DashScope MCP WebSearch service | [search.md](search.md) | | `bl skill add` | No Auth | Install skills from the Bailian skill registry into local agents | [skill.md](skill.md) | | `bl skill init` | No Auth | Install all bailian-* skills (one-shot bootstrap for new environments) | [skill.md](skill.md) | | `bl skill list` | No Auth | List registry skills and diff against local installs | [skill.md](skill.md) | diff --git a/skills/bailian-cli/reference/search.md b/skills/bailian-cli/reference/search.md index b311f800..7b63ecb8 100644 --- a/skills/bailian-cli/reference/search.md +++ b/skills/bailian-cli/reference/search.md @@ -3,36 +3,42 @@ > Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. -> **Agent routing (mandatory):** Before running any `bl search *` command, Read skill `bailian-web-search` if installed and follow its identity-based routing (Token Plan → model-native web search; default → MCP; eligible MCP failures → fall back once). Do **not** call `bl search web` directly from this reference alone — Token Plan keys cannot authorize Bailian MCP search. If that skill is missing, run `bl skill init` or fall back to `bl search web --help` / `bl text chat --help` after checking `bl config show --output json`. +> **Agent routing (mandatory):** Before running any `bl search *` command, Read skill `bailian-web-search` if installed and follow its routing (default `bl search web` = model mode for every identity, including Token Plan; the WebSearch MCP service requires explicit `--mode mcp` and a regular DashScope key; eligible MCP failures → fall back to model mode once). Do **not** route Token Plan keys to `bl search web --mode mcp` — they cannot authorize Bailian MCP search. If that skill is missing, run `bl skill init` or fall back to `bl search web --help` / `bl text chat --help` after checking `bl config show --output json`. Index: [index.md](index.md) ## Commands in this group -| Command | Authentication | Description | -| --------------- | -------------- | ---------------------------------------------------- | -| `bl search web` | API Key | Search the web using DashScope MCP WebSearch service | +| Command | Authentication | Description | +| --------------- | -------------- | --------------------------------------------------------------------------------------------------------- | +| `bl search web` | API Key | Search the web via a chat model with built-in web search (default) or the DashScope MCP WebSearch service | ## Command details ### `bl search web` -| Field | Value | -| ------------------ | ---------------------------------------------------- | -| **Name** | `search web` | -| **Description** | Search the web using DashScope MCP WebSearch service | -| **Authentication** | API Key | -| **Usage** | `bl search web --query [flags]` | +| Field | Value | +| ------------------ | --------------------------------------------------------------------------------------------------------- | +| **Name** | `search web` | +| **Description** | Search the web via a chat model with built-in web search (default) or the DashScope MCP WebSearch service | +| **Authentication** | API Key | +| **Usage** | `bl search web --query [flags]` | #### Flags -| Flag | Type | Required | Description | -| ------------------ | ------ | -------- | -------------------------------------- | -| `--query ` | string | no | Search query text | -| `--count ` | number | no | Number of search results (default: 10) | -| `--list-tools` | switch | no | List available MCP tools and exit | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------ | +| `--query ` | string | no | Search query text | +| `--count ` | number | no | Number of search results (default: 10) | +| `--list-tools` | switch | no | List available MCP tools and exit | +| `--mode ` | string | no | Search mode: model answers via Chat Completions with enable_search, mcp calls the WebSearch MCP service (default: model) | +| `--model ` | string | no | Model ID for --mode model (default: qwen3.8-max) | +| `--api-key ` | string | no | API key | +| `--base-url ` | string | no | API base URL | + +#### Notes + +- Defaults to --mode model: one non-streaming Chat Completions request with enable_search: true, returning the model answer as a single content[].text block (no pages list). Use --mode mcp for the WebSearch MCP service (pages list); --count and --list-tools are MCP-mode only. The model resolves from --model, default_text_model, then qwen3.8-max. #### Examples @@ -41,11 +47,15 @@ bl search web --query "Alibaba Cloud Bailian latest features" ``` ```bash -bl search web --query "TypeScript 5.9 new features" --count 5 +bl search web --query "Today's news" ``` ```bash -bl search web --query "Today's news" +bl search web --mode mcp --query "TypeScript 5.9 new features" --count 5 +``` + +```bash +bl search web --mode mcp --query "Alibaba Cloud Bailian latest features" ``` ```bash diff --git a/skills/bailian-cli/reference/text.md b/skills/bailian-cli/reference/text.md index eadf3d47..87231cf2 100644 --- a/skills/bailian-cli/reference/text.md +++ b/skills/bailian-cli/reference/text.md @@ -24,25 +24,26 @@ Index: [index.md](index.md) #### Flags -| Flag | Type | Required | Description | -| ------------------------- | ------ | -------- | --------------------------------------------------------------------------- | -| `--api ` | string | no | API to call (default: chat) | -| `--model ` | string | no | Model ID (default: qwen3.8-max) | -| `--message ` | array | no | Message text (repeatable, prefix role: to set role); or use --messages-file | -| `--messages-file ` | string | no | Messages array for one request (use - for stdin) | -| `--system ` | string | no | System prompt | -| `--max-tokens ` | number | no | Maximum tokens to generate (default: 4096) | -| `--temperature ` | number | no | Sampling temperature (0.0, 2.0] | -| `--top-p ` | number | no | Nucleus sampling threshold | -| `--stream` | switch | no | Stream response tokens (default: on in TTY) | -| `--tool ` | array | no | Tool definition as JSON or file path (repeatable) | -| `--enable-thinking` | switch | no | Enable thinking/reasoning mode (for qwen3/qwq models) | -| `--thinking-budget ` | number | no | Max tokens for thinking (default: 4096) | -| `--batch` | switch | no | Single synchronous Batch Chat request (non-streaming) | -| `--prime` | switch | no | Use Prime mode with a workspace-scoped endpoint | -| `--workspace-id ` | string | no | Workspace ID for the default Prime endpoint (or set BAILIAN_WORKSPACE_ID) | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| ------------------------------- | ------- | -------- | ------------------------------------------------------------------------------------- | +| `--api ` | string | no | API to call (default: chat) | +| `--model ` | string | no | Model ID (default: qwen3.8-max) | +| `--message ` | array | no | Message text (repeatable, prefix role: to set role); or use --messages-file | +| `--messages-file ` | string | no | Messages array for one request (use - for stdin) | +| `--system ` | string | no | System prompt | +| `--max-tokens ` | number | no | Maximum tokens to generate (default: 4096) | +| `--temperature ` | number | no | Sampling temperature (0.0, 2.0] | +| `--top-p ` | number | no | Nucleus sampling threshold | +| `--enable-search ` | boolean | no | Enable built-in web search (Chat Completions API only; default: true for qwen3.8-max) | +| `--stream` | switch | no | Stream response tokens (default: on in TTY) | +| `--tool ` | array | no | Tool definition as JSON or file path (repeatable) | +| `--enable-thinking` | switch | no | Enable thinking/reasoning mode (for qwen3/qwq models) | +| `--thinking-budget ` | number | no | Max tokens for thinking (default: 4096) | +| `--batch` | switch | no | Single synchronous Batch Chat request (non-streaming) | +| `--prime` | switch | no | Use Prime mode with a workspace-scoped endpoint | +| `--workspace-id ` | string | no | Workspace ID for the default Prime endpoint (or set BAILIAN_WORKSPACE_ID) | +| `--api-key ` | string | no | API key | +| `--base-url ` | string | no | API base URL | #### Notes @@ -50,6 +51,7 @@ Index: [index.md](index.md) - Batch Chat total request timeout defaults to 300s; --timeout must be 60–3600s. - Prime mode requires an explicit model and currently supports only the Chat Completions API. - Prime endpoint: configured --base-url, DASHSCOPE_BASE_URL, or profile base_url takes precedence; otherwise workspace is resolved from --workspace-id, BAILIAN_WORKSPACE_ID, then config workspace_id. +- --enable-search maps to the Chat Completions enable_search parameter and requires a model with built-in web search support; whitelisted models (qwen3.8-max) enable it by default unless --enable-search false is passed; for the Responses API use --tool '{"type":"web_search"}' instead. #### Examples @@ -89,6 +91,10 @@ bl text chat --message "Hello" --output json bl text chat --model qwq-plus --message "Solve 1+1" --enable-thinking ``` +```bash +bl text chat --enable-search true --message "Latest news about Alibaba Cloud" +``` + ```bash bl text chat --prime --workspace-id --model glm-5.3-prime --message "Explain this code" ``` diff --git a/skills/bailian-finetune/SKILL.md b/skills/bailian-finetune/SKILL.md index 81fc32fb..b881ffbf 100644 --- a/skills/bailian-finetune/SKILL.md +++ b/skills/bailian-finetune/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-finetune metadata: - version: "2.8.2" + version: "2.8.3" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-gen/SKILL.md b/skills/bailian-gen/SKILL.md index e79d9bc9..1189a019 100644 --- a/skills/bailian-gen/SKILL.md +++ b/skills/bailian-gen/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-gen metadata: - version: "2.8.2" + version: "2.8.3" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-knowledge/SKILL.md b/skills/bailian-knowledge/SKILL.md index 677c8da3..b47c6e05 100644 --- a/skills/bailian-knowledge/SKILL.md +++ b/skills/bailian-knowledge/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-knowledge metadata: - version: "2.8.2" + version: "2.8.3" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-managed-agent/SKILL.md b/skills/bailian-managed-agent/SKILL.md index df291f26..e9de66db 100644 --- a/skills/bailian-managed-agent/SKILL.md +++ b/skills/bailian-managed-agent/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-managed-agent metadata: - version: "2.8.2" + version: "2.8.3" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-memory/SKILL.md b/skills/bailian-memory/SKILL.md index f9a5b9cc..d95ec262 100644 --- a/skills/bailian-memory/SKILL.md +++ b/skills/bailian-memory/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-memory metadata: - version: "2.8.2" + version: "2.8.3" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-protocol/SKILL.md b/skills/bailian-protocol/SKILL.md index 896754ef..a113094e 100644 --- a/skills/bailian-protocol/SKILL.md +++ b/skills/bailian-protocol/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-protocol metadata: - version: "2.8.2" + version: "2.8.3" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-sandbox/SKILL.md b/skills/bailian-sandbox/SKILL.md index ac5cd047..43e0df22 100644 --- a/skills/bailian-sandbox/SKILL.md +++ b/skills/bailian-sandbox/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-sandbox metadata: - version: "2.8.2" + version: "2.8.3" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-web-search/SKILL.md b/skills/bailian-web-search/SKILL.md index 4793367a..9f49f1a6 100644 --- a/skills/bailian-web-search/SKILL.md +++ b/skills/bailian-web-search/SKILL.md @@ -1,16 +1,15 @@ --- name: bailian-web-search metadata: - version: "2.8.2" + version: "2.8.3" requires: bins: ["bl"] description: >- 阿里云百炼联网搜索(web search)入口:为联网搜索 / 网页搜索 / 查最新资讯做路径分发。 - 先识别当前连接身份:Token Plan(profile `token-plan` 或 base_url host 为 - `token-plan..maas.aliyuncs.com`)→ 模型自带搜索 - (`bl text chat --api responses --tool '{"type":"web_search"}'`);其他 / 默认 → - Bailian MCP(`bl search web`);仅在 MCP 鉴权失败、未开通或传输失败时兜底一次到模型自带搜索。 - Token Plan 联网搜索失败、两条路径鉴权混淆的排查也走本技能。 + `bl search web` 默认走 model 模式(Chat Completions enable_search),任何连接身份 + (含 Token Plan)均可用;仅当用户明确要 WebSearch MCP 服务或 pages 列表时用 + `--mode mcp`(仅普通 DashScope API Key);MCP 鉴权失败、未开通或传输失败时 + 兜底一次到 model 模式。联网搜索失败、两条路径鉴权混淆的排查也走本技能。 反触发:宿主可完成的 普通问答 / 编程 / 写作 → 不触发;知识库 RAG → bailian-cli (`bl knowledge`);生图/生视频/语音 → bailian-gen;精调/训练 → bailian-finetune; agents.yaml → bailian-managed-agent。 @@ -32,7 +31,7 @@ BAILIAN_CALLER_TYPE=agent BAILIAN_CALLER_NAME=codex BAILIAN_SKILL_NAME=bailian-w > > **Install (supported):** `bl skill init` -Token Plan keys do not authorize Bailian MCP search — never route them to `bl search web`. +Token Plan keys do not authorize Bailian MCP search — never route them to `bl search web --mode mcp` (the default model mode works for Token Plan). ## Routing workflow @@ -51,50 +50,63 @@ Treat the connection as **Token Plan** when **either** condition holds — this Do not rely on the profile name alone: users can create additional Token Plan profiles under custom names, and only the Base URL host check catches those. Anything else → **default** identity. -### Step 2 — Route by identity +### Step 2 — Route by mode and identity -| Condition | Route | Notes | -| ------------------------- | ------------------------------- | --------------------------------------------------- | -| Token Plan model identity | Model-native path | Preferred — the only path Token Plan keys authorize | -| Any other model / default | MCP path | Default for regular DashScope API keys | -| MCP path (eligible fail) | Model-native path, exactly once | Fallback — only for the failure classes below | +| Condition | Route | Notes | +| --------------------------------------------- | ------------------------------------- | --------------------------------------------------- | +| Any identity (default) | Model mode: `bl search web --query …` | CLI default; the model answers with `enable_search` | +| Regular key + user explicitly wants MCP/pages | `bl search web --mode mcp --query …` | Requires the WebSearch MCP activated for that key | +| Token Plan + user asks for MCP | Explain, stay on model mode | Token Plan keys cannot authorize Bailian MCP search | +| `--mode mcp` run (eligible fail) | Model mode, exactly once | Fallback — only for the failure classes below | -**User override:** if the user names a specific path or command, follow it **except** under Token Plan identity: do not call `bl search web` even if the user asks for MCP — explain that Token Plan keys cannot authorize Bailian MCP search, then use the model-native path (or ask once whether to switch profile / key). +**User override:** if the user names a specific path or command, follow it **except** under Token Plan identity: do not call `bl search web --mode mcp` even if the user asks for MCP — explain that Token Plan keys cannot authorize Bailian MCP search, then use the default model mode (or ask once whether to switch profile / key). -### Model-native path (Token Plan preferred / fallback target) +### Model path (default for every identity) + +```bash +bl search web --query "阿里云百炼最新功能" +``` + +- Without `--mode` the CLI runs model mode: one non-streaming Chat Completions request with `enable_search: true`; the reply is the model answer (no pages list). +- Works for Token Plan and regular identities alike: the CLI selects the credential per mode (model mode authenticates in the `text.chat` capability domain). +- **Do not hardcode `--model`:** omit it so the CLI uses `default_text_model` / built-in default; only pass `--model` when the user named one. +- Flags / usage: Read skill `bailian-cli` if installed, else `bl search web --help`. + +### Responses API alternative (only when the user names it) ```bash bl text chat --api responses --tool '{"type":"web_search"}' --message "搜索近期的阿里云新闻" ``` -- The Responses API enables native web search via the tool definition `{"type":"web_search"}`. -- Requires a model with native web search support (Qwen3.7+). **Do not hardcode `--model`:** omit it so the CLI uses `default_text_model` / built-in default; only pass `--model` when the user named one. +- The Responses API enables native web search via the tool definition `{"type":"web_search"}`; prefer it only when the user explicitly asks for the Responses API or tool-style results. +- Requires a model with native web search support (Qwen3.7+); the same "do not hardcode `--model`" rule applies. - Write `--message` in the user's language; the reply language follows the prompt (see `bailian-protocol` → Respond in the user's language). - Flags / usage: Read skill `bailian-cli` if installed, else `bl text chat --help`. -### MCP path (default) +### MCP path (explicit `--mode mcp`, regular keys only) ```bash -bl search web --query "阿里云百炼最新功能" +bl search web --mode mcp --query "阿里云百炼最新功能" ``` +- Route here only when the user explicitly wants the WebSearch MCP service or the pages list; regular DashScope API keys only — Token Plan keys cannot authorize it. - Requires the WebSearch MCP to be activated for the current key; on the not-activated error the CLI appends an activation hint with the marketplace URL — relay it to the user. - Flags / usage: Read skill `bailian-cli` if installed, else `bl search web --help`. -### Fallback (MCP → model-native, exactly once) +### Fallback (MCP → model, exactly once) -**Do not** fall back on every non-zero exit. Fall back **only** when `bl search web` fails for one of these classes (match stderr / message): +**Do not** fall back on every non-zero exit. Fall back **only** when a `bl search web --mode mcp` run fails for one of these classes (match stderr / message): -- **auth / permission** — key not valid for the MCP service (e.g. Token Plan key misrouted by identity detection), +- **auth / permission** — key not valid for the MCP service (e.g. Token Plan key misrouted to `--mode mcp`), - **MCP not activated** — `MCP request failed: 404` with `未开通` / `MCP不存在` / `MCP_IS_INVALID` (CLI may append an activation hint), - **MCP transport** — 405 / Streamable-HTTP unsupported, or clear network / timeout / DNS failures reaching the MCP endpoint. -**Do not fall back** for: missing `--query` / USAGE errors, rate limits, content-policy / business errors from a successful MCP session, or empty-but-successful result sets. Report those verbatim and stop (or ask the user); do not burn a Responses call. +**Do not fall back** for: missing `--query` / USAGE errors, rate limits, content-policy / business errors from a successful MCP session, or empty-but-successful result sets. Report those verbatim and stop (or ask the user); do not burn a model call. Default model-mode failures are **not** MCP failures — report them verbatim; never reinterpret them as MCP errors or retry them as MCP. Fallback discipline: -1. Re-issue the same query via the model-native path (omit `--model` unless the user named one). -2. If the fallback succeeds, tell the user the MCP path failed and — when the not-activated hint appeared — that activating the WebSearch MCP restores the default path. +1. Re-issue the same query via the default model mode (omit `--model` unless the user named one). +2. If the fallback succeeds, tell the user the MCP path failed and — when the not-activated hint appeared — that activating the WebSearch MCP restores the MCP path. 3. If the fallback also fails, stop and report both errors verbatim; follow the issue-reporting flow in `bailian-protocol` (ask once). Never loop retries. ## Quick examples @@ -103,21 +115,21 @@ Fallback discipline: # Step 1: identify the active model identity bl config show --output json -# Token Plan identity → model-native web search (no --model unless user named one) -bl text chat --api responses --tool '{"type":"web_search"}' --message "搜索近期的阿里云新闻" - -# Default identity → Bailian MCP search +# Default (any identity, including Token Plan): model mode bl search web --query "阿里云百炼最新功能" -# Eligible MCP failure → fall back once (same rule: no hardcoded --model) -bl text chat --api responses --tool '{"type":"web_search"}' --message "阿里云百炼最新功能" +# Regular key + user explicitly wants the MCP service / pages list +bl search web --mode mcp --query "阿里云百炼最新功能" + +# Responses API alternative (only when the user names it) +bl text chat --api responses --tool '{"type":"web_search"}' --message "搜索近期的阿里云新闻" ``` ## Routing reminders - Generic web research the host can do, ordinary Q&A, coding, writing → host-first; do not invoke `bl` (class 1 in `bailian-protocol`). Route only when the user names Bailian / DashScope / `bl` or continues an existing `bl` workflow (class 4). - Knowledge-base / RAG over Bailian corpora → hub skill `bailian-cli` (`bl knowledge`), not this skill. -- Summarize search results in the user's language; on the model-native path the CLI injects no default language — if a fixed language is required, pass `--system` in that language (do not hardcode 简体中文). +- Summarize search results in the user's language; write `--query` / `--message` in the user's language — the model path injects no default language (do not hardcode 简体中文); if a fixed reply language is required, use the Responses API alternative with `--system` in that language. - Other Bailian workflows (apps / usage / config) → hub skill `bailian-cli`; media generation → `bailian-gen`; fine-tuning → `bailian-finetune`; agents.yaml → `bailian-managed-agent`. Soft hand-off by skill name: Read if installed, else `bl … --help` or prompt `bl skill init`. ## references diff --git a/tools/generate-reference.ts b/tools/generate-reference.ts index bcb6e373..a194427a 100644 --- a/tools/generate-reference.ts +++ b/tools/generate-reference.ts @@ -76,9 +76,11 @@ const GENERATED_BANNER = const GROUP_AGENT_NOTICE: Readonly> = { search: "> **Agent routing (mandatory):** Before running any `bl search *` command, " + - "Read skill `bailian-web-search` if installed and follow its identity-based routing " + - "(Token Plan → model-native web search; default → MCP; eligible MCP failures → fall back once). " + - "Do **not** call `bl search web` directly from this reference alone — Token Plan keys cannot " + + "Read skill `bailian-web-search` if installed and follow its routing " + + "(default `bl search web` = model mode for every identity, including Token Plan; " + + "the WebSearch MCP service requires explicit `--mode mcp` and a regular DashScope key; " + + "eligible MCP failures → fall back to model mode once). " + + "Do **not** route Token Plan keys to `bl search web --mode mcp` — they cannot " + "authorize Bailian MCP search. If that skill is missing, run `bl skill init` or fall back to " + "`bl search web --help` / `bl text chat --help` after checking `bl config show --output json`.", };