mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
chore(deps): 更新 TypeScript 类型检查脚本及依赖锁定文件
- 修改 package.json 中 typecheck 脚本,新增对 tsconfig.web.json 的检查 - 更新 pnpm-lock.yaml 文件,增加多个依赖项和绑定包的版本信息及平台支持 - 新增 react、lightningcss 等多种平台及架构的预编译绑定库 - 添加多种类型定义依赖,提升类型覆盖范围 - 升级部分工具包及插件版本,优化构建和开发体验
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"build": "pnpm -r run build",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc -b packages/tool-bailian-kb"
|
||||
"typecheck": "tsc -b packages/tool-bailian-kb && tsc -p packages/tool-bailian-kb/tsconfig.web.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.2",
|
||||
|
||||
@@ -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 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,25 +12,53 @@ dsh bundle 分发面:`package.json` 的 `dsh.bundle.patch` 声明 + [`cordis.p
|
||||
workspaceId: !!js process.env.BAILIAN_WORKSPACE_ID
|
||||
```
|
||||
|
||||
`workspaceId` 默认从环境变量读取(`~/.dsh/.env` 写 `BAILIAN_WORKSPACE_ID=ws-xxx` 即可运行);未设置时插件加载期 fail loud。
|
||||
`workspaceId` 只是解析链的一层,不是唯一来源:config 显式值(含此环境变量读取)per-call 优先;未设置时回退到 `BAILIAN_WORKSPACE_ID` credential。同样回退覆盖 `defaultAgentId`(`BAILIAN_DEFAULT_AGENT_ID`)与 API key(`DASHSCOPE_API_KEY`)。
|
||||
|
||||
## 三个值的解析链
|
||||
|
||||
| 值 | 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,运行时校验) |
|
||||
|
||||
行为参数(`endpointHost`/`agentVersion`/`chatTimeoutMs`)只在 config 层,见 [tool-bailian-kb README](../tool-bailian-kb/README.md)。
|
||||
|
||||
## Web UI 配置卡片
|
||||
|
||||
装进 profile 后,Settings → Plugins 出现“百炼知识库”卡片,可配置三个 credential(写 `~/.dsh/.credentials.yaml`):
|
||||
|
||||
- **DashScope API Key** — write-only,`type=password` 遮罩输入草稿
|
||||
- **Bailian Workspace ID** — 明文(便于粘贴核对 workspace id)
|
||||
- **默认服务 ID(agent_id)** — 明文,附独立“清除”按钮(留空保存 = 不写,清除须显式 unset)
|
||||
|
||||
值永不回显:字段始终空白起步,仅显示 configured/来自环境变量 徽标;来自 shell export 或 `~/.dsh/.env` 的值只读(继承环境层),输入框禁用。
|
||||
|
||||
## 用户覆盖
|
||||
|
||||
用户 patch 层在本 bundle 之上,按 id 覆盖时**替换整个 config(无 deep-merge),必须连 workspaceId 一起重述**:
|
||||
用户 patch 层在本 bundle 之上,按 id 覆盖时**替换整个 config(无 deep-merge)**。`workspaceId`/`defaultAgentId` 均为可选,只需重述想显式固定的字段:
|
||||
|
||||
```yaml
|
||||
# ~/.dsh/cordis.patch.yml 或 profile 的 cordis.patch.yml
|
||||
- id: tool-bailian-kb
|
||||
config:
|
||||
workspaceId: ws-xxx
|
||||
defaultAgentId: aid-customer-service # 场景固定式部署
|
||||
defaultAgentId: aid-customer-service # 场景固定式部署;省略 workspaceId 走 credential
|
||||
chatTimeoutMs: 600000
|
||||
```
|
||||
|
||||
禁用:`- id: tool-bailian-kb` + `disabled: true`。
|
||||
|
||||
## 安装(本地 checkout 链接)
|
||||
|
||||
bundle 是 `dsh.bundle` 声明层,真正的插件包 `dsh-tool-bailian-kb` 是它的依赖;`link:` 安装不携带传递依赖,**两个包都要 add**(第二个无 bundle 声明,dsh 会以 plain dependency 装入,CLI 的 warning 即预期行为):
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web add /path/to/bailian-kb-dsh/packages/bundle
|
||||
dsh plugin --profile web add /path/to/bailian-kb-dsh/packages/tool-bailian-kb
|
||||
```
|
||||
|
||||
## 卸载
|
||||
|
||||
```sh
|
||||
dsh plugin --profile <name> remove bailian-kb-dsh
|
||||
dsh plugin --profile <name> remove bailian-kb-dsh dsh-tool-bailian-kb
|
||||
```
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# bailian-kb-dsh: inserts the Bailian knowledge-base consumer over dsh-base.
|
||||
# workspaceId reads BAILIAN_WORKSPACE_ID from the environment (~/.dsh/.env) so a
|
||||
# user patch is only needed to pin defaultAgentId or override the host/timeout.
|
||||
# An id-targeted user patch replaces this whole config: restate workspaceId too.
|
||||
# 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.
|
||||
|
||||
- insert:
|
||||
- id: tool-bailian-kb
|
||||
|
||||
@@ -7,16 +7,37 @@
|
||||
"types": "lib/index.d.ts",
|
||||
"exports": {
|
||||
".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" },
|
||||
"./client": { "default": "./lib/web/client.js" },
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"dsh": {
|
||||
"client": {
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-client-connection",
|
||||
"@deepseek-ai/dsh-client-locale",
|
||||
"@deepseek-ai/dsh-client-runtime",
|
||||
"@deepseek-ai/dsh-api-remotes",
|
||||
"@deepseek-ai/dsh-client-ui-settings-plugins"
|
||||
],
|
||||
"platform": "web"
|
||||
}
|
||||
},
|
||||
"files": ["lib", "skills"],
|
||||
"scripts": { "build": "tsc -b" },
|
||||
"scripts": { "build": "tsc -b && tsdown" },
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "^4.0.1",
|
||||
"@deepseek-ai/dsh-tools": "*",
|
||||
"@deepseek-ai/dsh-credentials": "*",
|
||||
"@deepseek-ai/dsh-skill": "*",
|
||||
"@deepseek-ai/schemastery": "^3.18.1"
|
||||
"@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-slots": "*",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis": "link:../../../deepseek-harness/vendor/cordis",
|
||||
@@ -24,6 +45,17 @@
|
||||
"@deepseek-ai/dsh-credentials": "link:../../../deepseek-harness/packages/credentials/credentials",
|
||||
"@deepseek-ai/dsh-skill": "link:../../../deepseek-harness/packages/skill/skill",
|
||||
"@deepseek-ai/schemastery": "link:../../../deepseek-harness/vendor/schemastery",
|
||||
"@types/node": "^22.0.0"
|
||||
"@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-slots": "link:../../../deepseek-harness/packages/client/ui-slots",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "~18.3.1",
|
||||
"lightningcss": "^1.32.0",
|
||||
"react": "^18.2.0",
|
||||
"tsdown": "^0.22.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ export class KbApiError extends Error {
|
||||
}
|
||||
|
||||
export interface KbClientOptions {
|
||||
workspaceId: string
|
||||
/** Resolves the current workspace id per call (patch config or credential); throws with guidance when unconfigured. */
|
||||
resolveWorkspaceId: () => Promise<string>
|
||||
endpointHost: string
|
||||
/** Service version forwarded on search/chat when set (deployment debug choice). */
|
||||
agentVersion?: string
|
||||
@@ -33,9 +34,9 @@ export class KbClient {
|
||||
}
|
||||
|
||||
private async post(path: string, body: unknown, accept: string, signal?: AbortSignal): Promise<Response> {
|
||||
const apiKey = await this.opts.resolveApiKey()
|
||||
const [apiKey, workspaceId] = await Promise.all([this.opts.resolveApiKey(), this.opts.resolveWorkspaceId()])
|
||||
const fetchImpl = this.opts.fetchImpl ?? fetch
|
||||
const url = kbEndpoint(this.opts.endpointHost, this.opts.workspaceId, path)
|
||||
const url = kbEndpoint(this.opts.endpointHost, workspaceId, path)
|
||||
const res = await fetchImpl(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Accept': accept },
|
||||
|
||||
@@ -16,11 +16,11 @@ export const inject = ['tools', 'credentials']
|
||||
|
||||
/** Bailian knowledge-base plugin configuration. */
|
||||
export interface Config {
|
||||
/** Bailian workspace id; the API host is the workspace subdomain `https://<workspaceId>.<endpointHost>`. */
|
||||
workspaceId: string
|
||||
/** 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). */
|
||||
workspaceId?: string
|
||||
/** API host suffix; replace for other regions or private deployments. */
|
||||
endpointHost: string
|
||||
/** Retrieval-service id pinned by this deployment; when set, the tools' agent_id parameter becomes optional. */
|
||||
/** Retrieval-service id pinned by this deployment; when unset, the per-call fallback reads the BAILIAN_DEFAULT_AGENT_ID credential. */
|
||||
defaultAgentId?: string
|
||||
/** Service version to call: `beta` (draft) or a published number; defaults to the latest published version. Never model-visible. */
|
||||
agentVersion?: string
|
||||
@@ -28,9 +28,9 @@ export interface Config {
|
||||
chatTimeoutMs: number
|
||||
}
|
||||
|
||||
/** Schemastery validation for {@link Config}; a missing workspaceId fails at load. */
|
||||
/** Schemastery validation for {@link Config}; workspaceId and defaultAgentId are optional — both resolve per call with a credentials fallback. */
|
||||
export const Config: z<Config> = z.object({
|
||||
workspaceId: z.string().required(),
|
||||
workspaceId: z.string(),
|
||||
endpointHost: z.string().default('cn-beijing.maas.aliyuncs.com'),
|
||||
defaultAgentId: z.string(),
|
||||
agentVersion: z.string(),
|
||||
@@ -44,16 +44,29 @@ export const Config: z<Config> = z.object({
|
||||
* @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
|
||||
const client = new KbClient({
|
||||
workspaceId: config.workspaceId,
|
||||
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 } : {}),
|
||||
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 ~/.dsh/.env or .credentials.yaml '
|
||||
+ '(create a key at https://bailian.console.aliyun.com/?tab=app#/api-key).',
|
||||
'DASHSCOPE_API_KEY is not configured. Set it in the web UI (Settings → Plugins → Bailian knowledge base) '
|
||||
+ 'or in ~/.dsh/.credentials.yaml (create a key at https://bailian.console.aliyun.com/?tab=app#/api-key).',
|
||||
)
|
||||
}
|
||||
return resolved.value
|
||||
@@ -61,7 +74,12 @@ export function apply(ctx: Context, config: Config): void {
|
||||
})
|
||||
for (const tool of createKbTools({
|
||||
client,
|
||||
...(config.defaultAgentId ? { defaultAgentId: config.defaultAgentId } : {}),
|
||||
resolveDefaultAgentId: pinnedAgentId !== undefined
|
||||
? async () => pinnedAgentId
|
||||
: async () => {
|
||||
const resolved = await ctx.credentials.resolve(credentialRef('BAILIAN_DEFAULT_AGENT_ID'))
|
||||
return resolved?.value
|
||||
},
|
||||
chatTimeoutMs: config.chatTimeoutMs,
|
||||
})) {
|
||||
ctx.tools.register(tool)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/**
|
||||
* The three model-facing knowledge tools. Schemas are static per deployment: a configured
|
||||
* defaultAgentId downgrades agent_id to optional at build time (never a runtime fallback chain).
|
||||
* The three model-facing knowledge tools. agent_id stays optional in the schema
|
||||
* regardless of deployment: the default service (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.
|
||||
*/
|
||||
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
@@ -15,7 +18,8 @@ const DEFAULT_TOP_K = 5
|
||||
|
||||
export interface KbToolDeps {
|
||||
client: KbClient
|
||||
defaultAgentId?: string
|
||||
/** Resolves the default agent id per call (patch config or credential); omitted means no default. */
|
||||
resolveDefaultAgentId?: () => Promise<string | undefined>
|
||||
chatTimeoutMs: number
|
||||
}
|
||||
|
||||
@@ -49,18 +53,18 @@ async function withServiceHint(client: KbClient, err: unknown): Promise<never> {
|
||||
* @returns definitions ready for `ctx.tools.register()`.
|
||||
*/
|
||||
export function createKbTools(deps: KbToolDeps) {
|
||||
const { client, defaultAgentId, chatTimeoutMs } = deps
|
||||
const { client, resolveDefaultAgentId, chatTimeoutMs } = deps
|
||||
const agentIdParam = {
|
||||
type: 'string' as const,
|
||||
...(defaultAgentId === undefined ? { required: true as const } : {}),
|
||||
description: defaultAgentId === undefined
|
||||
? 'Retrieval/Q&A service id (find one via kb_service_list).'
|
||||
: 'Retrieval/Q&A service id; omit to use this deployment\'s default service.',
|
||||
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 = (supplied: string | undefined): string => {
|
||||
const agentId = supplied ?? defaultAgentId
|
||||
if (agentId === undefined) throw new Error('agent_id is required: discover services with kb_service_list')
|
||||
return agentId
|
||||
const resolveAgentId = async (supplied: string | undefined): Promise<string> => {
|
||||
if (supplied !== undefined) return supplied
|
||||
const defaultId = resolveDefaultAgentId === undefined ? undefined : await resolveDefaultAgentId()
|
||||
if (defaultId === undefined) {
|
||||
throw new Error('agent_id is required: no default service is configured; discover services with kb_service_list')
|
||||
}
|
||||
return defaultId
|
||||
}
|
||||
|
||||
const serviceList = defineTool({
|
||||
@@ -163,7 +167,7 @@ export function createKbTools(deps: KbToolDeps) {
|
||||
const topK = args.top_k ?? DEFAULT_TOP_K
|
||||
const body: SearchRequest = {
|
||||
query: args.query,
|
||||
agent_id: resolveAgentId(args.agent_id),
|
||||
agent_id: await resolveAgentId(args.agent_id),
|
||||
...(client.agentVersion ? { agent_version: client.agentVersion } : {}),
|
||||
...(args.images && args.images.length > 0 ? { images: args.images } : {}),
|
||||
}
|
||||
@@ -210,7 +214,7 @@ export function createKbTools(deps: KbToolDeps) {
|
||||
const body = {
|
||||
input: { messages: [{ role: 'user' as const, content: args.message }] },
|
||||
parameters: { agent_options: {
|
||||
agent_id: resolveAgentId(args.agent_id),
|
||||
agent_id: await resolveAgentId(args.agent_id),
|
||||
...(client.agentVersion ? { agent_version: client.agentVersion } : {}),
|
||||
} },
|
||||
stream: true as const,
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
/* 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). */
|
||||
|
||||
.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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.chevron {
|
||||
flex: none;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
transition: transform .16s;
|
||||
}
|
||||
|
||||
.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. */
|
||||
.pending {
|
||||
flex: none;
|
||||
border-radius: 999px;
|
||||
padding: 1px 8px;
|
||||
font-size: 11px;
|
||||
line-height: 17px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
background: var(--dsw-alias-bg-module-platform);
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.field + .field {
|
||||
border-top: 1px solid var(--dsw-alias-border-l2);
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.badges {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
border-radius: 999px;
|
||||
padding: 1px 8px;
|
||||
font-size: 11px;
|
||||
line-height: 17px;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
background: var(--dsw-alias-bg-module-platform);
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.badgeMuted {
|
||||
border-radius: 999px;
|
||||
padding: 1px 8px;
|
||||
font-size: 11px;
|
||||
line-height: 17px;
|
||||
white-space: nowrap;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.clear {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clear:hover:not(:disabled) {
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.clear:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.input {
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
border-radius: 8px;
|
||||
background: var(--dsw-alias-bg-layer-3);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.input:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--dsw-alias-brand-primary);
|
||||
}
|
||||
|
||||
.input:disabled {
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 12px 0 4px;
|
||||
border-top: 1px solid var(--dsw-alias-border-l2);
|
||||
}
|
||||
|
||||
.failed {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--dsw-alias-label-error);
|
||||
}
|
||||
|
||||
.discard,
|
||||
.save {
|
||||
appearance: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 5px 14px;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.discard {
|
||||
border-color: var(--dsw-alias-border-l2);
|
||||
background: none;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.discard:hover:not(:disabled) {
|
||||
color: var(--dsw-alias-label-primary);
|
||||
border-color: var(--dsw-alias-label-dimmed);
|
||||
}
|
||||
|
||||
.save {
|
||||
background: var(--dsw-alias-label-primary);
|
||||
color: var(--dsw-alias-bg-layer-3);
|
||||
}
|
||||
|
||||
.discard:disabled,
|
||||
.save:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.discard:focus-visible,
|
||||
.save:focus-visible {
|
||||
outline: 2px solid var(--dsw-alias-brand-primary);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
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 type { BailianKbLocaleKey } from './locales.ts'
|
||||
import css from './BailianCard.module.css'
|
||||
|
||||
/** Props the renderer binds for the Bailian card. */
|
||||
export type BailianCardProps =
|
||||
PropsRuntime<'settings.plugin.item'>
|
||||
& PropsLocale<'tool-bailian-kb'>
|
||||
& InjectFace<BailianCardFace>
|
||||
|
||||
/** One field's render description. */
|
||||
interface FieldView {
|
||||
key: BailianFieldKey
|
||||
labelKey: BailianKbLocaleKey
|
||||
hintKey: BailianKbLocaleKey
|
||||
setKey: BailianKbLocaleKey
|
||||
unsetKey: BailianKbLocaleKey
|
||||
/** Password-masked drafting; only the API key is an actual secret. */
|
||||
secret: boolean
|
||||
}
|
||||
|
||||
/** The three controls, in card 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 },
|
||||
]
|
||||
|
||||
/**
|
||||
* Render the Bailian card.
|
||||
* @param props - locale copy, the card snapshot, and its actions.
|
||||
* @returns the card.
|
||||
*/
|
||||
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 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>
|
||||
{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>
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* The Bailian card's controller: staged drafts over the credentials domain.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import type { IApiClient } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
/** The credential references this card stages, keyed by their ref names. */
|
||||
export const BAILIAN_CARD_REFS = [
|
||||
'DASHSCOPE_API_KEY',
|
||||
'BAILIAN_WORKSPACE_ID',
|
||||
'BAILIAN_DEFAULT_AGENT_ID',
|
||||
] as const
|
||||
|
||||
/** One card field, addressed by its credential reference. */
|
||||
export type BailianFieldKey = (typeof BAILIAN_CARD_REFS)[number]
|
||||
|
||||
/** What the credentials domain reports for one reference (never the value). */
|
||||
export interface BailianCredentialView {
|
||||
/** Whether any layer supplies a value for the reference. */
|
||||
configured: boolean
|
||||
/** Whether `credentials.set` can affect it; false disables the control. */
|
||||
writable: boolean
|
||||
}
|
||||
|
||||
/** What the Bailian card renders. */
|
||||
export interface BailianCardState {
|
||||
/** Staged drafts, blank = keep the stored value. */
|
||||
drafts: Record<BailianFieldKey, string>
|
||||
/** Last credentials-domain answer per reference; unknown refs read as writable. */
|
||||
credentials: Record<BailianFieldKey, BailianCredentialView>
|
||||
/** Whether a save is in flight. */
|
||||
saving: boolean
|
||||
/** Whether the 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. */
|
||||
export interface BailianCardFace {
|
||||
hooks: {
|
||||
/** Card 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. */
|
||||
save: () => Promise<void>
|
||||
/** Drop every staged draft. */
|
||||
discard: () => void
|
||||
/** Remove the stored default service (`credentials.unset`), then re-read. */
|
||||
clearDefaultAgent: () => Promise<void>
|
||||
}
|
||||
|
||||
/** Bridge the credentials domain onto the card. */
|
||||
export class BailianCardController {
|
||||
private readonly store: SnapshotStore<BailianCardState>
|
||||
|
||||
/**
|
||||
* @param api - wire face used for the three credential references.
|
||||
*/
|
||||
constructor(private readonly api: Pick<IApiClient, 'credentials'>) {
|
||||
this.store = createSnapshotStore<BailianCardState>({
|
||||
drafts: {
|
||||
DASHSCOPE_API_KEY: '',
|
||||
BAILIAN_WORKSPACE_ID: '',
|
||||
BAILIAN_DEFAULT_AGENT_ID: '',
|
||||
},
|
||||
credentials: {
|
||||
DASHSCOPE_API_KEY: { configured: false, writable: true },
|
||||
BAILIAN_WORKSPACE_ID: { configured: false, writable: true },
|
||||
BAILIAN_DEFAULT_AGENT_ID: { configured: false, writable: true },
|
||||
},
|
||||
saving: false,
|
||||
clearing: false,
|
||||
failed: false,
|
||||
})
|
||||
void this.read()
|
||||
}
|
||||
|
||||
/** Whether any draft is staged. */
|
||||
get dirty(): boolean {
|
||||
return BAILIAN_CARD_REFS.some(key => this.store.getSnapshot().drafts[key] !== '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Stage one draft; any edit clears the failure mark so the banner does not
|
||||
* outlive the correction it asks for.
|
||||
* @param key - the field's credential reference.
|
||||
* @param text - the staged text.
|
||||
*/
|
||||
edit(key: BailianFieldKey, text: string): void {
|
||||
this.store.update(draft => {
|
||||
draft.drafts[key] = text
|
||||
draft.failed = false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
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
|
||||
}
|
||||
}))
|
||||
this.store.update(draft => {
|
||||
draft.saving = false
|
||||
draft.failed = failed
|
||||
if (!failed) for (const ref of staged.keys()) draft.drafts[ref] = ''
|
||||
})
|
||||
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] = ''
|
||||
draft.failed = false
|
||||
})
|
||||
}
|
||||
|
||||
/** Remove the stored default service so every call names one again. */
|
||||
async clearDefaultAgent(): Promise<void> {
|
||||
if (this.store.getSnapshot().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
|
||||
}
|
||||
this.store.update(draft => {
|
||||
draft.clearing = false
|
||||
draft.failed = failed
|
||||
})
|
||||
await this.read()
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-read after the Host reports a change to a reference this card 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.
|
||||
* @param ref - the reference the Host reports as changed.
|
||||
*/
|
||||
refresh(ref: string): void {
|
||||
if (!(BAILIAN_CARD_REFS as readonly string[]).includes(ref)) return
|
||||
void this.read()
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the face the card's slot registration injects.
|
||||
* @returns the card's snapshot and its actions.
|
||||
*/
|
||||
inject(): BailianCardFace {
|
||||
return {
|
||||
hooks: { bailianCard: this.store },
|
||||
edit: (key, text) => { this.edit(key, text) },
|
||||
save: () => this.save(),
|
||||
discard: () => { this.discard() },
|
||||
clearDefaultAgent: () => this.clearDefaultAgent(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the credentials domain about all three references and publish the
|
||||
* answer. A failed read keeps the last known state: the card stays usable
|
||||
* and a write still reaches the Host.
|
||||
*/
|
||||
private async read(): Promise<void> {
|
||||
let response: Awaited<ReturnType<IApiClient['credentials']['describe']>>
|
||||
try {
|
||||
response = await this.api.credentials.describe({ refs: [...BAILIAN_CARD_REFS] })
|
||||
} catch (_credentialReadFailure) {
|
||||
return
|
||||
}
|
||||
if (!response.result.ok) return
|
||||
const view = response.result.value.credentials
|
||||
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.
|
||||
draft.credentials[ref] = {
|
||||
configured: view[ref]?.configured ?? false,
|
||||
writable: view[ref]?.writable ?? true,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* CSS Modules for the browser half: the bundler (tsdown client preset) inlines
|
||||
* `*.module.css` imports as hashed class maps, this declaration gives the
|
||||
* import its type in the browser-only project.
|
||||
*/
|
||||
declare module '*.module.css' {
|
||||
const classes: Record<string, string>
|
||||
export default classes
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
||||
// Type-only: the locale plugin's Context merge (ctx.locale).
|
||||
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
||||
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
// Type-only: the remote service's Context merge (ctx.remote) and the forwarded
|
||||
// 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'
|
||||
import { BailianCard } from './BailianCard.tsx'
|
||||
import { BailianCardController } 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. */
|
||||
'tool-bailian-kb': BailianKbLocaleKey
|
||||
}
|
||||
}
|
||||
|
||||
/** Dictionary namespace owned by this plugin. */
|
||||
const NS = 'tool-bailian-kb'
|
||||
|
||||
/** Required services (cordis fiber inject). */
|
||||
export const inject = ['slots', 'locale', 'connection', 'remote']
|
||||
|
||||
/**
|
||||
* Mount the Bailian card into the plugin configuration section.
|
||||
* @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)
|
||||
// Values can change elsewhere (Models page, external file edits); the badges
|
||||
// must follow the Host, not the card's last write.
|
||||
ctx.effect(
|
||||
() => ctx.remote.$on('credentials/updated', ref => { card.refresh(ref) }),
|
||||
'tool-bailian-kb: credential invalidations',
|
||||
)
|
||||
|
||||
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
|
||||
name: 'settings.plugin.item',
|
||||
id: 'bailian-kb',
|
||||
order: 30,
|
||||
locale: NS,
|
||||
inject: () => card.inject(),
|
||||
}, BailianCard))
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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 keys this card renders. */
|
||||
export type BailianKbLocaleKey =
|
||||
| 'title' | 'description'
|
||||
| 'apiKey' | 'apiKeyHint' | 'apiKeySet' | 'apiKeyUnset'
|
||||
| 'workspaceId' | 'workspaceIdHint' | 'workspaceIdSet' | 'workspaceIdUnset'
|
||||
| 'agentId' | 'agentIdHint' | 'agentIdSet' | 'agentIdUnset'
|
||||
| 'fromEnv' | 'clear' | 'clearing' | 'expand' | 'collapse'
|
||||
| 'save' | 'saving' | 'discard' | 'unsaved' | 'saveFailed'
|
||||
|
||||
/** English copy. */
|
||||
export const en: Record<BailianKbLocaleKey, string> = {
|
||||
title: 'Bailian knowledge base',
|
||||
description: 'Account for the knowledge tools: API key, workspace, and default service.',
|
||||
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.',
|
||||
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.',
|
||||
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',
|
||||
unsaved: 'Unsaved',
|
||||
saveFailed: 'The Host did not accept these values; they were left for you to correct.',
|
||||
}
|
||||
|
||||
/** Simplified Chinese copy. */
|
||||
export const zh: Record<BailianKbLocaleKey, string> = {
|
||||
title: '百炼知识库',
|
||||
description: '知识库工具的账号信息:API 密钥、工作空间与默认服务。',
|
||||
apiKey: 'API 密钥',
|
||||
apiKeyHint: 'DashScope API key。保存在凭据存储中且不会再次显示;留空表示保持当前值。',
|
||||
apiKeySet: '已配置密钥。',
|
||||
apiKeyUnset: '未配置密钥;配置前知识库工具不可用。',
|
||||
workspaceId: '工作空间 ID',
|
||||
workspaceIdHint: '百炼工作空间 ID,即终端节点地址的子域名。留空表示保持当前值。',
|
||||
workspaceIdSet: '已配置工作空间。',
|
||||
workspaceIdUnset: '未配置工作空间;配置前知识库工具不可用。',
|
||||
agentId: '默认服务 ID',
|
||||
agentIdHint: '默认检索/问答服务的 agent_id;未设置时每次调用都需显式指定(可用 kb_service_list 发现 id)。留空表示保持当前值。',
|
||||
agentIdSet: '已配置默认服务。',
|
||||
agentIdUnset: '未配置默认服务;每次调用需显式指定。',
|
||||
fromEnv: '来自环境变量(此处只读)',
|
||||
clear: '清除默认',
|
||||
clearing: '清除中…',
|
||||
expand: '展开设置',
|
||||
collapse: '收起设置',
|
||||
save: '保存',
|
||||
saving: '保存中…',
|
||||
discard: '放弃',
|
||||
unsaved: '未保存',
|
||||
saveFailed: '宿主未接受这些值,已保留供你修改。',
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { KbApiError, KbClient } from '../src/client.js'
|
||||
|
||||
function makeClient(fetchImpl: typeof fetch) {
|
||||
return new KbClient({
|
||||
workspaceId: 'ws-1',
|
||||
resolveWorkspaceId: async () => 'ws-1',
|
||||
endpointHost: 'cn-beijing.maas.aliyuncs.com',
|
||||
resolveApiKey: async () => 'sk-test',
|
||||
fetchImpl,
|
||||
@@ -36,7 +36,7 @@ describe('KbClient.postJson', () => {
|
||||
const resolveApiKey = vi.fn(async () => 'sk-test')
|
||||
const fetchImpl = vi.fn(async () => new Response('{}', { status: 200 }))
|
||||
const client = new KbClient({
|
||||
workspaceId: 'ws-1',
|
||||
resolveWorkspaceId: async () => 'ws-1',
|
||||
endpointHost: 'h',
|
||||
resolveApiKey,
|
||||
fetchImpl: fetchImpl as unknown as typeof fetch,
|
||||
@@ -45,4 +45,20 @@ describe('KbClient.postJson', () => {
|
||||
await client.postJson('/p', {})
|
||||
expect(resolveApiKey).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('re-resolves the workspace id per call (credential hot-swap contract)', async () => {
|
||||
let workspaceId = 'ws-1'
|
||||
const fetchImpl = vi.fn(async () => new Response('{}', { status: 200 }))
|
||||
const client = new KbClient({
|
||||
resolveWorkspaceId: async () => workspaceId,
|
||||
endpointHost: 'h',
|
||||
resolveApiKey: async () => 'sk-test',
|
||||
fetchImpl: fetchImpl as unknown as typeof fetch,
|
||||
})
|
||||
await client.postJson('/p', {})
|
||||
workspaceId = 'ws-2'
|
||||
await client.postJson('/p', {})
|
||||
const urls = fetchImpl.mock.calls.map(call => (call as unknown as [string])[0])
|
||||
expect(urls).toEqual(['https://ws-1.h/p', 'https://ws-2.h/p'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { Config } from '../src/index.js'
|
||||
|
||||
describe('Config', () => {
|
||||
it('applies defaults and keeps required workspaceId', () => {
|
||||
it('applies defaults and accepts a pinned workspaceId', () => {
|
||||
const resolved = new Config({ workspaceId: 'ws-1' })
|
||||
expect(resolved.workspaceId).toBe('ws-1')
|
||||
expect(resolved.endpointHost).toBe('cn-beijing.maas.aliyuncs.com')
|
||||
@@ -10,7 +10,9 @@ describe('Config', () => {
|
||||
expect(resolved.defaultAgentId).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects a missing workspaceId (fail loud at load)', () => {
|
||||
expect(() => new Config({} as never)).toThrow()
|
||||
it('accepts a missing workspaceId (per-call credentials fallback)', () => {
|
||||
const resolved = new Config({} as never)
|
||||
expect(resolved.workspaceId).toBeUndefined()
|
||||
expect(resolved.endpointHost).toBe('cn-beijing.maas.aliyuncs.com')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,9 +4,9 @@ import { createKbTools } from '../src/tools.js'
|
||||
|
||||
const EXEC = {} as never
|
||||
|
||||
function toolsWith(postJson: unknown, postSse?: unknown, defaultAgentId?: string) {
|
||||
function toolsWith(postJson: unknown, postSse?: unknown, resolveDefaultAgentId?: () => Promise<string | undefined>) {
|
||||
const client = { postJson, postSse, agentVersion: undefined } as unknown as KbClient
|
||||
const list = createKbTools({ client, ...(defaultAgentId ? { defaultAgentId } : {}), chatTimeoutMs: 1000 })
|
||||
const list = createKbTools({ client, ...(resolveDefaultAgentId ? { resolveDefaultAgentId } : {}), chatTimeoutMs: 1000 })
|
||||
const byName = Object.fromEntries(list.map(t => [t.name, t]))
|
||||
return { byName, list }
|
||||
}
|
||||
@@ -36,23 +36,46 @@ describe('createKbTools', () => {
|
||||
expect(body.agent_id).toBe('aid-1')
|
||||
})
|
||||
|
||||
it('agent_id is required without defaultAgentId and optional with one', () => {
|
||||
it('agent_id stays optional in the schema regardless of a configured default', () => {
|
||||
const withoutDefault = toolsWith(vi.fn()).byName.kb_search!
|
||||
const withDefault = toolsWith(vi.fn(), undefined, 'aid-fixed').byName.kb_search!
|
||||
const withDefault = toolsWith(vi.fn(), undefined, async () => 'aid-fixed').byName.kb_search!
|
||||
// defineTool compiles the spec into JSON Schema: requiredness lives in the top-level `required` array.
|
||||
const requiredList = (tool: { parameters: Record<string, unknown> }) =>
|
||||
(tool.parameters.required ?? []) as string[]
|
||||
expect(requiredList(withoutDefault)).toContain('agent_id')
|
||||
// The default can arrive or leave at runtime via the credentials domain, so
|
||||
// the schema cannot promise requiredness either way.
|
||||
expect(requiredList(withoutDefault)).not.toContain('agent_id')
|
||||
expect(requiredList(withDefault)).not.toContain('agent_id')
|
||||
})
|
||||
|
||||
it('kb_search falls back to defaultAgentId as an explicit resolve step', async () => {
|
||||
it('kb_search falls back to the per-call default resolver as an explicit resolve step', async () => {
|
||||
const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse)
|
||||
const { byName } = toolsWith(postJson, undefined, 'aid-fixed')
|
||||
const { byName } = toolsWith(postJson, undefined, async () => 'aid-fixed')
|
||||
await byName.kb_search!.execute({ query: 'q' }, EXEC)
|
||||
expect((postJson.mock.calls[0]![1] as Record<string, unknown>).agent_id).toBe('aid-fixed')
|
||||
})
|
||||
|
||||
it('a missing agent_id without any default resolves to executable discovery guidance', async () => {
|
||||
const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse)
|
||||
const { byName } = toolsWith(postJson)
|
||||
const err = await byName.kb_search!.execute({ query: 'q' }, EXEC).catch((e: unknown) => e)
|
||||
expect((err as Error).message).toContain('kb_service_list')
|
||||
})
|
||||
|
||||
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)
|
||||
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((postJson.mock.calls[0]![1] as Record<string, unknown>).agent_id).toBe('aid-one')
|
||||
expect(postJson).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('a 4xx failure appends the current service list to the error', async () => {
|
||||
const postJson = vi.fn(async (path: string) => {
|
||||
if (path === '/api/v1/indices/knowledge/search') throw new KbApiError('agent not found', 400)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": { "rootDir": "src", "outDir": "lib" },
|
||||
"include": ["src"]
|
||||
"include": ["src"],
|
||||
"exclude": ["src/web"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"allowImportingTsExtensions": true,
|
||||
"types": []
|
||||
},
|
||||
"include": ["src/web"]
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Browser bundle for the plugin's client half, mirroring the host's tsdown
|
||||
* client preset (packages/client/tsdown.client.ts — spelled out here because
|
||||
* an out-of-tree package cannot import it): a closure-factory artifact that
|
||||
* calls window.__ModuleLoader__.load({id, factory}) and resolves externals
|
||||
* through the injected require. CSS Modules are compiled by lightningcss
|
||||
* inside the bundle: importing `x.module.css` yields the hashed class map and
|
||||
* auto-injects a <style data-plugin> tag at factory execution.
|
||||
*/
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { basename, dirname, resolve as resolvePath } from 'node:path'
|
||||
import { defineConfig } from 'tsdown'
|
||||
import { transform } from 'lightningcss'
|
||||
|
||||
/** Plugin id stamped into the __ModuleLoader__.load handoff and style tags. */
|
||||
const PLUGIN_ID = 'dsh-tool-bailian-kb'
|
||||
|
||||
/** The module specifiers the shell shares into the frozen module table. */
|
||||
const PLATFORM_MODULES = [
|
||||
'react', 'react/jsx-runtime', 'react-dom', 'react-dom/client', '@deepseek-ai/cordis',
|
||||
'@deepseek-ai/dsh-client-ui-slots',
|
||||
'@deepseek-ai/dsh-client-web-react',
|
||||
'@deepseek-ai/dsh-client-ui-primitives',
|
||||
'@deepseek-ai/dsh-client-ui-attachment',
|
||||
'@deepseek-ai/dsh-client-schema-form',
|
||||
]
|
||||
|
||||
/**
|
||||
* Documented host exemption (not a platform module): the snapshot-store
|
||||
* engine lives in runtime pending its rehoming; at runtime the lazy CJS table
|
||||
* answers the require natively.
|
||||
*/
|
||||
const RUNTIME_STORE_EXEMPTION = '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
/** Externals resolved from the loader module table. */
|
||||
const CLIENT_EXTERNALS: readonly string[] = [...PLATFORM_MODULES, RUNTIME_STORE_EXEMPTION]
|
||||
|
||||
/** Virtual-id wrapper keeping module CSS away from tsdown's own css pipeline. */
|
||||
const CSS_VIRTUAL_PREFIX = '\0dsh-css:'
|
||||
const CSS_VIRTUAL_SUFFIX = '.mjs'
|
||||
|
||||
export default defineConfig({
|
||||
name: `${PLUGIN_ID}/client`,
|
||||
entry: { client: 'src/web/index.ts' },
|
||||
// Browser bundle lands in its own lib/web subdir: the tsc node half owns
|
||||
// lib/ directly, and a shared outDir would clobber lib/client.js (the KbClient
|
||||
// module) with this artifact. The entryFileNames pin keeps it exactly
|
||||
// lib/web/client.js; the host serves it at /plugins/<id>/client.js via
|
||||
// exports["./client"]. clean must stay off — a default clean would wipe the
|
||||
// tsc-emitted node half.
|
||||
outDir: 'lib/web',
|
||||
format: 'cjs',
|
||||
platform: 'browser',
|
||||
dts: false,
|
||||
sourcemap: true,
|
||||
clean: false,
|
||||
external: [...CLIENT_EXTERNALS],
|
||||
// tsdown auto-externalizes package dependencies; anything NOT in the loader
|
||||
// module table must inline instead. A require() the table cannot answer is a
|
||||
// guaranteed runtime throw, so the rule is the table list itself.
|
||||
noExternal: (id: string) => (CLIENT_EXTERNALS.includes(id) ? undefined : true),
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'production'),
|
||||
'import.meta.env.MODE': JSON.stringify(process.env.NODE_ENV ?? 'production'),
|
||||
'import.meta.env': JSON.stringify({ MODE: process.env.NODE_ENV ?? 'production' }),
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
// Bundle purity gate (build-time mirror of the module-edge rules):
|
||||
// platform seed entries stay external; every other @deepseek-ai value
|
||||
// import is a build error — a cross-plugin value import either inlines a
|
||||
// duplicate runtime instance or requires a specifier the frozen module
|
||||
// table cannot answer. Cross-plugin collaboration goes through cordis
|
||||
// services instead (type-only imports are erased and never reach this gate).
|
||||
name: 'dsh-client-bundle-purity',
|
||||
resolveId(source: string) {
|
||||
if (!source.startsWith('@deepseek-ai/')) return null
|
||||
if (CLIENT_EXTERNALS.includes(source)) return null // platform module: external wins
|
||||
throw new Error(
|
||||
`client bundle purity: "${source}" is not a platform module (CLIENT_EXTERNALS) — `
|
||||
+ 'cross-plugin value imports are forbidden; collaborate through cordis services '
|
||||
+ '(type-only imports are erased and never reach this gate)',
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dsh-css-modules-inline',
|
||||
resolveId(source: string, importer: string | undefined) {
|
||||
if (!source.endsWith('.module.css')) return null
|
||||
const abs = importer !== undefined ? resolvePath(dirname(importer), source) : source
|
||||
return CSS_VIRTUAL_PREFIX + abs + CSS_VIRTUAL_SUFFIX
|
||||
},
|
||||
async load(virtualId: string) {
|
||||
if (!virtualId.startsWith(CSS_VIRTUAL_PREFIX)) return null
|
||||
const fileId = virtualId.slice(CSS_VIRTUAL_PREFIX.length, -CSS_VIRTUAL_SUFFIX.length)
|
||||
// The virtual id otherwise hides the physical stylesheet from the watch graph.
|
||||
this.addWatchFile(fileId)
|
||||
const source = await readFile(fileId)
|
||||
const { code, exports: cssExports } = transform({
|
||||
filename: fileId,
|
||||
code: source,
|
||||
cssModules: { pattern: '[hash]_[local]' },
|
||||
minify: true,
|
||||
})
|
||||
const classMap: Record<string, string> = {}
|
||||
for (const [local, exp] of Object.entries(cssExports ?? {})) classMap[local] = exp.name
|
||||
// One <style data-plugin> per module file; idempotent under re-evaluation.
|
||||
return [
|
||||
`const css = ${JSON.stringify(code.toString())};`,
|
||||
`const tagId = ${JSON.stringify(`${PLUGIN_ID}/${basename(fileId)}`)};`,
|
||||
'if (typeof document !== \'undefined\' && document.querySelector(\'style[data-plugin-css=\' + JSON.stringify(tagId) + \']\') === null) {',
|
||||
' const tag = document.createElement(\'style\');',
|
||||
` tag.dataset.plugin = ${JSON.stringify(PLUGIN_ID)};`,
|
||||
' tag.dataset.pluginCss = tagId;',
|
||||
' tag.textContent = css;',
|
||||
' document.head.appendChild(tag);',
|
||||
'}',
|
||||
`export default ${JSON.stringify(classMap)};`,
|
||||
].join('\n')
|
||||
},
|
||||
},
|
||||
],
|
||||
outputOptions: {
|
||||
entryFileNames: 'client.js',
|
||||
banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(PLUGIN_ID)}, factory: (require) => {`,
|
||||
footer: 'return module.exports; } });',
|
||||
intro: 'var module = { exports: {} }; var exports = module.exports;',
|
||||
},
|
||||
})
|
||||
Generated
+830
-10
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,4 @@
|
||||
packages:
|
||||
- packages/*
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
|
||||
Reference in New Issue
Block a user