mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
refactor(tool-bailian-kb): 重构默认服务配置管理及设置页同步逻辑
- 替换默认服务配置字段为 defaultRetrieveAgentId 和 defaultChatAgentId,支持分别管理检索和对话服务 - 增加 Bailian knowledge base 插件设置命名空间,配置支持用户层叠覆盖和实时生效 - 统一凭证与设置配置的回退链,新增一阶段设置用户层优先读取字段 - 实现凭证值向设置文档的单次迁移,辅助设置页首次展示历史值 - Bailian 页面控制器支持基于设置作用域的双域同步,回显设置字段且支持删除操作 - 工具代码切换为从设置作用域异步解析默认服务 id,agent_id 在运行时均为可选但调用时必填 - README 与文档更新,包含配置项扩展、回退链说明以及 UI 配置页操作说明 - 底层依赖由 `dsh-client-ui-settings-plugins` 切换为 `dsh-client-ui-settings` - 修正 patch 配置与 API 参数命名,完善错误提示和超时配置读取
This commit is contained in:
+18
-17
@@ -1,6 +1,6 @@
|
||||
# bailian-kb-dsh(分发包)
|
||||
|
||||
dsh bundle 分发面:`package.json` 的 `dsh.bundle.patch` 声明 + [`cordis.patch.yml`](cordis.patch.yml),向 profile 插入 `tool-bailian-kb` row,并随包分发浏览器端配置卡片(`dsh.client` → `lib/client.js`)。
|
||||
dsh bundle 分发面:`package.json` 的 `dsh.bundle.patch` 声明 + [`cordis.patch.yml`](cordis.patch.yml),向 profile 插入 `tool-bailian-kb` row,并随包分发浏览器端配置页(`dsh.client` → `lib/client.js`)。
|
||||
|
||||
## Patch row
|
||||
|
||||
@@ -12,37 +12,38 @@ dsh bundle 分发面:`package.json` 的 `dsh.bundle.patch` 声明 + [`cordis.p
|
||||
workspaceId: !!js process.env.BAILIAN_WORKSPACE_ID
|
||||
```
|
||||
|
||||
`workspaceId` 只是解析链的一层,不是唯一来源:config 显式值(含此环境变量读取)per-call 优先;未设置时回退到 `BAILIAN_WORKSPACE_ID` credential。同样回退覆盖 `defaultAgentId`(`BAILIAN_DEFAULT_AGENT_ID`)与 API key(`DASHSCOPE_API_KEY`)。
|
||||
`workspaceId` 只是解析链的一层,不是唯一来源:Config 同时注册为 `bailian-kb` settings namespace,patch entry 作 base 层,设置页/设置文档的用户层叠在其上;都未设置时 per-call 回退到 `BAILIAN_WORKSPACE_ID` credential。同样回退覆盖 `defaultRetrieveAgentId`(`BAILIAN_DEFAULT_RETRIEVE_AGENT_ID`)、`defaultChatAgentId`(`BAILIAN_DEFAULT_CHAT_AGENT_ID`)与 API key(`DASHSCOPE_API_KEY`,无 settings 面)。
|
||||
|
||||
## 三个值的解析链
|
||||
## 四个值的解析链
|
||||
|
||||
| 值 | 1️⃣ config 显式值(本 patch 或用户覆盖) | 2️⃣ credential(UI 卡片 / `~/.dsh/.credentials.yaml`) | 3️⃣ 都缺失时 |
|
||||
|---|---|---|---|
|
||||
| `DASHSCOPE_API_KEY` | —(无 config 面) | ✅ | 工具调用报错并引导配置 |
|
||||
| `BAILIAN_WORKSPACE_ID` | `workspaceId` | ✅ | 工具调用报错并引导配置 |
|
||||
| `BAILIAN_DEFAULT_AGENT_ID` | `defaultAgentId` | ✅ | `agent_id` 参数变必填(schema 恒 optional,运行时校验) |
|
||||
| 值 | 1️⃣ settings 用户层(设置页可编辑、回显) | 2️⃣ entry config(本 patch 或用户覆盖,作 base 层) | 3️⃣ credential(`~/.dsh/.credentials.yaml` / env) | 4️⃣ 都缺失时 |
|
||||
|---|---|---|---|---|
|
||||
| `DASHSCOPE_API_KEY` | —(无 settings 面) | —(无 config 面) | ✅ | 工具调用报错并引导配置 |
|
||||
| `BAILIAN_WORKSPACE_ID` | ✅ `workspaceId` | ✅ `workspaceId` | ✅ | 工具调用报错并引导配置 |
|
||||
| `BAILIAN_DEFAULT_RETRIEVE_AGENT_ID` | ✅ `defaultRetrieveAgentId` | ✅ `defaultRetrieveAgentId` | ✅ | `kb_search` 的 `agent_id` 参数变必填(schema 恒 optional,运行时校验) |
|
||||
| `BAILIAN_DEFAULT_CHAT_AGENT_ID` | ✅ `defaultChatAgentId` | ✅ `defaultChatAgentId` | ✅ | `kb_chat` 的 `agent_id` 参数变必填(schema 恒 optional,运行时校验) |
|
||||
|
||||
行为参数(`endpointHost`/`agentVersion`/`chatTimeoutMs`)只在 config 层,见 [tool-bailian-kb README](../tool-bailian-kb/README.md)。
|
||||
行为参数(`endpointHost`/`agentVersion`/`chatTimeoutMs`)在 config/settings 层(设置文档可改,实时生效),见 [tool-bailian-kb README](../tool-bailian-kb/README.md)。
|
||||
|
||||
## Web UI 配置卡片
|
||||
## Web UI 配置页
|
||||
|
||||
装进 profile 后,Settings → Plugins 出现“百炼知识库”卡片,可配置三个 credential(写 `~/.dsh/.credentials.yaml`):
|
||||
装进 profile 后,Settings 左侧导航出现“百炼知识库”页(`settings.section` 槽位):
|
||||
|
||||
- **DashScope API Key** — write-only,`type=password` 遮罩输入草稿
|
||||
- **Bailian Workspace ID** — 明文(便于粘贴核对 workspace id)
|
||||
- **默认服务 ID(agent_id)** — 明文,附独立“清除”按钮(留空保存 = 不写,清除须显式 unset)
|
||||
- **DashScope API Key** — write-only(凭据域 wire 结构上无值位,永不回显),`type=password` 遮罩输入草稿,仅显示 configured/来自环境变量 徽标;写 `~/.dsh/.credentials.yaml`
|
||||
- **Bailian Workspace ID / 默认检索服务 ID / 默认对话服务 ID** — **回显**:读写 `bailian-kb` settings 用户层(设置文档),预填当前解析值;清空保存 = 移除用户层,回退 entry config → credential;每个默认服务 ID 附“清除”按钮(同时 unset settings 用户层与 credential,避免回退链复活旧值)
|
||||
|
||||
值永不回显:字段始终空白起步,仅显示 configured/来自环境变量 徽标;来自 shell export 或 `~/.dsh/.env` 的值只读(继承环境层),输入框禁用。
|
||||
降级:远程浏览器(非 loopback,settings RPC 不可达)或未组合 settings 服务时,ID 字段退回旧的 write-only credential 控件,页面顶部显示提示。
|
||||
|
||||
## 用户覆盖
|
||||
|
||||
用户 patch 层在本 bundle 之上,按 id 覆盖时**替换整个 config(无 deep-merge)**。`workspaceId`/`defaultAgentId` 均为可选,只需重述想显式固定的字段:
|
||||
用户 patch 层在本 bundle 之上,按 id 覆盖时**替换整个 config(无 deep-merge)**,覆盖后的 config 成为 settings namespace 的新 base 层(设置页的用户层仍叠在其上)。`workspaceId`/`defaultRetrieveAgentId`/`defaultChatAgentId` 均为可选,只需重述想显式固定的字段:
|
||||
|
||||
```yaml
|
||||
# ~/.dsh/cordis.patch.yml 或 profile 的 cordis.patch.yml
|
||||
- id: tool-bailian-kb
|
||||
config:
|
||||
defaultAgentId: aid-customer-service # 场景固定式部署;省略 workspaceId 走 credential
|
||||
defaultRetrieveAgentId: aid-search-service # 检索服务;省略 workspaceId 走 credential
|
||||
defaultChatAgentId: aid-chat-service # 对话服务
|
||||
chatTimeoutMs: 600000
|
||||
```
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
# workspaceId here is one resolution layer, not the only one: a config value
|
||||
# (this env read included) wins per call; when it is unset the plugin resolves
|
||||
# the BAILIAN_WORKSPACE_ID credential instead (web UI card or
|
||||
# ~/.dsh/.credentials.yaml). The same fallback covers defaultAgentId via
|
||||
# BAILIAN_DEFAULT_AGENT_ID, and the API key via DASHSCOPE_API_KEY.
|
||||
# ~/.dsh/.credentials.yaml). The same fallback covers defaultRetrieveAgentId
|
||||
# via BAILIAN_DEFAULT_RETRIEVE_AGENT_ID, defaultChatAgentId via
|
||||
# BAILIAN_DEFAULT_CHAT_AGENT_ID, and the API key via DASHSCOPE_API_KEY.
|
||||
|
||||
- insert:
|
||||
- id: tool-bailian-kb
|
||||
|
||||
@@ -4,23 +4,26 @@
|
||||
|
||||
## Config
|
||||
|
||||
Config 同时注册为 `bailian-kb` settings namespace(`installSettingsSection`):profile patch 的 entry config 作为 base 层,用户在设置页/设置文档的修改叠在其上且实时生效(所有值每次调用经 source thunk 读取,无需重启或重注册工具)。
|
||||
|
||||
| 字段 | 类型 | 默认 | 语义 |
|
||||
|---|---|---|---|
|
||||
| `workspaceId` | string | **必填** | 百炼工作空间 id;API host 为 workspace 子域名 `https://<workspaceId>.<endpointHost>` |
|
||||
| `workspaceId` | string? | — | 百炼工作空间 id;API host 为 workspace 子域名 `https://<workspaceId>.<endpointHost>`。未设置时每次调用回退 `BAILIAN_WORKSPACE_ID` credential |
|
||||
| `endpointHost` | string | `cn-beijing.maas.aliyuncs.com` | host 后缀,其他 region/私有化时替换 |
|
||||
| `defaultAgentId` | string? | — | 场景固定式部署绑定的检索服务;**配置后 `agent_id` 参数在注册期变为可选**(加载期静态决定 schema,非运行时 fallback) |
|
||||
| `defaultRetrieveAgentId` | string? | — | 默认检索服务;`kb_search` 的 `agent_id` 参数 schema 恒可选,默认值每次调用运行时解析(settings/config → credential) |
|
||||
| `defaultChatAgentId` | string? | — | 默认对话服务;`kb_chat` 的 `agent_id` 参数 schema 恒可选,默认值每次调用运行时解析(settings/config → credential) |
|
||||
| `agentVersion` | string? | — | `beta`(草稿调试)或已发布版本号;不暴露给模型 |
|
||||
| `chatTimeoutMs` | number | 300000 | kb_chat 超时;服务端是分钟级 agentic loop |
|
||||
|
||||
凭证:`DASHSCOPE_API_KEY` 走 `ctx.credentials` 引用,每次调用重新解析(热更换生效),未配置时报错并附获取指引。
|
||||
凭证与回退链:`DASHSCOPE_API_KEY` 只走 `ctx.credentials` 引用(write-only,每次调用重新解析,热更换生效);`workspaceId`/`defaultRetrieveAgentId`/`defaultChatAgentId` 先取 settings 解析值(用户层 > entry config),缺失时回退同名 credential(`BAILIAN_WORKSPACE_ID`/`BAILIAN_DEFAULT_RETRIEVE_AGENT_ID`/`BAILIAN_DEFAULT_CHAT_AGENT_ID`),都没有时报错并附配置指引。
|
||||
|
||||
## 工具
|
||||
|
||||
| 工具 | 参数 | 返回 |
|
||||
|---|---|---|
|
||||
| `kb_service_list` | `scene?`(chat\|search,省略查双场景合并)、`name_filter?` | 服务清单(agent_id、名称、scene、status、绑定知识库)+ total + truncated;分页内部消化(单 scene 100 条上限) |
|
||||
| `kb_search` | `query`、`agent_id`(见 defaultAgentId)、`top_k?`(默认 5,**客户端截断**——服务端无此参数)、`images?` | chunks(text/score/来源)+ total |
|
||||
| `kb_chat` | `message`、`agent_id` | 完整答案(内部消费 SSE 流缓冲返回)+ request_id |
|
||||
| `kb_search` | `query`、`agent_id`(见 defaultRetrieveAgentId)、`top_k?`(默认 5,**客户端截断**——服务端无此参数)、`images?` | chunks(text/score/来源)+ total |
|
||||
| `kb_chat` | `message`、`agent_id`(见 defaultChatAgentId) | 完整答案(内部消费 SSE 流缓冲返回)+ request_id |
|
||||
|
||||
## 错误语义
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"@deepseek-ai/dsh-client-locale",
|
||||
"@deepseek-ai/dsh-client-runtime",
|
||||
"@deepseek-ai/dsh-api-remotes",
|
||||
"@deepseek-ai/dsh-client-ui-settings-plugins"
|
||||
"@deepseek-ai/dsh-client-ui-settings"
|
||||
],
|
||||
"platform": "web"
|
||||
}
|
||||
@@ -28,14 +28,14 @@
|
||||
"@deepseek-ai/cordis": "^4.0.1",
|
||||
"@deepseek-ai/dsh-tools": "*",
|
||||
"@deepseek-ai/dsh-credentials": "*",
|
||||
"@deepseek-ai/dsh-settings": "*",
|
||||
"@deepseek-ai/dsh-skill": "*",
|
||||
"@deepseek-ai/schemastery": "^3.18.1",
|
||||
"@deepseek-ai/dsh-api-remotes": "*",
|
||||
"@deepseek-ai/dsh-client-connection": "*",
|
||||
"@deepseek-ai/dsh-client-locale": "*",
|
||||
"@deepseek-ai/dsh-client-runtime": "*",
|
||||
"@deepseek-ai/dsh-client-ui-settings-plugins": "*",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "*",
|
||||
"@deepseek-ai/dsh-client-ui-settings": "*",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "*",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
@@ -43,14 +43,14 @@
|
||||
"@deepseek-ai/cordis": "link:../../../deepseek-harness/vendor/cordis",
|
||||
"@deepseek-ai/dsh-tools": "link:../../../deepseek-harness/packages/core/tools",
|
||||
"@deepseek-ai/dsh-credentials": "link:../../../deepseek-harness/packages/credentials/credentials",
|
||||
"@deepseek-ai/dsh-settings": "link:../../../deepseek-harness/packages/settings/settings",
|
||||
"@deepseek-ai/dsh-skill": "link:../../../deepseek-harness/packages/skill/skill",
|
||||
"@deepseek-ai/schemastery": "link:../../../deepseek-harness/vendor/schemastery",
|
||||
"@deepseek-ai/dsh-api-remotes": "link:../../../deepseek-harness/packages/api/remotes",
|
||||
"@deepseek-ai/dsh-client-connection": "link:../../../deepseek-harness/packages/client/connection",
|
||||
"@deepseek-ai/dsh-client-locale": "link:../../../deepseek-harness/packages/client/locale",
|
||||
"@deepseek-ai/dsh-client-runtime": "link:../../../deepseek-harness/packages/client/runtime",
|
||||
"@deepseek-ai/dsh-client-ui-settings-plugins": "link:../../../deepseek-harness/packages/client/ui-settings-plugins",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "link:../../../deepseek-harness/packages/client/ui-primitives",
|
||||
"@deepseek-ai/dsh-client-ui-settings": "link:../../../deepseek-harness/packages/client/ui-settings",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "link:../../../deepseek-harness/packages/client/ui-slots",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "~18.3.1",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
||||
import { settingsNamespace, type SettingsRegisterOptions, type SettingsScope } from '@deepseek-ai/dsh-settings'
|
||||
import { KbClient } from './client.js'
|
||||
import { registerSkill } from './skill.js'
|
||||
import { createKbTools } from './tools.js'
|
||||
@@ -14,58 +15,120 @@ import { createKbTools } from './tools.js'
|
||||
export const name = 'tool-bailian-kb'
|
||||
export const inject = ['tools', 'credentials']
|
||||
|
||||
/** Settings namespace this plugin registers when a settings service is composed. */
|
||||
const SETTINGS_NS = settingsNamespace('bailian-kb')
|
||||
|
||||
/** Settings fields seeded once from their credential references ({@link seedFromCredentials}). */
|
||||
const CREDENTIAL_SEEDS = [
|
||||
['workspaceId', 'BAILIAN_WORKSPACE_ID'],
|
||||
['defaultRetrieveAgentId', 'BAILIAN_DEFAULT_RETRIEVE_AGENT_ID'],
|
||||
['defaultChatAgentId', 'BAILIAN_DEFAULT_CHAT_AGENT_ID'],
|
||||
] as const
|
||||
|
||||
/**
|
||||
* One-time migration: before this section existed, the workspace and
|
||||
* default-service ids lived only as credentials, which the wire never echoes.
|
||||
* Seed each field the resolved section does not answer from the WRITABLE
|
||||
* credential layer (`file`), so the page shows the value the deployment
|
||||
* already runs with; env-sourced values stay where they are — freezing one
|
||||
* into the document would shadow later environment changes.
|
||||
* @param ctx - registrant context carrying credentials.
|
||||
* @param scope - the registered `bailian-kb` scope the seed writes through.
|
||||
*/
|
||||
async function seedFromCredentials(ctx: Context, scope: SettingsScope<Config>): Promise<void> {
|
||||
try {
|
||||
const seeds: Partial<Record<(typeof CREDENTIAL_SEEDS)[number][0], string>> = {}
|
||||
for (const [field, ref] of CREDENTIAL_SEEDS) {
|
||||
if (scope.get()[field]) continue
|
||||
const resolved = await ctx.credentials.resolve(credentialRef(ref))
|
||||
if (resolved?.source !== 'file') continue
|
||||
seeds[field] = resolved.value
|
||||
}
|
||||
if (Object.keys(seeds).length > 0) await scope.update(seeds)
|
||||
} catch (_migrationFailure) {
|
||||
// Best-effort: a failed seed leaves the credential fallback in place, so
|
||||
// resolution still answers — the page merely starts blank.
|
||||
}
|
||||
}
|
||||
|
||||
/** Bailian knowledge-base plugin configuration. */
|
||||
export interface Config {
|
||||
/** Bailian workspace id; the API host is the workspace subdomain `https://<workspaceId>.<endpointHost>`. Optional here: an unset value falls back per call to the BAILIAN_WORKSPACE_ID credential (Settings → Plugins card or ~/.dsh/.credentials.yaml). */
|
||||
/** Bailian workspace id; the API host is the workspace subdomain `https://<workspaceId>.<endpointHost>`. Optional here: an unset value falls back per call to the BAILIAN_WORKSPACE_ID credential (env/.env or ~/.dsh/.credentials.yaml). Editable with echo on the Settings → 百炼知识库 page (settings layer). */
|
||||
workspaceId?: string
|
||||
/** API host suffix; replace for other regions or private deployments. */
|
||||
endpointHost: string
|
||||
/** Retrieval-service id pinned by this deployment; when unset, the per-call fallback reads the BAILIAN_DEFAULT_AGENT_ID credential. */
|
||||
defaultAgentId?: string
|
||||
/** Retrieval-service id pinned by this deployment; when unset, the per-call fallback reads the BAILIAN_DEFAULT_RETRIEVE_AGENT_ID credential. */
|
||||
defaultRetrieveAgentId?: string
|
||||
/** Q&A-service id pinned by this deployment; when unset, the per-call fallback reads the BAILIAN_DEFAULT_CHAT_AGENT_ID credential. */
|
||||
defaultChatAgentId?: string
|
||||
/** Service version to call: `beta` (draft) or a published number; defaults to the latest published version. Never model-visible. */
|
||||
agentVersion?: string
|
||||
/** kb_chat timeout in milliseconds; the server side is a minutes-scale agentic loop. */
|
||||
chatTimeoutMs: number
|
||||
}
|
||||
|
||||
/** Schemastery validation for {@link Config}; workspaceId and defaultAgentId are optional — both resolve per call with a credentials fallback. */
|
||||
/** Schemastery validation for {@link Config}; workspaceId and default agent ids are optional — both resolve per call with a credentials fallback. */
|
||||
export const Config: z<Config> = z.object({
|
||||
workspaceId: z.string(),
|
||||
endpointHost: z.string().default('cn-beijing.maas.aliyuncs.com'),
|
||||
defaultAgentId: z.string(),
|
||||
defaultRetrieveAgentId: z.string(),
|
||||
defaultChatAgentId: z.string(),
|
||||
agentVersion: z.string(),
|
||||
chatTimeoutMs: z.number().default(300_000),
|
||||
})
|
||||
|
||||
/**
|
||||
* Register the three knowledge tools over one shared client, plus the
|
||||
* management skill when a skills registry is composed.
|
||||
* management skill when a skills registry is composed. The Config doubles as
|
||||
* the `bailian-kb` settings section (entry config as the base layer), so
|
||||
* every value is read through the live source thunk per call — tool schemas
|
||||
* are static (agent_id stays optional regardless), so a settings edit needs
|
||||
* no re-registration.
|
||||
* @param ctx - registrant context carrying tools and credentials.
|
||||
* @param config - deployment's workspace, host, pinning, and timeout choices.
|
||||
*/
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const pinnedWorkspaceId = config.workspaceId
|
||||
const pinnedAgentId = config.defaultAgentId
|
||||
// The active configuration source: the composition entry until a settings
|
||||
// service attaches, then the resolved section (schema defaults → entry
|
||||
// base → user layer). Detach falls back to the entry automatically.
|
||||
// Hand-rolled instead of `installSettingsSection` for two extras it does
|
||||
// not carry: the `expose` opt-in (this page edits the section from the
|
||||
// browser) and the scope handle the credential migration writes through.
|
||||
let current: () => Config = () => config
|
||||
ctx.inject(['settings'], (sctx) => {
|
||||
// `expose` is the wire opt-in the harness documents as deferred work; the
|
||||
// assertion keeps this compiling against pristine upstream types, which do
|
||||
// not declare it yet. Until upstream lands it the option is ignored and
|
||||
// the browser page degrades to its credentials-only fallback.
|
||||
const options = { base: config, expose: true } as SettingsRegisterOptions<Config>
|
||||
const scope = sctx.settings.register(SETTINGS_NS, Config, options)
|
||||
current = () => scope.get()
|
||||
sctx.effect(() => () => { current = () => config }, 'tool-bailian-kb: settings source fallback')
|
||||
void seedFromCredentials(ctx, scope)
|
||||
})
|
||||
|
||||
const client = new KbClient({
|
||||
resolveWorkspaceId: pinnedWorkspaceId === undefined
|
||||
? async () => {
|
||||
const resolved = await ctx.credentials.resolve(credentialRef('BAILIAN_WORKSPACE_ID'))
|
||||
if (!resolved) {
|
||||
throw new Error(
|
||||
'BAILIAN_WORKSPACE_ID is not configured. Set it in the web UI (Settings → Plugins → Bailian knowledge base) '
|
||||
+ 'or in ~/.dsh/.credentials.yaml; the workspace id appears as the subdomain of your Bailian endpoints.',
|
||||
)
|
||||
}
|
||||
return resolved.value
|
||||
}
|
||||
: async () => pinnedWorkspaceId,
|
||||
endpointHost: config.endpointHost,
|
||||
...(config.agentVersion ? { agentVersion: config.agentVersion } : {}),
|
||||
resolveWorkspaceId: async () => {
|
||||
const pinned = current().workspaceId
|
||||
if (pinned) return pinned
|
||||
const resolved = await ctx.credentials.resolve(credentialRef('BAILIAN_WORKSPACE_ID'))
|
||||
if (!resolved) {
|
||||
throw new Error(
|
||||
'BAILIAN_WORKSPACE_ID is not configured. Set the workspace id in the web UI (Settings → 百炼知识库) '
|
||||
+ 'or in ~/.dsh/.credentials.yaml; it appears as the subdomain of your Bailian endpoints.',
|
||||
)
|
||||
}
|
||||
return resolved.value
|
||||
},
|
||||
// Live settings reads: the client keeps no copy, so a committed edit to
|
||||
// the section applies on the next call.
|
||||
get endpointHost() { return current().endpointHost },
|
||||
get agentVersion() { return current().agentVersion },
|
||||
resolveApiKey: async () => {
|
||||
const resolved = await ctx.credentials.resolve(credentialRef('DASHSCOPE_API_KEY'))
|
||||
if (!resolved) {
|
||||
throw new Error(
|
||||
'DASHSCOPE_API_KEY is not configured. Set it in the web UI (Settings → Plugins → Bailian knowledge base) '
|
||||
'DASHSCOPE_API_KEY is not configured. Set it in the web UI (Settings → 百炼知识库) '
|
||||
+ 'or in ~/.dsh/.credentials.yaml (create a key at https://bailian.console.aliyun.com/?tab=app#/api-key).',
|
||||
)
|
||||
}
|
||||
@@ -74,13 +137,19 @@ export function apply(ctx: Context, config: Config): void {
|
||||
})
|
||||
for (const tool of createKbTools({
|
||||
client,
|
||||
resolveDefaultAgentId: pinnedAgentId !== undefined
|
||||
? async () => pinnedAgentId
|
||||
: async () => {
|
||||
const resolved = await ctx.credentials.resolve(credentialRef('BAILIAN_DEFAULT_AGENT_ID'))
|
||||
return resolved?.value
|
||||
},
|
||||
chatTimeoutMs: config.chatTimeoutMs,
|
||||
resolveDefaultRetrieveAgentId: async () => {
|
||||
const pinned = current().defaultRetrieveAgentId
|
||||
if (pinned) return pinned
|
||||
const resolved = await ctx.credentials.resolve(credentialRef('BAILIAN_DEFAULT_RETRIEVE_AGENT_ID'))
|
||||
return resolved?.value
|
||||
},
|
||||
resolveDefaultChatAgentId: async () => {
|
||||
const pinned = current().defaultChatAgentId
|
||||
if (pinned) return pinned
|
||||
const resolved = await ctx.credentials.resolve(credentialRef('BAILIAN_DEFAULT_CHAT_AGENT_ID'))
|
||||
return resolved?.value
|
||||
},
|
||||
get chatTimeoutMs() { return current().chatTimeoutMs },
|
||||
})) {
|
||||
ctx.tools.register(tool)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* The three model-facing knowledge tools. agent_id stays optional in the schema
|
||||
* regardless of deployment: the default service (patch config or credential)
|
||||
* regardless of deployment: the default services (patch config or credential)
|
||||
* can change at runtime through the credentials domain, so the fallback runs
|
||||
* per call and a missing default surfaces as an executable error instead of a
|
||||
* load-time schema difference.
|
||||
@@ -18,8 +18,11 @@ const DEFAULT_TOP_K = 5
|
||||
|
||||
export interface KbToolDeps {
|
||||
client: KbClient
|
||||
/** Resolves the default agent id per call (patch config or credential); omitted means no default. */
|
||||
resolveDefaultAgentId?: () => Promise<string | undefined>
|
||||
/** Resolves the default retrieval agent id per call (settings/patch config or credential); omitted means no default for kb_search. */
|
||||
resolveDefaultRetrieveAgentId?: () => Promise<string | undefined>
|
||||
/** Resolves the default chat agent id per call (settings/patch config or credential); omitted means no default for kb_chat. */
|
||||
resolveDefaultChatAgentId?: () => Promise<string | undefined>
|
||||
/** Read per call (a live-settings deployment supplies a getter). */
|
||||
chatTimeoutMs: number
|
||||
}
|
||||
|
||||
@@ -53,16 +56,26 @@ async function withServiceHint(client: KbClient, err: unknown): Promise<never> {
|
||||
* @returns definitions ready for `ctx.tools.register()`.
|
||||
*/
|
||||
export function createKbTools(deps: KbToolDeps) {
|
||||
const { client, resolveDefaultAgentId, chatTimeoutMs } = deps
|
||||
// chatTimeoutMs is deliberately NOT destructured: reading it off deps at
|
||||
// execute time keeps a live-settings getter live.
|
||||
const { client, resolveDefaultRetrieveAgentId, resolveDefaultChatAgentId } = deps
|
||||
const agentIdParam = {
|
||||
type: 'string' as const,
|
||||
description: 'Retrieval/Q&A service id; omit to use the default service when this deployment configures one (find ids via kb_service_list).',
|
||||
}
|
||||
const resolveAgentId = async (supplied: string | undefined): Promise<string> => {
|
||||
const resolveRetrieveAgentId = async (supplied: string | undefined): Promise<string> => {
|
||||
if (supplied !== undefined) return supplied
|
||||
const defaultId = resolveDefaultAgentId === undefined ? undefined : await resolveDefaultAgentId()
|
||||
const defaultId = resolveDefaultRetrieveAgentId === undefined ? undefined : await resolveDefaultRetrieveAgentId()
|
||||
if (defaultId === undefined) {
|
||||
throw new Error('agent_id is required: no default service is configured; discover services with kb_service_list')
|
||||
throw new Error('agent_id is required: no default retrieval service is configured; discover services with kb_service_list')
|
||||
}
|
||||
return defaultId
|
||||
}
|
||||
const resolveChatAgentId = async (supplied: string | undefined): Promise<string> => {
|
||||
if (supplied !== undefined) return supplied
|
||||
const defaultId = resolveDefaultChatAgentId === undefined ? undefined : await resolveDefaultChatAgentId()
|
||||
if (defaultId === undefined) {
|
||||
throw new Error('agent_id is required: no default chat service is configured; discover services with kb_service_list')
|
||||
}
|
||||
return defaultId
|
||||
}
|
||||
@@ -167,7 +180,7 @@ export function createKbTools(deps: KbToolDeps) {
|
||||
const topK = args.top_k ?? DEFAULT_TOP_K
|
||||
const body: SearchRequest = {
|
||||
query: args.query,
|
||||
agent_id: await resolveAgentId(args.agent_id),
|
||||
agent_id: await resolveRetrieveAgentId(args.agent_id),
|
||||
...(client.agentVersion ? { agent_version: client.agentVersion } : {}),
|
||||
...(args.images && args.images.length > 0 ? { images: args.images } : {}),
|
||||
}
|
||||
@@ -211,10 +224,11 @@ export function createKbTools(deps: KbToolDeps) {
|
||||
render: (_args, value) => [{ type: 'text', text: value.answer.length === 0 ? '(empty answer)' : value.answer }],
|
||||
},
|
||||
async execute(args) {
|
||||
const chatTimeoutMs = deps.chatTimeoutMs
|
||||
const body = {
|
||||
input: { messages: [{ role: 'user' as const, content: args.message }] },
|
||||
parameters: { agent_options: {
|
||||
agent_id: await resolveAgentId(args.agent_id),
|
||||
agent_id: await resolveChatAgentId(args.agent_id),
|
||||
...(client.agentVersion ? { agent_version: client.agentVersion } : {}),
|
||||
} },
|
||||
stream: true as const,
|
||||
|
||||
@@ -1,85 +1,47 @@
|
||||
/* Bailian card: header, credential fields, clear control, and save footer.
|
||||
Mirrors the host plugin-card chrome (an out-of-tree bundle cannot value-
|
||||
import the host card components, only their platform primitives). */
|
||||
/* Bailian settings page: title row, credential fields, clear control, and
|
||||
save footer, in the settings-panel design language (16/24 title, 14/22
|
||||
intro, `--dsw-alias-*` tokens — an out-of-tree bundle cannot value-import
|
||||
the host section components, only their platform primitives). */
|
||||
|
||||
.card {
|
||||
list-style: none;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
border-radius: 12px;
|
||||
background: var(--dsw-alias-bg-layer-3);
|
||||
transition: border-color .16s, background .16s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--dsw-alias-label-dimmed);
|
||||
}
|
||||
|
||||
/* An open card reads as the one being worked on, not merely taller. */
|
||||
.cardOpen {
|
||||
background: var(--dsw-alias-bg-layer-2);
|
||||
border-color: var(--dsw-alias-label-dimmed);
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
appearance: none;
|
||||
border: 0;
|
||||
background: none;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.header:focus-visible {
|
||||
outline: 2px solid var(--dsw-alias-brand-primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* Name over description: the description is what tells two plugins apart. */
|
||||
.headText {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
gap: 12px;
|
||||
max-width: 720px;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
.headRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.intro {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.chevron {
|
||||
flex: none;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
transition: transform .16s;
|
||||
/* The three controls grouped as one outlined object on the panel fill. */
|
||||
.form {
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
border-radius: 12px;
|
||||
padding: 4px 16px 12px;
|
||||
background: var(--dsw-alias-bg-layer-3);
|
||||
}
|
||||
|
||||
.chevronOpen {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.body {
|
||||
border-top: 1px solid var(--dsw-alias-border-l2);
|
||||
margin: 0 16px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Carried on the header so a collapsed card still says it holds edits. */
|
||||
/* Carried beside the title so a scrolled page still says it holds edits. */
|
||||
.pending {
|
||||
flex: none;
|
||||
border-radius: 999px;
|
||||
@@ -92,6 +54,14 @@
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
/* Degraded-transport notice: the write-only fallback explains itself once. */
|
||||
.notice {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--dsw-alias-state-warn-label);
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -197,7 +167,7 @@
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 12px 0 4px;
|
||||
padding: 12px 0 0;
|
||||
border-top: 1px solid var(--dsw-alias-border-l2);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
/**
|
||||
* The Bailian knowledge-base card: three write-only credential controls plus
|
||||
* the default-service clear. Values never ride a response, so each control
|
||||
* starts blank and reports only configured/unconfigured; the API key drafts
|
||||
* behind a password mask while the workspace and agent ids draft in the clear
|
||||
* — they are pasted identifiers, not secrets, and a visible draft can be
|
||||
* proofread.
|
||||
* The Bailian knowledge-base settings page: one section page in the
|
||||
* Settings left nav. The workspace, default-retrieval-service and
|
||||
* default-chat-service ids echo from the `bailian-kb` settings section
|
||||
* while the scope is ready (clearing one falls back down the resolution
|
||||
* chain), and degrade to write-only credential controls otherwise; the
|
||||
* API key is always write-only — it drafts behind a password mask,
|
||||
* starts blank, and reports only configured/unconfigured.
|
||||
*/
|
||||
|
||||
import { useState } from 'react'
|
||||
import { IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { BAILIAN_CARD_REFS, type BailianCardFace, type BailianFieldKey } from './bailian-card-controller.ts'
|
||||
import {
|
||||
BAILIAN_CARD_REFS, dirtyOf, echoedValue, SETTINGS_FIELDS,
|
||||
type BailianCardFace, type BailianFieldKey,
|
||||
} from './bailian-card-controller.ts'
|
||||
import type { BailianKbLocaleKey } from './locales.ts'
|
||||
import css from './BailianCard.module.css'
|
||||
|
||||
/** Props the renderer binds for the Bailian card. */
|
||||
/** Props the renderer binds for the Bailian section page. */
|
||||
export type BailianCardProps =
|
||||
PropsRuntime<'settings.plugin.item'>
|
||||
PropsRuntime<'settings.section'>
|
||||
& PropsLocale<'tool-bailian-kb'>
|
||||
& InjectFace<BailianCardFace>
|
||||
|
||||
@@ -24,113 +26,123 @@ export type BailianCardProps =
|
||||
interface FieldView {
|
||||
key: BailianFieldKey
|
||||
labelKey: BailianKbLocaleKey
|
||||
/** Echo-mode explanation (settings-backed value, blank save = fall back). */
|
||||
hintKey: BailianKbLocaleKey
|
||||
/** Write-only explanation (credential store, blank = keep the stored value). */
|
||||
fallbackHintKey: BailianKbLocaleKey
|
||||
setKey: BailianKbLocaleKey
|
||||
unsetKey: BailianKbLocaleKey
|
||||
/** Password-masked drafting; only the API key is an actual secret. */
|
||||
secret: boolean
|
||||
}
|
||||
|
||||
/** The three controls, in card order. */
|
||||
/** The controls, in page order. */
|
||||
const FIELDS: readonly FieldView[] = [
|
||||
{ key: 'DASHSCOPE_API_KEY', labelKey: 'apiKey', hintKey: 'apiKeyHint', setKey: 'apiKeySet', unsetKey: 'apiKeyUnset', secret: true },
|
||||
{ key: 'BAILIAN_WORKSPACE_ID', labelKey: 'workspaceId', hintKey: 'workspaceIdHint', setKey: 'workspaceIdSet', unsetKey: 'workspaceIdUnset', secret: false },
|
||||
{ key: 'BAILIAN_DEFAULT_AGENT_ID', labelKey: 'agentId', hintKey: 'agentIdHint', setKey: 'agentIdSet', unsetKey: 'agentIdUnset', secret: false },
|
||||
{ key: 'DASHSCOPE_API_KEY', labelKey: 'apiKey', hintKey: 'apiKeyHint', fallbackHintKey: 'apiKeyHint', setKey: 'apiKeySet', unsetKey: 'apiKeyUnset', secret: true },
|
||||
{ key: 'BAILIAN_WORKSPACE_ID', labelKey: 'workspaceId', hintKey: 'workspaceIdHint', fallbackHintKey: 'workspaceIdHintFallback', setKey: 'workspaceIdSet', unsetKey: 'workspaceIdUnset', secret: false },
|
||||
{ key: 'BAILIAN_DEFAULT_RETRIEVE_AGENT_ID', labelKey: 'retrieveAgentId', hintKey: 'retrieveAgentIdHint', fallbackHintKey: 'retrieveAgentIdHintFallback', setKey: 'retrieveAgentIdSet', unsetKey: 'retrieveAgentIdUnset', secret: false },
|
||||
{ key: 'BAILIAN_DEFAULT_CHAT_AGENT_ID', labelKey: 'chatAgentId', hintKey: 'chatAgentIdHint', fallbackHintKey: 'chatAgentIdHintFallback', setKey: 'chatAgentIdSet', unsetKey: 'chatAgentIdUnset', secret: false },
|
||||
]
|
||||
|
||||
/**
|
||||
* Render the Bailian card.
|
||||
* @param props - locale copy, the card snapshot, and its actions.
|
||||
* @returns the card.
|
||||
* Render the Bailian section page.
|
||||
* @param props - locale copy, the page snapshot, and its actions.
|
||||
* @returns the section page.
|
||||
*/
|
||||
export function BailianCard(props: BailianCardProps) {
|
||||
const { t } = props
|
||||
const state = props.useBailianCard(snapshot => snapshot)
|
||||
const [open, setOpen] = useState(false)
|
||||
const dirty = BAILIAN_CARD_REFS.some(key => state.drafts[key] !== '')
|
||||
const dirty = dirtyOf(state)
|
||||
const busy = state.saving || state.clearing
|
||||
return (
|
||||
<li className={css.card + (open ? ` ${css.cardOpen}` : '')}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.header}
|
||||
aria-expanded={open}
|
||||
aria-label={`${props.t(open ? 'collapse' : 'expand')}: ${props.t('title')}`}
|
||||
onClick={() => { setOpen(!open) }}
|
||||
>
|
||||
<span className={css.headText}>
|
||||
<span className={css.name}>{t('title')}</span>
|
||||
<span className={css.description}>{t('description')}</span>
|
||||
</span>
|
||||
<section className={css.section}>
|
||||
<div className={css.headRow}>
|
||||
<h2 className={css.title}>{t('title')}</h2>
|
||||
{dirty ? <span className={css.pending}>{t('unsaved')}</span> : null}
|
||||
<IconChevronDownOutline14 className={css.chevron + (open ? ` ${css.chevronOpen}` : '')} />
|
||||
</button>
|
||||
{open
|
||||
? (
|
||||
<div className={css.body}>
|
||||
{FIELDS.map(field => {
|
||||
const credential = state.credentials[field.key]
|
||||
// The launch environment wins and refuses writes: the badge says
|
||||
// where the value lives instead of a control that cannot act.
|
||||
const stateLabel = credential.configured
|
||||
? (credential.writable ? t(field.setKey) : t('fromEnv'))
|
||||
: t(field.unsetKey)
|
||||
const showClear = field.key === 'BAILIAN_DEFAULT_AGENT_ID' && credential.configured
|
||||
return (
|
||||
<div className={css.field} key={field.key}>
|
||||
<div className={css.head}>
|
||||
<label className={css.label} htmlFor={`bailian-kb-${field.key}`}>{t(field.labelKey)}</label>
|
||||
<span className={css.badges}>
|
||||
{showClear
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
className={css.clear}
|
||||
disabled={busy || !credential.writable}
|
||||
onClick={() => { void props.clearDefaultAgent() }}
|
||||
>
|
||||
{t(state.clearing ? 'clearing' : 'clear')}
|
||||
</button>
|
||||
)
|
||||
: null}
|
||||
<span className={credential.configured ? css.badge : css.badgeMuted}>{stateLabel}</span>
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
id={`bailian-kb-${field.key}`}
|
||||
className={css.input}
|
||||
type={field.secret ? 'password' : 'text'}
|
||||
autoComplete="off"
|
||||
value={state.drafts[field.key]}
|
||||
disabled={!credential.writable || busy}
|
||||
onChange={(event) => { props.edit(field.key, event.target.value) }}
|
||||
/>
|
||||
<p className={css.hint}>{t(field.hintKey)}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className={css.footer}>
|
||||
{state.failed ? <p className={css.failed} role="status">{t('saveFailed')}</p> : null}
|
||||
<button
|
||||
type="button"
|
||||
className={css.discard}
|
||||
disabled={!dirty || busy}
|
||||
onClick={props.discard}
|
||||
>
|
||||
{t('discard')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={css.save}
|
||||
disabled={!dirty || busy}
|
||||
onClick={() => { void props.save() }}
|
||||
>
|
||||
{t(state.saving ? 'saving' : 'save')}
|
||||
</button>
|
||||
</div>
|
||||
<p className={css.intro}>{t('description')}</p>
|
||||
<div className={css.form}>
|
||||
{state.settings.status === 'unavailable'
|
||||
? <p className={css.notice}>{t('settingsUnavailable')}</p>
|
||||
: null}
|
||||
{FIELDS.map(field => {
|
||||
const credential = state.credentials[field.key]
|
||||
// Echo mode: the settings scope answers with the resolved value, so
|
||||
// the control is an ordinary pre-filled input. Otherwise the control
|
||||
// is write-only and the badge is all the state there is.
|
||||
const echo = SETTINGS_FIELDS[field.key] !== undefined && state.settings.status === 'ready'
|
||||
const echoed = echoedValue(state, field.key)
|
||||
const value = state.drafts[field.key] ?? (echo ? echoed : '')
|
||||
const disabled = busy || (echo ? !state.settings.writable : !credential.writable)
|
||||
// The launch environment wins over the credential store and refuses
|
||||
// writes; in echo mode a non-empty settings value shadows both, so
|
||||
// the badge only reports the fallback under an empty input.
|
||||
const badge = echo
|
||||
? (echoed !== ''
|
||||
? undefined
|
||||
: credential.configured
|
||||
? { label: t('fallbackConfigured'), set: true }
|
||||
: { label: t(field.unsetKey), set: false })
|
||||
: credential.configured
|
||||
? { label: credential.writable ? t(field.setKey) : t('fromEnv'), set: true }
|
||||
: { label: t(field.unsetKey), set: false }
|
||||
const showClear = (field.key === 'BAILIAN_DEFAULT_RETRIEVE_AGENT_ID' || field.key === 'BAILIAN_DEFAULT_CHAT_AGENT_ID')
|
||||
&& (credential.configured || (echo && echoed !== ''))
|
||||
return (
|
||||
<div className={css.field} key={field.key}>
|
||||
<div className={css.head}>
|
||||
<label className={css.label} htmlFor={`bailian-kb-${field.key}`}>{t(field.labelKey)}</label>
|
||||
<span className={css.badges}>
|
||||
{showClear
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
className={css.clear}
|
||||
disabled={busy}
|
||||
onClick={() => { void props.clearDefaultAgent(field.key as 'BAILIAN_DEFAULT_RETRIEVE_AGENT_ID' | 'BAILIAN_DEFAULT_CHAT_AGENT_ID') }}
|
||||
>
|
||||
{t(state.clearing ? 'clearing' : 'clear')}
|
||||
</button>
|
||||
)
|
||||
: null}
|
||||
{badge !== undefined
|
||||
? <span className={badge.set ? css.badge : css.badgeMuted}>{badge.label}</span>
|
||||
: null}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
id={`bailian-kb-${field.key}`}
|
||||
className={css.input}
|
||||
type={field.secret ? 'password' : 'text'}
|
||||
autoComplete="off"
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={(event) => { props.edit(field.key, event.target.value) }}
|
||||
/>
|
||||
<p className={css.hint}>{t(echo ? field.hintKey : field.fallbackHintKey)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
<div className={css.footer}>
|
||||
{state.failed ? <p className={css.failed} role="status">{t('saveFailed')}</p> : null}
|
||||
<button
|
||||
type="button"
|
||||
className={css.discard}
|
||||
disabled={!dirty || busy}
|
||||
onClick={props.discard}
|
||||
>
|
||||
{t('discard')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={css.save}
|
||||
disabled={!dirty || busy}
|
||||
onClick={() => { void props.save() }}
|
||||
>
|
||||
{t(state.saving ? 'saving' : 'save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,51 @@
|
||||
/**
|
||||
* The Bailian card's controller: staged drafts over the credentials domain.
|
||||
* The Bailian page's controller: a hybrid form over two domains.
|
||||
*
|
||||
* All three values ride credential references (no settings namespace is
|
||||
* involved — an out-of-tree package cannot expose one to the browser), so the
|
||||
* card never holds a stored literal: it learns only whether each reference is
|
||||
* configured and writable, stages drafts locally, and one save writes every
|
||||
* non-blank draft through `credentials.set`. A blank draft writes nothing and
|
||||
* keeps the stored value. The default-service reference is the one value a
|
||||
* user can meaningfully remove, so it alone gets a clear action
|
||||
* (`credentials.unset`), immediate rather than staged.
|
||||
* The workspace, default-retrieval-service and default-chat-service ids live
|
||||
* in the `bailian-kb` settings section the Host half registers, so while the
|
||||
* settings scope is `ready` they ECHO: the page shows the resolved value and
|
||||
* stages edits over it (clearing a field unsets the user layer, falling back
|
||||
* to the entry config and then the credential store). When the scope is
|
||||
* unavailable — a remote browser (memory mode) or a composition without a
|
||||
* settings service — both fields degrade to the original write-only credential
|
||||
* controls.
|
||||
*
|
||||
* The API key always rides its credential reference (write-only by design:
|
||||
* the wire is structurally value-free), so that control starts blank and
|
||||
* reports only configured/unconfigured.
|
||||
*/
|
||||
|
||||
import type { IApiClient } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { createSnapshotStore, type SettingsScope, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
/** The credential references this card stages, keyed by their ref names. */
|
||||
/** The credential references this page addresses, keyed by their ref names. */
|
||||
export const BAILIAN_CARD_REFS = [
|
||||
'DASHSCOPE_API_KEY',
|
||||
'BAILIAN_WORKSPACE_ID',
|
||||
'BAILIAN_DEFAULT_AGENT_ID',
|
||||
'BAILIAN_DEFAULT_RETRIEVE_AGENT_ID',
|
||||
'BAILIAN_DEFAULT_CHAT_AGENT_ID',
|
||||
] as const
|
||||
|
||||
/** One card field, addressed by its credential reference. */
|
||||
/** One page field, addressed by its credential reference. */
|
||||
export type BailianFieldKey = (typeof BAILIAN_CARD_REFS)[number]
|
||||
|
||||
/** Settings-section field names of the echoing controls. */
|
||||
export type BailianSettingsField = 'workspaceId' | 'defaultRetrieveAgentId' | 'defaultChatAgentId'
|
||||
|
||||
/** Credential reference → settings-section field, for the hybrid controls. */
|
||||
export const SETTINGS_FIELDS: Partial<Record<BailianFieldKey, BailianSettingsField>> = {
|
||||
BAILIAN_WORKSPACE_ID: 'workspaceId',
|
||||
BAILIAN_DEFAULT_RETRIEVE_AGENT_ID: 'defaultRetrieveAgentId',
|
||||
BAILIAN_DEFAULT_CHAT_AGENT_ID: 'defaultChatAgentId',
|
||||
}
|
||||
|
||||
/** The section subset this page reads and writes (the namespace holds the whole plugin Config). */
|
||||
export interface BailianKbSection {
|
||||
workspaceId?: string
|
||||
defaultRetrieveAgentId?: string
|
||||
defaultChatAgentId?: string
|
||||
}
|
||||
|
||||
/** What the credentials domain reports for one reference (never the value). */
|
||||
export interface BailianCredentialView {
|
||||
/** Whether any layer supplies a value for the reference. */
|
||||
@@ -32,65 +54,125 @@ export interface BailianCredentialView {
|
||||
writable: boolean
|
||||
}
|
||||
|
||||
/** What the Bailian card renders. */
|
||||
/** The page's mirror of the settings scope. */
|
||||
export interface BailianSettingsView {
|
||||
/** `ready` enables echo; `unavailable` degrades to write-only credentials. */
|
||||
status: 'loading' | 'ready' | 'unavailable'
|
||||
/** Whether the Host settings document accepts writes. */
|
||||
writable: boolean
|
||||
/** Resolved section values (entry base + user layer) for the two hybrid controls. */
|
||||
values: BailianKbSection
|
||||
}
|
||||
|
||||
/** What the Bailian page renders. */
|
||||
export interface BailianCardState {
|
||||
/** Staged drafts, blank = keep the stored value. */
|
||||
drafts: Record<BailianFieldKey, string>
|
||||
/** Staged drafts; undefined = untouched (the control shows the echoed value). */
|
||||
drafts: Record<BailianFieldKey, string | undefined>
|
||||
/** Last credentials-domain answer per reference; unknown refs read as writable. */
|
||||
credentials: Record<BailianFieldKey, BailianCredentialView>
|
||||
/** Settings-scope echo state for the id fields. */
|
||||
settings: BailianSettingsView
|
||||
/** Whether a save is in flight. */
|
||||
saving: boolean
|
||||
/** Whether the default-service clear is in flight. */
|
||||
/** Whether a default-service clear is in flight. */
|
||||
clearing: boolean
|
||||
/** Whether the last save or clear was refused; drafts are kept for correction. */
|
||||
failed: boolean
|
||||
}
|
||||
|
||||
/** The registration-side face the card's slot entry injects. */
|
||||
/** The registration-side face the page's slot entry injects. */
|
||||
export interface BailianCardFace {
|
||||
hooks: {
|
||||
/** Card snapshot bound by the renderer as useBailianCard. */
|
||||
/** Page snapshot bound by the renderer as useBailianCard. */
|
||||
bailianCard: SnapshotStore<BailianCardState>
|
||||
}
|
||||
/** Stage one draft. */
|
||||
edit: (key: BailianFieldKey, text: string) => void
|
||||
/** Write every non-blank draft through `credentials.set`, then re-read. */
|
||||
/** Write every staged draft through its domain, then re-read. */
|
||||
save: () => Promise<void>
|
||||
/** Drop every staged draft. */
|
||||
discard: () => void
|
||||
/** Remove the stored default service (`credentials.unset`), then re-read. */
|
||||
clearDefaultAgent: () => Promise<void>
|
||||
/** Remove the stored default service from every writable layer, then re-read. */
|
||||
clearDefaultAgent: (key: 'BAILIAN_DEFAULT_RETRIEVE_AGENT_ID' | 'BAILIAN_DEFAULT_CHAT_AGENT_ID') => Promise<void>
|
||||
}
|
||||
|
||||
/** Bridge the credentials domain onto the card. */
|
||||
/** The text a field's control shows when its draft is untouched. */
|
||||
export function echoedValue(state: BailianCardState, key: BailianFieldKey): string {
|
||||
const field = SETTINGS_FIELDS[key]
|
||||
if (field === undefined || state.settings.status !== 'ready') return ''
|
||||
return state.settings.values[field] ?? ''
|
||||
}
|
||||
|
||||
/** Whether one field stages a change a save would write. */
|
||||
function staged(state: BailianCardState, key: BailianFieldKey): boolean {
|
||||
const draft = state.drafts[key]
|
||||
if (draft === undefined) return false
|
||||
const field = SETTINGS_FIELDS[key]
|
||||
if (field !== undefined && state.settings.status === 'ready') {
|
||||
return draft !== echoedValue(state, key)
|
||||
}
|
||||
// Write-only control: blank means untouched, never "erase the stored value".
|
||||
return draft !== ''
|
||||
}
|
||||
|
||||
/** Whether any field stages a change (enables Save/Discard). */
|
||||
export function dirtyOf(state: BailianCardState): boolean {
|
||||
return BAILIAN_CARD_REFS.some(key => staged(state, key))
|
||||
}
|
||||
|
||||
/** Bridge the settings scope and the credentials domain onto the page. */
|
||||
export class BailianCardController {
|
||||
private readonly store: SnapshotStore<BailianCardState>
|
||||
|
||||
/**
|
||||
* @param api - wire face used for the three credential references.
|
||||
* @param scope - the bound `bailian-kb` settings scope (echo transport).
|
||||
*/
|
||||
constructor(private readonly api: Pick<IApiClient, 'credentials'>) {
|
||||
constructor(
|
||||
private readonly api: Pick<IApiClient, 'credentials'>,
|
||||
private readonly scope: SettingsScope<BailianKbSection>,
|
||||
) {
|
||||
this.store = createSnapshotStore<BailianCardState>({
|
||||
drafts: {
|
||||
DASHSCOPE_API_KEY: '',
|
||||
BAILIAN_WORKSPACE_ID: '',
|
||||
BAILIAN_DEFAULT_AGENT_ID: '',
|
||||
DASHSCOPE_API_KEY: undefined,
|
||||
BAILIAN_WORKSPACE_ID: undefined,
|
||||
BAILIAN_DEFAULT_RETRIEVE_AGENT_ID: undefined,
|
||||
BAILIAN_DEFAULT_CHAT_AGENT_ID: undefined,
|
||||
},
|
||||
credentials: {
|
||||
DASHSCOPE_API_KEY: { configured: false, writable: true },
|
||||
BAILIAN_WORKSPACE_ID: { configured: false, writable: true },
|
||||
BAILIAN_DEFAULT_AGENT_ID: { configured: false, writable: true },
|
||||
BAILIAN_DEFAULT_RETRIEVE_AGENT_ID: { configured: false, writable: true },
|
||||
BAILIAN_DEFAULT_CHAT_AGENT_ID: { configured: false, writable: true },
|
||||
},
|
||||
settings: { status: 'loading', writable: false, values: {} },
|
||||
saving: false,
|
||||
clearing: false,
|
||||
failed: false,
|
||||
})
|
||||
this.syncSettings()
|
||||
void this.read()
|
||||
}
|
||||
|
||||
/** Whether any draft is staged. */
|
||||
get dirty(): boolean {
|
||||
return BAILIAN_CARD_REFS.some(key => this.store.getSnapshot().drafts[key] !== '')
|
||||
/**
|
||||
* Mirror the scope snapshot into the page state. Called at construction and
|
||||
* from the registration-side subscription (the scope self-refreshes on
|
||||
* pushed document invalidations and connection resets).
|
||||
*/
|
||||
syncSettings(): void {
|
||||
const snapshot = this.scope.getSnapshot()
|
||||
const value = snapshot.value
|
||||
this.store.update(draft => {
|
||||
draft.settings = {
|
||||
status: snapshot.status,
|
||||
writable: snapshot.writable,
|
||||
values: {
|
||||
...(value?.workspaceId !== undefined ? { workspaceId: value.workspaceId } : {}),
|
||||
...(value?.defaultRetrieveAgentId !== undefined ? { defaultRetrieveAgentId: value.defaultRetrieveAgentId } : {}),
|
||||
...(value?.defaultChatAgentId !== undefined ? { defaultChatAgentId: value.defaultChatAgentId } : {}),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,66 +189,94 @@ export class BailianCardController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Write every non-blank draft, then re-read all references. A refused write
|
||||
* keeps its draft: the copy tells the user the values were left to correct.
|
||||
* Write every staged draft through its domain: echoing fields go to the
|
||||
* settings user layer (blank = unset, falling back to entry config and the
|
||||
* credential store), write-only fields go to `credentials.set`. A refused
|
||||
* credential write keeps its draft; a refused settings write self-heals by
|
||||
* the scope's own recovery read (the control snaps back to the Host value).
|
||||
*/
|
||||
async save(): Promise<void> {
|
||||
const staged = new Map(
|
||||
BAILIAN_CARD_REFS
|
||||
.map(key => [key, this.store.getSnapshot().drafts[key]] as const)
|
||||
.filter(([, text]) => text !== ''),
|
||||
)
|
||||
if (staged.size === 0 || this.store.getSnapshot().saving) return
|
||||
const state = this.store.getSnapshot()
|
||||
if (state.saving || !dirtyOf(state)) return
|
||||
this.store.update(draft => { draft.saving = true })
|
||||
let failed = false
|
||||
await Promise.all([...staged].map(async ([ref, value]) => {
|
||||
try {
|
||||
const response = await this.api.credentials.set({ ref, value })
|
||||
if (!response.result.ok) failed = true
|
||||
} catch (_credentialWriteFailure) {
|
||||
failed = true
|
||||
const writes: Promise<void>[] = []
|
||||
const settled: BailianFieldKey[] = []
|
||||
for (const key of BAILIAN_CARD_REFS) {
|
||||
if (!staged(state, key)) continue
|
||||
const text = state.drafts[key] as string
|
||||
const field = SETTINGS_FIELDS[key]
|
||||
if (field !== undefined && state.settings.status === 'ready') {
|
||||
writes.push(text === '' ? this.scope.unset(field) : this.scope.set(field, text))
|
||||
settled.push(key)
|
||||
continue
|
||||
}
|
||||
}))
|
||||
writes.push((async () => {
|
||||
try {
|
||||
const response = await this.api.credentials.set({ ref: key, value: text })
|
||||
if (response.result.ok) settled.push(key)
|
||||
else failed = true
|
||||
} catch (_credentialWriteFailure) {
|
||||
failed = true
|
||||
}
|
||||
})())
|
||||
}
|
||||
await Promise.all(writes)
|
||||
this.store.update(draft => {
|
||||
draft.saving = false
|
||||
draft.failed = failed
|
||||
if (!failed) for (const ref of staged.keys()) draft.drafts[ref] = ''
|
||||
for (const key of settled) draft.drafts[key] = undefined
|
||||
})
|
||||
this.syncSettings()
|
||||
await this.read()
|
||||
}
|
||||
|
||||
/** Drop every staged draft and the failure mark. */
|
||||
discard(): void {
|
||||
this.store.update(draft => {
|
||||
for (const ref of BAILIAN_CARD_REFS) draft.drafts[ref] = ''
|
||||
for (const ref of BAILIAN_CARD_REFS) draft.drafts[ref] = undefined
|
||||
draft.failed = false
|
||||
})
|
||||
}
|
||||
|
||||
/** Remove the stored default service so every call names one again. */
|
||||
async clearDefaultAgent(): Promise<void> {
|
||||
if (this.store.getSnapshot().clearing) return
|
||||
/**
|
||||
* Remove the stored default service from every writable layer — the
|
||||
* settings user layer AND the credential store, so the fallback chain does
|
||||
* not resurrect the value the user just cleared. Both removals are
|
||||
* idempotent; the credential unset is skipped when nothing is stored there.
|
||||
* @param key - which default service credential to clear.
|
||||
*/
|
||||
async clearDefaultAgent(key: 'BAILIAN_DEFAULT_RETRIEVE_AGENT_ID' | 'BAILIAN_DEFAULT_CHAT_AGENT_ID'): Promise<void> {
|
||||
const settingsField: BailianSettingsField = key === 'BAILIAN_DEFAULT_RETRIEVE_AGENT_ID' ? 'defaultRetrieveAgentId' : 'defaultChatAgentId'
|
||||
const state = this.store.getSnapshot()
|
||||
if (state.clearing) return
|
||||
this.store.update(draft => { draft.clearing = true })
|
||||
let failed = false
|
||||
try {
|
||||
const response = await this.api.credentials.unset({ ref: 'BAILIAN_DEFAULT_AGENT_ID' })
|
||||
if (!response.result.ok) failed = true
|
||||
} catch (_credentialWriteFailure) {
|
||||
failed = true
|
||||
if (state.settings.status === 'ready') await this.scope.unset(settingsField)
|
||||
if (state.credentials[key].configured) {
|
||||
try {
|
||||
const response = await this.api.credentials.unset({ ref: key })
|
||||
if (!response.result.ok) failed = true
|
||||
} catch (_credentialWriteFailure) {
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
this.store.update(draft => {
|
||||
draft.clearing = false
|
||||
draft.failed = failed
|
||||
draft.drafts[key] = undefined
|
||||
})
|
||||
this.syncSettings()
|
||||
await this.read()
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-read after the Host reports a change to a reference this card watches.
|
||||
* Re-read after the Host reports a change to a reference this page watches.
|
||||
*
|
||||
* A value can be written from somewhere else — the Models page addresses
|
||||
* DASHSCOPE_API_KEY too, and the file store accepts external edits — so
|
||||
* without this the badges keep reporting a state the Host already replaced.
|
||||
* (Settings-document changes reach the page through the scope instead.)
|
||||
* @param ref - the reference the Host reports as changed.
|
||||
*/
|
||||
refresh(ref: string): void {
|
||||
@@ -175,8 +285,8 @@ export class BailianCardController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the face the card's slot registration injects.
|
||||
* @returns the card's snapshot and its actions.
|
||||
* Build the face the page's slot registration injects.
|
||||
* @returns the page's snapshot and its actions.
|
||||
*/
|
||||
inject(): BailianCardFace {
|
||||
return {
|
||||
@@ -184,13 +294,13 @@ export class BailianCardController {
|
||||
edit: (key, text) => { this.edit(key, text) },
|
||||
save: () => this.save(),
|
||||
discard: () => { this.discard() },
|
||||
clearDefaultAgent: () => this.clearDefaultAgent(),
|
||||
clearDefaultAgent: (key) => this.clearDefaultAgent(key),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the credentials domain about all three references and publish the
|
||||
* answer. A failed read keeps the last known state: the card stays usable
|
||||
* answer. A failed read keeps the last known state: the page stays usable
|
||||
* and a write still reaches the Host.
|
||||
*/
|
||||
private async read(): Promise<void> {
|
||||
@@ -205,7 +315,7 @@ export class BailianCardController {
|
||||
this.store.update(draft => {
|
||||
for (const ref of BAILIAN_CARD_REFS) {
|
||||
// An unknown reference reads as writable: the control stays usable and
|
||||
// the Host is what refuses, rather than the card guessing a refusal.
|
||||
// the Host is what refuses, rather than the page guessing a refusal.
|
||||
draft.credentials[ref] = {
|
||||
configured: view[ref]?.configured ?? false,
|
||||
writable: view[ref]?.writable ?? true,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
/**
|
||||
* Bailian knowledge-base plugin, browser half: one card in the plugin
|
||||
* configuration section staging the three credential references the Host half
|
||||
* resolves per call. The card is pure credentials-domain — this package
|
||||
* exposes no settings namespace (an out-of-tree package cannot get one onto
|
||||
* the browser settings surface), so nothing here touches a settings scope.
|
||||
* Bailian knowledge-base plugin, browser half: one section page in the
|
||||
* Settings left nav. The workspace, default-retrieval-service and
|
||||
* default-chat-service ids ride the `bailian-kb` settings namespace the
|
||||
* Host half registers (echoing values through `ctx.settingsScope`,
|
||||
* degrading to write-only credential controls when the scope is
|
||||
* unavailable); the API key stays pure credentials-domain and never
|
||||
* echoes.
|
||||
*/
|
||||
|
||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
||||
@@ -14,16 +16,17 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
// credential-update events.
|
||||
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
||||
// Type-only: the 'settings.plugin.item' SlotMap merge, declared by the plugins
|
||||
// settings section this card registers into.
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
|
||||
// The 'settings.section' SlotMap merge AND the ctx.settingsScope service,
|
||||
// both declared by the settings domain base (type-only: the service arrives
|
||||
// through cordis, never a value import).
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import { BailianCard } from './BailianCard.tsx'
|
||||
import { BailianCardController } from './bailian-card-controller.ts'
|
||||
import { BailianCardController, type BailianKbSection } from './bailian-card-controller.ts'
|
||||
import { en, zh, type BailianKbLocaleKey } from './locales.ts'
|
||||
|
||||
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
interface LocaleNamespaceMap {
|
||||
/** The Bailian card's copy. */
|
||||
/** The Bailian section page's copy. */
|
||||
'tool-bailian-kb': BailianKbLocaleKey
|
||||
}
|
||||
}
|
||||
@@ -32,17 +35,29 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
const NS = 'tool-bailian-kb'
|
||||
|
||||
/** Required services (cordis fiber inject). */
|
||||
export const inject = ['slots', 'locale', 'connection', 'remote']
|
||||
export const inject = ['slots', 'locale', 'connection', 'remote', 'settingsScope']
|
||||
|
||||
/**
|
||||
* Mount the Bailian card into the plugin configuration section.
|
||||
* Mount the Bailian section page into the Settings left nav.
|
||||
* @param ctx - the browser plugin context.
|
||||
*/
|
||||
export function apply(ctx: ClientContext): void {
|
||||
const { api } = ctx.get('connection') as ConnectionHandle
|
||||
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'tool-bailian-kb: dictionaries')
|
||||
|
||||
const card = new BailianCardController(api)
|
||||
// Registration-time text: the nav label is a thunk the shell resolves per
|
||||
// render, so copy freshness rides the locale revision without re-registering.
|
||||
const t = ctx.locale.bind(NS)
|
||||
|
||||
// The echo transport: bound on this fiber, self-refreshing on pushed
|
||||
// settings-document invalidations and connection resets. A remote browser
|
||||
// binds in memory mode and the page degrades to write-only controls.
|
||||
const scope = ctx.settingsScope.bind<BailianKbSection>({ namespace: 'bailian-kb' })
|
||||
const card = new BailianCardController(api, scope)
|
||||
ctx.effect(
|
||||
() => scope.subscribe(() => { card.syncSettings() }),
|
||||
'tool-bailian-kb: settings echo',
|
||||
)
|
||||
// Values can change elsewhere (Models page, external file edits); the badges
|
||||
// must follow the Host, not the card's last write.
|
||||
ctx.effect(
|
||||
@@ -50,10 +65,11 @@ export function apply(ctx: ClientContext): void {
|
||||
'tool-bailian-kb: credential invalidations',
|
||||
)
|
||||
|
||||
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
|
||||
name: 'settings.plugin.item',
|
||||
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
||||
name: 'settings.section',
|
||||
id: 'bailian-kb',
|
||||
order: 30,
|
||||
order: 20,
|
||||
label: () => t('nav'),
|
||||
locale: NS,
|
||||
inject: () => card.inject(),
|
||||
}, BailianCard))
|
||||
|
||||
@@ -1,40 +1,51 @@
|
||||
/**
|
||||
* Locale bundles for the Bailian knowledge-base plugin card. The card rides
|
||||
* the credentials domain for all three values, so every copy is written for
|
||||
* write-only controls: state is reported as configured/unconfigured, and a
|
||||
* stored value is never echoed back.
|
||||
* Locale bundles for the Bailian knowledge-base settings page. The workspace,
|
||||
* default-retrieval-service and default-chat-service ids echo from the
|
||||
* settings section while it is available and fall back to write-only
|
||||
* credential controls otherwise; the API key copy is always written for a
|
||||
* write-only control: state is reported as configured/unconfigured, and a
|
||||
* stored key is never echoed back.
|
||||
*/
|
||||
|
||||
/** Locale keys this card renders. */
|
||||
/** Locale keys this page renders. */
|
||||
export type BailianKbLocaleKey =
|
||||
| 'title' | 'description'
|
||||
| 'nav' | 'title' | 'description' | 'settingsUnavailable' | 'fallbackConfigured'
|
||||
| 'apiKey' | 'apiKeyHint' | 'apiKeySet' | 'apiKeyUnset'
|
||||
| 'workspaceId' | 'workspaceIdHint' | 'workspaceIdSet' | 'workspaceIdUnset'
|
||||
| 'agentId' | 'agentIdHint' | 'agentIdSet' | 'agentIdUnset'
|
||||
| 'fromEnv' | 'clear' | 'clearing' | 'expand' | 'collapse'
|
||||
| 'workspaceId' | 'workspaceIdHint' | 'workspaceIdHintFallback' | 'workspaceIdSet' | 'workspaceIdUnset'
|
||||
| 'retrieveAgentId' | 'retrieveAgentIdHint' | 'retrieveAgentIdHintFallback' | 'retrieveAgentIdSet' | 'retrieveAgentIdUnset'
|
||||
| 'chatAgentId' | 'chatAgentIdHint' | 'chatAgentIdHintFallback' | 'chatAgentIdSet' | 'chatAgentIdUnset'
|
||||
| 'fromEnv' | 'clear' | 'clearing'
|
||||
| 'save' | 'saving' | 'discard' | 'unsaved' | 'saveFailed'
|
||||
|
||||
/** English copy. */
|
||||
export const en: Record<BailianKbLocaleKey, string> = {
|
||||
nav: 'Bailian KB',
|
||||
title: 'Bailian knowledge base',
|
||||
description: 'Account for the knowledge tools: API key, workspace, and default service.',
|
||||
description: 'Account for the knowledge tools: API key, workspace, and default services.',
|
||||
settingsUnavailable: 'The settings document is not reachable from this browser; values below are write-only and stored in the credential store.',
|
||||
fallbackConfigured: 'Falling back to a configured credential-store value.',
|
||||
apiKey: 'API key',
|
||||
apiKeyHint: 'DashScope API key. Stored in the credentials store and never shown again; leave blank to keep the current one.',
|
||||
apiKeySet: 'A key is configured.',
|
||||
apiKeyUnset: 'No key is configured; knowledge tools fail until one is.',
|
||||
workspaceId: 'Workspace id',
|
||||
workspaceIdHint: 'Bailian workspace id — the subdomain of your endpoints. Leave blank to keep the current one.',
|
||||
workspaceIdHint: 'Bailian workspace id — the subdomain of your endpoints. Stored in the settings document; clear and save to fall back to the credential store.',
|
||||
workspaceIdHintFallback: 'Bailian workspace id — the subdomain of your endpoints. Leave blank to keep the current one.',
|
||||
workspaceIdSet: 'A workspace is configured.',
|
||||
workspaceIdUnset: 'No workspace is configured; knowledge tools fail until one is.',
|
||||
agentId: 'Default service id',
|
||||
agentIdHint: 'agent_id of the default retrieval/Q&A service; when unset, every call must name one (kb_service_list discovers ids). Leave blank to keep the current one.',
|
||||
agentIdSet: 'A default service is configured.',
|
||||
agentIdUnset: 'No default service; every call must name one.',
|
||||
retrieveAgentId: 'Default retrieval service id',
|
||||
retrieveAgentIdHint: 'agent_id of the default retrieval service (kb_search); when unset, every call must name one (kb_service_list discovers ids). Stored in the settings document.',
|
||||
retrieveAgentIdHintFallback: 'agent_id of the default retrieval service (kb_search); when unset, every call must name one (kb_service_list discovers ids). Leave blank to keep the current one.',
|
||||
retrieveAgentIdSet: 'A default retrieval service is configured.',
|
||||
retrieveAgentIdUnset: 'No default retrieval service; every kb_search call must name one.',
|
||||
chatAgentId: 'Default chat service id',
|
||||
chatAgentIdHint: 'agent_id of the default Q&A service (kb_chat); when unset, every call must name one (kb_service_list discovers ids). Stored in the settings document.',
|
||||
chatAgentIdHintFallback: 'agent_id of the default Q&A service (kb_chat); when unset, every call must name one (kb_service_list discovers ids). Leave blank to keep the current one.',
|
||||
chatAgentIdSet: 'A default chat service is configured.',
|
||||
chatAgentIdUnset: 'No default chat service; every kb_chat call must name one.',
|
||||
fromEnv: 'Set by the environment (read-only here)',
|
||||
clear: 'Clear default',
|
||||
clearing: 'Clearing…',
|
||||
expand: 'Show settings',
|
||||
collapse: 'Hide settings',
|
||||
save: 'Save',
|
||||
saving: 'Saving…',
|
||||
discard: 'Discard',
|
||||
@@ -44,25 +55,33 @@ export const en: Record<BailianKbLocaleKey, string> = {
|
||||
|
||||
/** Simplified Chinese copy. */
|
||||
export const zh: Record<BailianKbLocaleKey, string> = {
|
||||
nav: '百炼知识库',
|
||||
title: '百炼知识库',
|
||||
description: '知识库工具的账号信息:API 密钥、工作空间与默认服务。',
|
||||
settingsUnavailable: '当前浏览器无法访问设置文档;以下字段仅可写入凭据存储,不回显。',
|
||||
fallbackConfigured: '回退:凭据存储中已有值。',
|
||||
apiKey: 'API 密钥',
|
||||
apiKeyHint: 'DashScope API key。保存在凭据存储中且不会再次显示;留空表示保持当前值。',
|
||||
apiKeySet: '已配置密钥。',
|
||||
apiKeyUnset: '未配置密钥;配置前知识库工具不可用。',
|
||||
workspaceId: '工作空间 ID',
|
||||
workspaceIdHint: '百炼工作空间 ID,即终端节点地址的子域名。留空表示保持当前值。',
|
||||
workspaceIdHint: '百炼工作空间 ID,即终端节点地址的子域名。存入设置文档;清空并保存则回退到凭据存储。',
|
||||
workspaceIdHintFallback: '百炼工作空间 ID,即终端节点地址的子域名。留空表示保持当前值。',
|
||||
workspaceIdSet: '已配置工作空间。',
|
||||
workspaceIdUnset: '未配置工作空间;配置前知识库工具不可用。',
|
||||
agentId: '默认服务 ID',
|
||||
agentIdHint: '默认检索/问答服务的 agent_id;未设置时每次调用都需显式指定(可用 kb_service_list 发现 id)。留空表示保持当前值。',
|
||||
agentIdSet: '已配置默认服务。',
|
||||
agentIdUnset: '未配置默认服务;每次调用需显式指定。',
|
||||
retrieveAgentId: '默认检索服务 ID',
|
||||
retrieveAgentIdHint: '默认检索服务(kb_search)的 agent_id;未设置时每次调用都需显式指定(可用 kb_service_list 发现 id)。存入设置文档。',
|
||||
retrieveAgentIdHintFallback: '默认检索服务(kb_search)的 agent_id;未设置时每次调用都需显式指定(可用 kb_service_list 发现 id)。留空表示保持当前值。',
|
||||
retrieveAgentIdSet: '已配置默认检索服务。',
|
||||
retrieveAgentIdUnset: '未配置默认检索服务;每次 kb_search 调用需显式指定。',
|
||||
chatAgentId: '默认对话服务 ID',
|
||||
chatAgentIdHint: '默认对话服务(kb_chat)的 agent_id;未设置时每次调用都需显式指定(可用 kb_service_list 发现 id)。存入设置文档。',
|
||||
chatAgentIdHintFallback: '默认对话服务(kb_chat)的 agent_id;未设置时每次调用都需显式指定(可用 kb_service_list 发现 id)。留空表示保持当前值。',
|
||||
chatAgentIdSet: '已配置默认对话服务。',
|
||||
chatAgentIdUnset: '未配置默认对话服务;每次 kb_chat 调用需显式指定。',
|
||||
fromEnv: '来自环境变量(此处只读)',
|
||||
clear: '清除默认',
|
||||
clearing: '清除中…',
|
||||
expand: '展开设置',
|
||||
collapse: '收起设置',
|
||||
save: '保存',
|
||||
saving: '保存中…',
|
||||
discard: '放弃',
|
||||
|
||||
@@ -7,7 +7,8 @@ describe('Config', () => {
|
||||
expect(resolved.workspaceId).toBe('ws-1')
|
||||
expect(resolved.endpointHost).toBe('cn-beijing.maas.aliyuncs.com')
|
||||
expect(resolved.chatTimeoutMs).toBe(300_000)
|
||||
expect(resolved.defaultAgentId).toBeUndefined()
|
||||
expect(resolved.defaultRetrieveAgentId).toBeUndefined()
|
||||
expect(resolved.defaultChatAgentId).toBeUndefined()
|
||||
})
|
||||
|
||||
it('accepts a missing workspaceId (per-call credentials fallback)', () => {
|
||||
|
||||
@@ -4,9 +4,9 @@ import { createKbTools } from '../src/tools.js'
|
||||
|
||||
const EXEC = {} as never
|
||||
|
||||
function toolsWith(postJson: unknown, postSse?: unknown, resolveDefaultAgentId?: () => Promise<string | undefined>) {
|
||||
function toolsWith(postJson: unknown, postSse?: unknown, resolveDefaultRetrieveAgentId?: () => Promise<string | undefined>, resolveDefaultChatAgentId?: () => Promise<string | undefined>) {
|
||||
const client = { postJson, postSse, agentVersion: undefined } as unknown as KbClient
|
||||
const list = createKbTools({ client, ...(resolveDefaultAgentId ? { resolveDefaultAgentId } : {}), chatTimeoutMs: 1000 })
|
||||
const list = createKbTools({ client, ...(resolveDefaultRetrieveAgentId ? { resolveDefaultRetrieveAgentId } : {}), ...(resolveDefaultChatAgentId ? { resolveDefaultChatAgentId } : {}), chatTimeoutMs: 1000 })
|
||||
const byName = Object.fromEntries(list.map(t => [t.name, t]))
|
||||
return { byName, list }
|
||||
}
|
||||
@@ -48,7 +48,7 @@ describe('createKbTools', () => {
|
||||
expect(requiredList(withDefault)).not.toContain('agent_id')
|
||||
})
|
||||
|
||||
it('kb_search falls back to the per-call default resolver as an explicit resolve step', async () => {
|
||||
it('kb_search falls back to the per-call default retrieve resolver as an explicit resolve step', async () => {
|
||||
const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse)
|
||||
const { byName } = toolsWith(postJson, undefined, async () => 'aid-fixed')
|
||||
await byName.kb_search!.execute({ query: 'q' }, EXEC)
|
||||
@@ -65,13 +65,13 @@ describe('createKbTools', () => {
|
||||
it('kb_search re-resolves the default per call (credential hot-swap contract)', async () => {
|
||||
const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse)
|
||||
let current: string | undefined
|
||||
const resolveDefaultAgentId = vi.fn(async () => current)
|
||||
const { byName } = toolsWith(postJson, undefined, resolveDefaultAgentId)
|
||||
const resolveDefaultRetrieveAgentId = vi.fn(async () => current)
|
||||
const { byName } = toolsWith(postJson, undefined, resolveDefaultRetrieveAgentId)
|
||||
current = 'aid-one'
|
||||
await byName.kb_search!.execute({ query: 'q' }, EXEC)
|
||||
current = undefined
|
||||
await byName.kb_search!.execute({ query: 'q' }, EXEC).catch(() => {})
|
||||
expect(resolveDefaultAgentId).toHaveBeenCalledTimes(2)
|
||||
expect(resolveDefaultRetrieveAgentId).toHaveBeenCalledTimes(2)
|
||||
expect((postJson.mock.calls[0]![1] as Record<string, unknown>).agent_id).toBe('aid-one')
|
||||
expect(postJson).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@@ -101,4 +101,17 @@ describe('createKbTools', () => {
|
||||
const err = await byName.kb_chat!.execute({ message: 'q', agent_id: 'aid-1' }, EXEC).catch((e: unknown) => e)
|
||||
expect((err as Error).message).toMatch(/timed out.*kb_search/s)
|
||||
})
|
||||
|
||||
it('kb_chat reads chatTimeoutMs off deps per call (live-settings getter stays live)', async () => {
|
||||
const timeout = Object.assign(new Error('operation timed out'), { name: 'TimeoutError' })
|
||||
const postSse = vi.fn(async () => { throw timeout })
|
||||
const client = { postJson: vi.fn(), postSse, agentVersion: undefined } as unknown as KbClient
|
||||
// Mirrors the host apply: a getter over the mutable settings source.
|
||||
let timeoutMs = 1000
|
||||
const list = createKbTools({ client, get chatTimeoutMs() { return timeoutMs } })
|
||||
const chat = list.find(t => t.name === 'kb_chat')!
|
||||
timeoutMs = 2222
|
||||
const err = await chat.execute({ message: 'q', agent_id: 'aid-1' }, EXEC).catch((e: unknown) => e)
|
||||
expect((err as Error).message).toContain('2222ms')
|
||||
})
|
||||
})
|
||||
|
||||
Generated
+6
-6
@@ -38,18 +38,18 @@ importers:
|
||||
'@deepseek-ai/dsh-client-runtime':
|
||||
specifier: link:../../../deepseek-harness/packages/client/runtime
|
||||
version: link:../../../deepseek-harness/packages/client/runtime
|
||||
'@deepseek-ai/dsh-client-ui-primitives':
|
||||
specifier: link:../../../deepseek-harness/packages/client/ui-primitives
|
||||
version: link:../../../deepseek-harness/packages/client/ui-primitives
|
||||
'@deepseek-ai/dsh-client-ui-settings-plugins':
|
||||
specifier: link:../../../deepseek-harness/packages/client/ui-settings-plugins
|
||||
version: link:../../../deepseek-harness/packages/client/ui-settings-plugins
|
||||
'@deepseek-ai/dsh-client-ui-settings':
|
||||
specifier: link:../../../deepseek-harness/packages/client/ui-settings
|
||||
version: link:../../../deepseek-harness/packages/client/ui-settings
|
||||
'@deepseek-ai/dsh-client-ui-slots':
|
||||
specifier: link:../../../deepseek-harness/packages/client/ui-slots
|
||||
version: link:../../../deepseek-harness/packages/client/ui-slots
|
||||
'@deepseek-ai/dsh-credentials':
|
||||
specifier: link:../../../deepseek-harness/packages/credentials/credentials
|
||||
version: link:../../../deepseek-harness/packages/credentials/credentials
|
||||
'@deepseek-ai/dsh-settings':
|
||||
specifier: link:../../../deepseek-harness/packages/settings/settings
|
||||
version: link:../../../deepseek-harness/packages/settings/settings
|
||||
'@deepseek-ai/dsh-skill':
|
||||
specifier: link:../../../deepseek-harness/packages/skill/skill
|
||||
version: link:../../../deepseek-harness/packages/skill/skill
|
||||
|
||||
Reference in New Issue
Block a user