feat(bailian): 增加高级配置区和相关交互优化

- 将配置字段调整为高级配置隐藏,支持折叠展开展示
- 为API Key和工作空间ID添加“去获取”外部链接,引导用户至控制台
- 优化填写状态提示,新增已配置完成的绿色成功提示
- 新增高级配置按钮,折叠字段组及保存、放弃操作
- 细化自动填充状态处理,支持失败、等待登录、成功及已配置等多状态显示
- 更新本地化文本,支持高级配置及“去获取”等新文案
- 调整CSS样式,新增高级配置相关样式和状态样式
- 升级版本号至0.1.15
This commit is contained in:
zeyu.fz
2026-08-21 16:17:18 +08:00
parent 8714f61496
commit 70dbc2110d
4 changed files with 256 additions and 94 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ali/bailian-kb-dsh",
"version": "0.1.12",
"version": "0.1.15",
"description": "Bailian knowledge-base tools for DeepSeek Harness: kb_search and kb_chat over the DashScope RAG API, plus the bl CLI management skill.",
"type": "module",
"main": "lib/index.js",
@@ -87,14 +87,33 @@
}
.label {
flex: 1;
min-width: 0;
font-size: 13px;
font-weight: 500;
line-height: 1.5;
color: var(--dsw-alias-label-primary);
}
.labelWrap {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.getLink {
flex: none;
font-size: 12px;
line-height: 1.5;
color: var(--dsw-alias-brand-primary);
text-decoration: none;
white-space: nowrap;
}
.getLink:hover {
text-decoration: underline;
}
.badges {
display: inline-flex;
align-items: center;
@@ -112,6 +131,17 @@
color: var(--dsw-alias-label-secondary);
}
.badgeSuccess {
border-radius: 999px;
padding: 1px 8px;
font-size: 11px;
line-height: 17px;
white-space: nowrap;
font-weight: 500;
background: var(--dsw-alias-state-success-tertiary);
color: var(--dsw-alias-state-success-primary);
}
.badgeMuted {
border-radius: 999px;
padding: 1px 8px;
@@ -169,6 +199,67 @@
color: var(--dsw-alias-label-tertiary);
}
.autofillNoticeSuccess {
margin: 0;
font-size: 12px;
line-height: 1.5;
color: var(--dsw-alias-state-success-primary);
}
.advancedSection {
border-top: none;
}
.field + .advancedSection,
.notice + .advancedSection {
border-top: 1px solid var(--dsw-alias-border-l2);
}
.advancedToggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 12px 0;
border: none;
background: none;
font: inherit;
font-size: 13px;
font-weight: 500;
line-height: 1.5;
color: var(--dsw-alias-label-primary);
cursor: pointer;
}
.advancedToggle:hover {
color: var(--dsw-alias-label-secondary);
}
.advancedToggle::after {
content: '';
display: inline-block;
width: 6px;
height: 6px;
border-right: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
transform: translateY(-2px) rotate(45deg);
transition: transform 0.2s ease;
}
.advancedToggle[aria-expanded='true']::after {
transform: translateY(1px) rotate(-135deg);
}
.advancedFields {
display: flex;
flex-direction: column;
}
.advancedFields .field:first-child {
padding-top: 0;
border-top: 1px solid var(--dsw-alias-border-l2);
}
.footer {
display: flex;
align-items: center;
+150 -88
View File
@@ -8,14 +8,18 @@
* starts blank, and reports only configured/unconfigured.
*/
import { useState } from 'react'
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import {
BAILIAN_CARD_REFS, dirtyOf, echoedValue, SETTINGS_FIELDS,
type BailianCardFace, type BailianFieldKey,
dirtyOf, echoedValue, SETTINGS_FIELDS,
type BailianCardFace, type BailianCardState, type BailianFieldKey,
} from './bailian-card-controller.ts'
import type { BailianKbLocaleKey } from './locales.ts'
import css from './BailianCard.module.css'
/** Bailian console URL that lets the user create or copy an API key. */
const BAILIAN_CONSOLE_API_KEY_URL = 'https://bailian.console.aliyun.com/cn-beijing?tab=globalset#/efm/api_key'
/** Props the renderer binds for the Bailian section page. */
export type BailianCardProps =
PropsRuntime<'settings.section'>
@@ -26,6 +30,8 @@ export type BailianCardProps =
interface FieldView {
key: BailianFieldKey
labelKey: BailianKbLocaleKey
/** Locale key for the "get" link shown next to the label. */
getKey?: BailianKbLocaleKey
/** Echo-mode explanation (settings-backed value, blank save = fall back). */
hintKey: BailianKbLocaleKey
/** Write-only explanation (credential store, blank = keep the stored value). */
@@ -34,21 +40,27 @@ interface FieldView {
unsetKey: BailianKbLocaleKey
/** Password-masked drafting; only the API key is an actual secret. */
secret: boolean
/** When true the field is hidden inside the collapsible advanced section. */
advanced?: boolean
/** External console URL for the "get" link. */
getUrl?: string
}
/** The controls, in page order. */
const FIELDS: readonly FieldView[] = [
{ 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 },
{ key: 'DASHSCOPE_API_KEY', labelKey: 'apiKey', getKey: 'apiKeyGet', hintKey: 'apiKeyHint', fallbackHintKey: 'apiKeyHint', setKey: 'apiKeySet', unsetKey: 'apiKeyUnset', secret: true, advanced: true, getUrl: BAILIAN_CONSOLE_API_KEY_URL },
{ key: 'BAILIAN_WORKSPACE_ID', labelKey: 'workspaceId', getKey: 'workspaceIdGet', hintKey: 'workspaceIdHint', fallbackHintKey: 'workspaceIdHintFallback', setKey: 'workspaceIdSet', unsetKey: 'workspaceIdUnset', secret: false, advanced: true, getUrl: BAILIAN_CONSOLE_API_KEY_URL },
{ key: 'BAILIAN_DEFAULT_RETRIEVE_AGENT_ID', labelKey: 'retrieveAgentId', hintKey: 'retrieveAgentIdHint', fallbackHintKey: 'retrieveAgentIdHintFallback', setKey: 'retrieveAgentIdSet', unsetKey: 'retrieveAgentIdUnset', secret: false, advanced: true },
{ key: 'BAILIAN_DEFAULT_CHAT_AGENT_ID', labelKey: 'chatAgentId', hintKey: 'chatAgentIdHint', fallbackHintKey: 'chatAgentIdHintFallback', setKey: 'chatAgentIdSet', unsetKey: 'chatAgentIdUnset', secret: false, advanced: true },
]
/** Result-notice locale key per settled autofill outcome. */
const AUTOFILL_NOTICES: Partial<Record<string, BailianKbLocaleKey>> = {
done: 'autofillDone',
awaitingLogin: 'autofillAwaitingLogin',
failed: 'autofillFailed',
const ADVANCED_FIELDS = FIELDS.filter(field => field.advanced)
/** Whether both the API key and the workspace id already hold values. */
function fullyConfigured(state: BailianCardState): boolean {
const apiKeyConfigured = state.credentials.DASHSCOPE_API_KEY.configured
const workspaceConfigured = state.credentials.BAILIAN_WORKSPACE_ID.configured || echoedValue(state, 'BAILIAN_WORKSPACE_ID') !== ''
return apiKeyConfigured && workspaceConfigured
}
/**
@@ -58,12 +70,103 @@ const AUTOFILL_NOTICES: Partial<Record<string, BailianKbLocaleKey>> = {
*/
export function BailianCard(props: BailianCardProps) {
const { t } = props
const [advancedOpen, setAdvancedOpen] = useState(false)
const state = props.useBailianCard(snapshot => snapshot)
const dirty = dirtyOf(state)
const busy = state.saving || state.clearing
const autofillNotice = AUTOFILL_NOTICES[state.autofill]
// The flow spans a browser login, so the button stays disabled until it settles.
const autofillBusy = state.autofill === 'running' || state.autofill === 'awaitingLogin'
// Pick the notice under the autofill button: settled outcome first, then
// a green "already configured" hint, then the default invitation.
let autofillNotice: BailianKbLocaleKey | undefined
let autofillNoticeSuccess = false
if (state.autofill === 'done') {
autofillNotice = 'autofillDone'
autofillNoticeSuccess = true
} else if (state.autofill === 'awaitingLogin') {
autofillNotice = 'autofillAwaitingLogin'
} else if (state.autofill === 'failed') {
autofillNotice = 'autofillFailed'
} else if (fullyConfigured(state)) {
autofillNotice = 'autofillConfigured'
autofillNoticeSuccess = true
}
const renderField = (field: FieldView) => {
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}>
<span className={css.labelWrap}>
<label className={css.label} htmlFor={`bailian-kb-${field.key}`}>{t(field.labelKey)}</label>
{field.getUrl !== undefined && field.getKey !== undefined
? (
<a
className={css.getLink}
href={field.getUrl}
target="_blank"
rel="noreferrer"
>
{t(field.getKey)}
</a>
)
: null}
</span>
<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.badgeSuccess : 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>
)
}
return (
<section className={css.section}>
<div className={css.headRow}>
@@ -80,7 +183,7 @@ export function BailianCard(props: BailianCardProps) {
>
{t(state.autofill === 'running' ? 'autofilling' : 'autofill')}
</button>
<span className={css.hint}>
<span className={autofillNoticeSuccess ? css.autofillNoticeSuccess : css.hint}>
{autofillNotice !== undefined ? t(autofillNotice) : t('autofillHint')}
{/* The host opens the page itself; this link is the fallback when it cannot. */}
{state.autofillLoginUrl !== undefined && state.autofillLoginUrl !== ''
@@ -95,82 +198,41 @@ export function BailianCard(props: BailianCardProps) {
{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 className={css.advancedSection}>
<button
type="button"
className={css.advancedToggle}
aria-expanded={advancedOpen}
onClick={() => { setAdvancedOpen(open => !open) }}
>
{t('advancedConfig')}
</button>
{advancedOpen
? (
<div className={css.advancedFields}>
{ADVANCED_FIELDS.map(renderField)}
<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>
<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 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>
)
: null}
</div>
</div>
</section>
+12 -3
View File
@@ -10,14 +10,15 @@
/** Locale keys this page renders. */
export type BailianKbLocaleKey =
| 'nav' | 'title' | 'description' | 'settingsUnavailable' | 'fallbackConfigured'
| 'apiKey' | 'apiKeyHint' | 'apiKeySet' | 'apiKeyUnset'
| 'workspaceId' | 'workspaceIdHint' | 'workspaceIdHintFallback' | 'workspaceIdSet' | 'workspaceIdUnset'
| 'apiKey' | 'apiKeyHint' | 'apiKeySet' | 'apiKeyUnset' | 'apiKeyGet'
| 'workspaceId' | 'workspaceIdHint' | 'workspaceIdHintFallback' | 'workspaceIdSet' | 'workspaceIdUnset' | 'workspaceIdGet'
| 'retrieveAgentId' | 'retrieveAgentIdHint' | 'retrieveAgentIdHintFallback' | 'retrieveAgentIdSet' | 'retrieveAgentIdUnset'
| 'chatAgentId' | 'chatAgentIdHint' | 'chatAgentIdHintFallback' | 'chatAgentIdSet' | 'chatAgentIdUnset'
| 'fromEnv' | 'clear' | 'clearing'
| 'save' | 'saving' | 'discard' | 'unsaved' | 'saveFailed'
| 'advancedConfig'
| 'autofill' | 'autofilling' | 'autofillHint'
| 'autofillDone' | 'autofillAwaitingLogin' | 'autofillOpenUrl' | 'autofillFailed'
| 'autofillDone' | 'autofillAwaitingLogin' | 'autofillOpenUrl' | 'autofillFailed' | 'autofillConfigured'
/** English copy. */
export const en: Record<BailianKbLocaleKey, string> = {
@@ -30,11 +31,13 @@ export const en: Record<BailianKbLocaleKey, string> = {
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.',
apiKeyGet: 'Get',
workspaceId: 'Workspace id',
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.',
workspaceIdGet: 'Get',
retrieveAgentId: 'Default retrieval service id',
retrieveAgentIdHint: 'agent_id of the default retrieval service (kb_search); when unset, every call must name one (`bl knowledge 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 (`bl knowledge service list` discovers ids). Leave blank to keep the current one.',
@@ -53,6 +56,7 @@ export const en: Record<BailianKbLocaleKey, string> = {
discard: 'Discard',
unsaved: 'Unsaved',
saveFailed: 'The Host did not accept these values; they were left for you to correct.',
advancedConfig: 'Advanced configuration',
autofill: 'Fetch from console login',
autofilling: 'Starting…',
autofillHint: 'Sign in to the Bailian console to fill in that account’s API key and workspace id.',
@@ -60,6 +64,7 @@ export const en: Record<BailianKbLocaleKey, string> = {
autofillAwaitingLogin: 'Waiting for the Bailian console login to finish in a browser on the host machine…',
autofillOpenUrl: 'Open the login page manually',
autofillFailed: 'Auto-fill failed — the credential may be locked by an environment variable, the Host refused the write, or the login was abandoned.',
autofillConfigured: 'Configured. Click button to fetch this account’s API key and workspace id again.',
}
/** Simplified Chinese copy. */
@@ -73,11 +78,13 @@ export const zh: Record<BailianKbLocaleKey, string> = {
apiKeyHint: 'DashScope API key。保存在凭据存储中且不会再次显示;留空表示保持当前值。',
apiKeySet: '已配置密钥。',
apiKeyUnset: '未配置密钥;配置前知识库工具不可用。',
apiKeyGet: '去获取',
workspaceId: '工作空间 ID',
workspaceIdHint: '百炼工作空间 ID,即终端节点地址的子域名。存入设置文档;清空并保存则回退到凭据存储。',
workspaceIdHintFallback: '百炼工作空间 ID,即终端节点地址的子域名。留空表示保持当前值。',
workspaceIdSet: '已配置工作空间。',
workspaceIdUnset: '未配置工作空间;配置前知识库工具不可用。',
workspaceIdGet: '去获取',
retrieveAgentId: '默认检索服务 ID',
retrieveAgentIdHint: '默认检索服务(kb_search)的 agent_id;未设置时每次调用都需显式指定(可用 `bl knowledge service list` 发现 id)。存入设置文档。',
retrieveAgentIdHintFallback: '默认检索服务(kb_search)的 agent_id;未设置时每次调用都需显式指定(可用 `bl knowledge service list` 发现 id)。留空表示保持当前值。',
@@ -96,6 +103,7 @@ export const zh: Record<BailianKbLocaleKey, string> = {
discard: '放弃',
unsaved: '未保存',
saveFailed: '宿主未接受这些值,已保留供你修改。',
advancedConfig: '高级配置',
autofill: '自动获取',
autofilling: '启动中…',
autofillHint: '登录百炼控制台,自动填入该账号的 API 密钥与工作空间 ID。',
@@ -103,4 +111,5 @@ export const zh: Record<BailianKbLocaleKey, string> = {
autofillAwaitingLogin: '等待在宿主机浏览器中完成百炼控制台登录…',
autofillOpenUrl: '手动打开登录页',
autofillFailed: '自动获取失败——凭据可能被环境变量锁定、宿主拒绝了写入,或登录未完成。',
autofillConfigured: '已配置完成,点击按钮重新获取该账号的 API 密钥与工作空间 ID。',
}