diff --git a/packages/tool-bailian-kb/package.json b/packages/tool-bailian-kb/package.json index 6a3f8a4..39bfeba 100644 --- a/packages/tool-bailian-kb/package.json +++ b/packages/tool-bailian-kb/package.json @@ -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", diff --git a/packages/tool-bailian-kb/src/web/BailianCard.module.css b/packages/tool-bailian-kb/src/web/BailianCard.module.css index e20c778..09e560e 100644 --- a/packages/tool-bailian-kb/src/web/BailianCard.module.css +++ b/packages/tool-bailian-kb/src/web/BailianCard.module.css @@ -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; diff --git a/packages/tool-bailian-kb/src/web/BailianCard.tsx b/packages/tool-bailian-kb/src/web/BailianCard.tsx index a3be523..8b87562 100644 --- a/packages/tool-bailian-kb/src/web/BailianCard.tsx +++ b/packages/tool-bailian-kb/src/web/BailianCard.tsx @@ -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> = { - 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> = { */ 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 ( +
+
+ + + {field.getUrl !== undefined && field.getKey !== undefined + ? ( + + {t(field.getKey)} + + ) + : null} + + + {showClear + ? ( + + ) + : null} + {badge !== undefined + ? {badge.label} + : null} + +
+ { props.edit(field.key, event.target.value) }} + /> +

{t(echo ? field.hintKey : field.fallbackHintKey)}

+
+ ) + } + return (
@@ -80,7 +183,7 @@ export function BailianCard(props: BailianCardProps) { > {t(state.autofill === 'running' ? 'autofilling' : 'autofill')} - + {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' ?

{t('settingsUnavailable')}

: 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 ( -
-
- - - {showClear - ? ( - - ) - : null} - {badge !== undefined - ? {badge.label} - : null} - +
+ + {advancedOpen + ? ( +
+ {ADVANCED_FIELDS.map(renderField)} +
+ {state.failed ?

{t('saveFailed')}

: null} + + +
- { props.edit(field.key, event.target.value) }} - /> -

{t(echo ? field.hintKey : field.fallbackHintKey)}

-
- ) - })} -
- {state.failed ?

{t('saveFailed')}

: null} - - + ) + : null}
diff --git a/packages/tool-bailian-kb/src/web/locales.ts b/packages/tool-bailian-kb/src/web/locales.ts index 60becf4..0448c69 100644 --- a/packages/tool-bailian-kb/src/web/locales.ts +++ b/packages/tool-bailian-kb/src/web/locales.ts @@ -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 = { @@ -30,11 +31,13 @@ export const en: Record = { 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 = { 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 = { 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 = { 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 = { discard: '放弃', unsaved: '未保存', saveFailed: '宿主未接受这些值,已保留供你修改。', + advancedConfig: '高级配置', autofill: '自动获取', autofilling: '启动中…', autofillHint: '登录百炼控制台,自动填入该账号的 API 密钥与工作空间 ID。', @@ -103,4 +111,5 @@ export const zh: Record = { autofillAwaitingLogin: '等待在宿主机浏览器中完成百炼控制台登录…', autofillOpenUrl: '手动打开登录页', autofillFailed: '自动获取失败——凭据可能被环境变量锁定、宿主拒绝了写入,或登录未完成。', + autofillConfigured: '已配置完成,点击按钮重新获取该账号的 API 密钥与工作空间 ID。', }