chore: establish YCode replication baseline and TODO
This commit is contained in:
commit
6dc59f89ba
1118 files changed
+196002
No files matched your search
@@ -0,0 +1,72 @@
|
||||
import type { ChatEvent } from '@shared/types'
|
||||
import type { AgentMode, PermissionMode, PlanContent, QuestionItem, QuestionOption } from '@shared/conversation'
|
||||
import type { CapabilityToolSnapshot } from '../capabilities/runtime'
|
||||
import { agentMode, modeToolDenial } from './mode-policy'
|
||||
import type { InteractionBroker } from '../interaction-broker'
|
||||
import { executeTool, type ToolResult } from './tools'
|
||||
|
||||
export interface ConversationToolContext {
|
||||
runId: string
|
||||
toolCallId: string
|
||||
workspaceRoot: string
|
||||
permissionMode?: PermissionMode
|
||||
agentMode?: AgentMode
|
||||
signal: AbortSignal
|
||||
interactions?: InteractionBroker
|
||||
emit(event: ChatEvent): void
|
||||
}
|
||||
export interface ConversationToolResult extends ToolResult { metadata?: Record<string, unknown> }
|
||||
function record(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
|
||||
}
|
||||
export function parseQuestions(value: unknown): QuestionItem[] {
|
||||
if (!Array.isArray(value) || !value.length) throw new Error('提问内容为空')
|
||||
return value.map((item, index) => {
|
||||
const question = record(item)
|
||||
if (typeof question.question !== 'string' || !question.question.trim()) throw new Error('问题内容无效')
|
||||
const options: Array<string | QuestionOption> = (Array.isArray(question.options) ? question.options : []).map((option) => {
|
||||
if (typeof option === 'string') return option
|
||||
const value = record(option)
|
||||
return Object.fromEntries(Object.entries(value).filter(([key, content]) => ['id', 'option', 'question', 'description', 'hint'].includes(key) && typeof content === 'string'))
|
||||
})
|
||||
return { id: String(question.id || `question-${index}`), question: question.question, options, multiSelect: question.multiSelect === true }
|
||||
})
|
||||
}
|
||||
|
||||
/** The native kernel and compatible gateway share the same host execution and interaction path. */
|
||||
export async function executeConversationTool(name: string, args: Record<string, unknown>, ctx: ConversationToolContext): Promise<ConversationToolResult> {
|
||||
if (ctx.signal.aborted) throw Object.assign(new Error('回合已取消'), { name: 'AbortError' })
|
||||
const denial = modeToolDenial(agentMode(ctx.agentMode), name)
|
||||
if (denial) return denial
|
||||
if (['ask_user_question', 'ask_followup_question', 'askfollowupquestion', 'ask-followup-question'].includes(name) && ctx.interactions) {
|
||||
const response = await ctx.interactions.question(ctx.toolCallId, String(args.title || '请回答以下问题'), parseQuestions(args.questions ?? [{ question: args.question, options: args.follow_up ?? args.options ?? [] }]))
|
||||
return { ok: true, content: response.skipped ? '用户跳过了问题' : JSON.stringify(response.answer), data: response.answer,
|
||||
metadata: { questionAnswer: response.answer, questionSkipped: response.skipped } }
|
||||
}
|
||||
if (name === 'todo_write' || name === 'create_plan') {
|
||||
const source = Array.isArray(args.todos) ? args.todos : Array.isArray(args.steps) ? args.steps : []
|
||||
const plan: PlanContent = { id: String(args.planId || 'current-plan'), title: String(args.title || '任务计划'), steps: source.map((item, index) => {
|
||||
const value = record(item)
|
||||
const rawStatus = String(value.status || 'pending')
|
||||
const status = ['pending', 'in_progress', 'completed', 'cancelled'].includes(rawStatus) ? rawStatus as PlanContent['steps'][number]['status'] : 'pending'
|
||||
return { id: String(value.id || index), text: typeof item === 'string' ? item : String(value.content || value.text || value.title || ''), status }
|
||||
}) }
|
||||
return { ok: true, content: JSON.stringify(plan), data: plan }
|
||||
}
|
||||
return executeTool(name, args, { workspaceRoot: ctx.workspaceRoot, signal: ctx.signal, permissionMode: ctx.permissionMode,
|
||||
approve: ctx.interactions ? async (title) => {
|
||||
const response = await ctx.interactions!.approval(ctx.toolCallId, title, [
|
||||
{ optionId: 'allow-once', name: '允许一次', kind: 'allow_once' },
|
||||
{ optionId: 'reject-once', name: '拒绝', kind: 'reject_once' }
|
||||
])
|
||||
return response.optionId === 'allow-once'
|
||||
} : undefined })
|
||||
}
|
||||
|
||||
/** One mode gate before both built-in and dynamic tools; hidden names are still rejected. */
|
||||
export async function executeTurnTool(name: string, args: Record<string, unknown>, ctx: ConversationToolContext, capabilities?: CapabilityToolSnapshot): Promise<ConversationToolResult> {
|
||||
if (ctx.signal.aborted) throw Object.assign(new Error('回合已取消'), { name: 'AbortError' })
|
||||
const denial = modeToolDenial(agentMode(ctx.agentMode), name)
|
||||
if (denial) return denial
|
||||
return await capabilities?.execute(name, args, ctx) ?? await executeConversationTool(name, args, ctx)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { ChatMessage, ToolCallRecord } from '@shared/types'
|
||||
import { getMessageParts } from '@shared/message-parts'
|
||||
import type { WireMessage } from '../gateway'
|
||||
|
||||
/** Preserve ordered assistant/tool exchanges when using a stateless compatible gateway. */
|
||||
export function assistantWireHistory(message: ChatMessage): WireMessage[] {
|
||||
const messages: WireMessage[] = []
|
||||
const parts = getMessageParts(message)
|
||||
let text = ''
|
||||
for (let index = 0; index < parts.length; index++) {
|
||||
const part = parts[index]
|
||||
if (part.type === 'text') { text += part.text; continue }
|
||||
if (part.type !== 'tool') continue
|
||||
const tools: ToolCallRecord[] = []
|
||||
while (index < parts.length && parts[index].type === 'tool') {
|
||||
const candidate = parts[index] as { type: 'tool'; toolId: string }
|
||||
const tool = message.tools?.find((tool) => tool.id === candidate.toolId)
|
||||
if (tool) tools.push(tool)
|
||||
index++
|
||||
}
|
||||
index--
|
||||
if (!tools.length) continue
|
||||
messages.push({ role: 'assistant', content: text || null, tool_calls: tools.map((tool) => ({ id: tool.id, type: 'function', function: { name: tool.name, arguments: tool.argsRaw || '{}' } })) })
|
||||
text = ''
|
||||
for (const tool of tools) messages.push({ role: 'tool', tool_call_id: tool.id, content: tool.status === 'ok' ? tool.output ?? tool.summary ?? JSON.stringify(tool.result ?? '') : `ERROR (${tool.status}): ${tool.output ?? tool.summary ?? '工具未完成'}` })
|
||||
}
|
||||
if (text) messages.push({ role: 'assistant', content: text })
|
||||
return messages
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Agent 循环(最小闭环)。
|
||||
*
|
||||
* 一次「回合」= 用户发一条消息,到助手给出终态为止:
|
||||
* 模型流式输出 → 若产生工具调用则执行并回填 → 再让模型继续 → 直到无工具调用
|
||||
*
|
||||
* 分层约定:
|
||||
* - loop 只负责编排(拼消息、跑工具、发事件),不关心 UI 与持久化细节
|
||||
* - 所有对外输出都通过 emit(ChatEvent),渲染进程据此增量渲染
|
||||
*/
|
||||
|
||||
import type { ChatEvent, ChatMessage, ToolCallRecord } from '@shared/types'
|
||||
import { Gateway, GatewayError, type WireMessage } from '../gateway'
|
||||
import { toolSpecs } from './tools'
|
||||
import { executeTurnTool } from './conversation-tools'
|
||||
import type { InteractionBroker } from '../interaction-broker'
|
||||
import type { AgentMode, PermissionMode } from '@shared/conversation'
|
||||
import { agentMode, agentModeInstructions, modeToolSpecs } from './mode-policy'
|
||||
import type { CapabilityToolSnapshot } from '../capabilities/runtime'
|
||||
|
||||
/** 回填给模型的工具结果上限,避免撑爆上下文 */
|
||||
const TOOL_RESULT_LIMIT = 8000
|
||||
/** UI 展示用的摘要长度 */
|
||||
const TOOL_SUMMARY_LIMIT = 600
|
||||
|
||||
export interface RunTurnOptions {
|
||||
capabilityTools?: CapabilityToolSnapshot
|
||||
supportsToolCall?: boolean
|
||||
interactions?: InteractionBroker
|
||||
permissionMode?: PermissionMode
|
||||
agentMode?: AgentMode
|
||||
images?: Array<{ type: 'image'; data: string; mimeType: string }>
|
||||
wireHistory?: WireMessage[]
|
||||
runId: string
|
||||
/** 本回合之前的历史(不含本次用户消息) */
|
||||
history: ChatMessage[]
|
||||
userText: string
|
||||
signal: AbortSignal
|
||||
emit: (event: ChatEvent) => void
|
||||
|
||||
// ---- 依赖注入:让 loop 不依赖 electron,可被离线测试直接驱动 ----
|
||||
gateway: Gateway
|
||||
model: string
|
||||
maxToolRounds: number
|
||||
workspaceRoot: string
|
||||
}
|
||||
|
||||
export interface TurnResult {
|
||||
content: string
|
||||
tools: ToolCallRecord[]
|
||||
}
|
||||
|
||||
function systemPrompt(workspaceRoot: string, model: string): string {
|
||||
return [
|
||||
'你是 YCode,一个运行在用户本机的 AI 助手。',
|
||||
'',
|
||||
'## 工作方式',
|
||||
'- 需要了解文件内容或目录结构时,主动调用工具,不要凭空猜测。',
|
||||
'- 工具只能访问用户的工作目录内部,越界请求会被拒绝。',
|
||||
'- 任务完成后用简洁的中文回答,必要时用 Markdown。',
|
||||
'',
|
||||
'## 环境',
|
||||
`- 工作目录:${workspaceRoot}`,
|
||||
`- 当前模型:${model}`
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export async function runTurn(options: RunTurnOptions): Promise<TurnResult> {
|
||||
const mode = agentMode(options.agentMode)
|
||||
const { runId, history, userText, signal, emit, gateway, model, maxToolRounds, workspaceRoot } =
|
||||
options
|
||||
|
||||
const messages: WireMessage[] = [
|
||||
{ role: 'system', content: [systemPrompt(workspaceRoot, model), agentModeInstructions(mode)].filter(Boolean).join('\n\n') },
|
||||
...(options.wireHistory ?? history
|
||||
.filter((m) => m.role === 'user' || (m.role === 'assistant' && m.content))
|
||||
.map<WireMessage>((m) => ({ role: m.role, content: m.content }))),
|
||||
{ role: 'user', content: options.images?.length ? [{ type: 'text', text: userText }, ...options.images.map((image) => ({ type: 'image_url' as const, image_url: { url: `data:${image.mimeType};base64,${image.data}` } }))] : userText }
|
||||
]
|
||||
|
||||
const tools: ToolCallRecord[] = []
|
||||
let fullText = ''
|
||||
let inFlightTool: ToolCallRecord | null = null
|
||||
let inFlightStartedAt: number | undefined
|
||||
|
||||
try {
|
||||
for (let round = 0; round < maxToolRounds; round++) {
|
||||
let roundText = ''
|
||||
let reasoning = ''
|
||||
const pendingCalls: { id: string; name: string; argsRaw: string }[] = []
|
||||
|
||||
for await (const delta of gateway.stream({ messages, tools: options.supportsToolCall === false ? undefined : modeToolSpecs([...toolSpecs(), ...(options.capabilityTools?.specs ?? [])], mode), signal })) {
|
||||
if (delta.type === 'text') {
|
||||
roundText += delta.text
|
||||
fullText += delta.text
|
||||
emit({ runId, type: 'text', delta: delta.text })
|
||||
} else if (delta.type === 'reasoning') {
|
||||
reasoning += delta.text
|
||||
emit({ runId, type: 'part', part: { type: 'reasoning', id: `reasoning-${round}`, text: reasoning } })
|
||||
} else if (delta.type === 'tool_calls') {
|
||||
for (const call of delta.calls) {
|
||||
pendingCalls.push({ id: call.id, name: call.function.name, argsRaw: call.function.arguments })
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reasoning) emit({ runId, type: 'part', part: { type: 'reasoning', id: `reasoning-${round}`, text: reasoning, finished: true } })
|
||||
|
||||
if (pendingCalls.length === 0) {
|
||||
// 没有工具调用 → 本回合结束
|
||||
break
|
||||
}
|
||||
|
||||
// 助手这一轮的消息(含工具调用)要回填进上下文
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: roundText || null,
|
||||
tool_calls: pendingCalls.map((c) => ({
|
||||
id: c.id,
|
||||
type: 'function' as const,
|
||||
function: { name: c.name, arguments: c.argsRaw }
|
||||
}))
|
||||
})
|
||||
|
||||
for (const call of pendingCalls) {
|
||||
const record: ToolCallRecord = {
|
||||
id: call.id,
|
||||
name: call.name,
|
||||
args: safeParse(call.argsRaw),
|
||||
argsRaw: call.argsRaw,
|
||||
status: 'running'
|
||||
}
|
||||
tools.push(record)
|
||||
record.metadata = options.capabilityTools?.metadata?.(record.name)
|
||||
emit({ runId, type: 'tool_call', call: { ...record } })
|
||||
|
||||
const startedAt = Date.now()
|
||||
inFlightTool = record
|
||||
inFlightStartedAt = startedAt
|
||||
const args = asRecord(record.args)
|
||||
const ctx = { runId, toolCallId: call.id,
|
||||
workspaceRoot, signal, interactions: options.interactions, permissionMode: options.permissionMode, agentMode: mode, emit }
|
||||
const result = await executeTurnTool(call.name, args, ctx, options.capabilityTools)
|
||||
record.metadata = result.metadata ?? record.metadata
|
||||
record.result = result.data ?? { success: result.ok, content: result.content }
|
||||
record.status = signal.aborted ? 'cancelled' : result.ok ? 'ok' : 'error'
|
||||
record.durationMs = elapsedSince(startedAt)
|
||||
record.output = result.content
|
||||
record.summary = truncate(result.content, TOOL_SUMMARY_LIMIT)
|
||||
emit({ runId, type: 'tool_result', call: { ...record } })
|
||||
inFlightTool = null
|
||||
inFlightStartedAt = undefined
|
||||
|
||||
if (signal.aborted) {
|
||||
const error = new Error('回合已取消')
|
||||
error.name = 'AbortError'
|
||||
throw error
|
||||
}
|
||||
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: call.id,
|
||||
content: result.ok
|
||||
? truncate(result.content, TOOL_RESULT_LIMIT)
|
||||
: `ERROR: ${result.content}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
emit({ runId, type: 'done', content: fullText, tools })
|
||||
return { content: fullText, tools }
|
||||
} catch (err) {
|
||||
if (inFlightTool?.status === 'running') {
|
||||
const cancelled = (err as Error).name === 'AbortError' || signal.aborted
|
||||
inFlightTool.status = cancelled ? 'cancelled' : 'error'
|
||||
if (inFlightStartedAt !== undefined) {
|
||||
inFlightTool.durationMs = elapsedSince(inFlightStartedAt)
|
||||
}
|
||||
emit({ runId, type: 'tool_result', call: { ...inFlightTool } })
|
||||
inFlightTool = null
|
||||
inFlightStartedAt = undefined
|
||||
}
|
||||
if ((err as Error).name === 'AbortError' || signal.aborted) {
|
||||
emit({ runId, type: 'cancelled' })
|
||||
throw err
|
||||
}
|
||||
const message =
|
||||
err instanceof GatewayError
|
||||
? [err.message, err.body].filter(Boolean).join('\n')
|
||||
: (err as Error).message
|
||||
emit({ runId, type: 'error', message })
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function safeParse(raw: string): unknown {
|
||||
try {
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {}
|
||||
}
|
||||
|
||||
function truncate(text: string, limit: number): string {
|
||||
return text.length <= limit ? text : `${text.slice(0, limit)}\n…(已截断,共 ${text.length} 字符)`
|
||||
}
|
||||
|
||||
function elapsedSince(startedAt: number): number {
|
||||
return Math.max(0, Date.now() - startedAt)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { AgentMode } from '@shared/conversation'
|
||||
import type { ToolSpec } from '@shared/types'
|
||||
import type { SessionContext } from '@shared/workspaces'
|
||||
|
||||
const THINKING_LEVELS = new Set(['off', 'low', 'medium', 'high', 'xhigh', 'max'])
|
||||
/** Accepts only levels this host persists; OMP's `auto`/`minimal` are not offered by the selector. */
|
||||
export function thinkingLevel(value: unknown): import('@shared/workspaces').ThinkingLevel | undefined {
|
||||
if (value === undefined || value === null) return undefined
|
||||
if (typeof value === 'string' && THINKING_LEVELS.has(value)) return value as import('@shared/workspaces').ThinkingLevel
|
||||
throw new Error('无效的思考强度')
|
||||
}
|
||||
|
||||
/** Host adapter for the source craft/plan/ask UI contract; not an OMP engine mode. */
|
||||
export function agentMode(value: unknown): AgentMode {
|
||||
if (value === undefined) return 'craft'
|
||||
if (value === 'craft' || value === 'plan' || value === 'ask') return value
|
||||
throw new Error('无效的对话模式,仅支持 craft、plan、ask')
|
||||
}
|
||||
|
||||
/** Source ConfigCapability clears expert in plan/ask; selecting an expert restores craft. */
|
||||
export function agentModeContextPatch(previous: SessionContext, patch: SessionContext): SessionContext {
|
||||
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) throw new Error('会话配置无效')
|
||||
if (patch.expertId !== undefined && patch.expertId !== null && (typeof patch.expertId !== 'string' || !patch.expertId.trim())) throw new Error('专家标识无效')
|
||||
const mode = patch.agentMode === undefined ? agentMode(previous.agentMode) : agentMode(patch.agentMode)
|
||||
const next = { ...patch, agentMode: mode }
|
||||
if (patch.expertId) next.agentMode = 'craft'
|
||||
else if (patch.agentMode !== undefined && mode !== 'craft') next.expertId = null
|
||||
return next
|
||||
}
|
||||
|
||||
const READ_TOOLS = new Set(['list_dir', 'read_file', 'ask_user_question', 'ask_followup_question',
|
||||
'askfollowupquestion', 'ask-followup-question', 'ycode_load_skill', 'ycode_read_skill_resource', 'ycode_expert_resource'])
|
||||
const PLAN_TOOLS = new Set(['todo_write', 'create_plan'])
|
||||
export function modeAllowsTool(mode: AgentMode, name: string): boolean {
|
||||
return mode === 'craft' || READ_TOOLS.has(name) || mode === 'plan' && PLAN_TOOLS.has(name)
|
||||
}
|
||||
export function modeToolSpecs(specs: ToolSpec[], mode: AgentMode): ToolSpec[] {
|
||||
return mode === 'craft' ? specs : specs.filter(spec => modeAllowsTool(mode, spec.function.name))
|
||||
}
|
||||
export function modeToolDenial(mode: AgentMode, name: string): { ok: false; content: string } | undefined {
|
||||
if (modeAllowsTool(mode, name)) return undefined
|
||||
return { ok: false, content: `当前 ${mode} 模式不允许工具 ${name}。此模式只允许读取和提问${mode === 'plan' ? ',以及维护会话计划' : ''};写文件、命令执行、连接器和专家包修改需切换到 craft。` }
|
||||
}
|
||||
export function agentModeInstructions(mode: AgentMode): string {
|
||||
if (mode === 'craft') return '<ycode-agent-mode mode="craft">当前会话为 craft 执行模式;此前回合的 plan/ask 只读限制不再决定本回合模式。可按用户任务使用当前已提供的工具,仍须遵守本地权限和审批;此模式不自动授予完全访问。</ycode-agent-mode>'
|
||||
return `<ycode-agent-mode mode="${mode}">当前会话为 ${mode === 'plan' ? '规划' : '问答'}模式。${mode === 'plan'
|
||||
? '先读取资料、澄清需求并给出可执行计划,可以维护会话中的 todo/plan;不要实际实施代码修改。'
|
||||
: '只读取资料、解释问题并按需向用户提问;不要修改文件或维护执行计划。'}宿主会拒绝文件写入、shell、MCP 和专家包修改,即使用户开启完全访问也不放行;需要实施时请用户先切换 craft。该限制是 YCode 本地工具策略,不等同于 WorkBuddy 或 OMP 的全部原生模式行为;文件读取范围仍受当前本地权限配置约束。</ycode-agent-mode>`
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
/**
|
||||
* 以 OMP 为内核运行一次对话回合。
|
||||
*
|
||||
* 与 `loop.ts` 的 `runTurn` 输出同一套 ChatEvent,渲染进程无需感知内核差异。
|
||||
*
|
||||
* 事件映射(docs/reference/omp/rpc.md Event Stream Schema):
|
||||
* - `message_update.assistantMessageEvent.type = "text_delta"` → ChatEvent `text`
|
||||
* - `tool_execution_start / _update / _end` → ChatEvent `tool_call` / `tool_result`
|
||||
* - `agent_end`(yielded 非假)→ ChatEvent `done`
|
||||
* - `prompt_result.status = "error"` → ChatEvent `error`
|
||||
* - 取消(abort)→ ChatEvent `cancelled`
|
||||
*/
|
||||
|
||||
import type { ChatEvent, ToolCallRecord } from '@shared/types'
|
||||
import { OmpKernel, type OmpFrame, type OmpToolInfo } from '../omp'
|
||||
import { toolSpecs } from './tools'
|
||||
import { executeTurnTool } from './conversation-tools'
|
||||
import type { AgentMode, PermissionMode } from '@shared/conversation'
|
||||
import { agentMode, agentModeInstructions, modeToolSpecs } from './mode-policy'
|
||||
import type { InteractionBroker } from '../interaction-broker'
|
||||
import type { CapabilityToolSnapshot } from '../capabilities/runtime'
|
||||
import type { ThinkingLevel } from '@shared/workspaces'
|
||||
|
||||
const TOOL_SUMMARY_LIMIT = 600
|
||||
const TOOL_JSON_FALLBACK_LIMIT = 16_000
|
||||
|
||||
export interface RunOmpTurnOptions {
|
||||
capabilityTools?: CapabilityToolSnapshot
|
||||
supportsToolCall?: boolean
|
||||
workspaceRoot?: string
|
||||
permissionMode?: PermissionMode
|
||||
agentMode?: AgentMode
|
||||
thinkingLevel?: ThinkingLevel
|
||||
interactions?: InteractionBroker
|
||||
images?: Array<{ type: 'image'; data: string; mimeType: string }>
|
||||
runId: string
|
||||
kernel: OmpKernel
|
||||
userText: string
|
||||
signal: AbortSignal
|
||||
emit: (event: ChatEvent) => void
|
||||
}
|
||||
|
||||
export interface OmpTurnResult {
|
||||
content: string
|
||||
tools: ToolCallRecord[]
|
||||
/** 终态:正常结束为 done;abort 后到达的 agent_end 记为 cancelled */
|
||||
status: 'done' | 'cancelled'
|
||||
}
|
||||
|
||||
export async function runOmpTurn(options: RunOmpTurnOptions): Promise<OmpTurnResult> {
|
||||
const { runId, kernel, signal, emit } = options
|
||||
const mode = agentMode(options.agentMode)
|
||||
const instructions = agentModeInstructions(mode)
|
||||
const userText = instructions && !options.userText.includes(instructions) ? `${options.userText}\n\n${instructions}` : options.userText
|
||||
|
||||
// 已取消的回合不能再向共享内核发送 prompt 或 abort。
|
||||
if (signal.aborted) {
|
||||
emit({ runId, type: 'cancelled' })
|
||||
const error = new Error('回合已取消')
|
||||
error.name = 'AbortError'
|
||||
throw error
|
||||
}
|
||||
if (options.workspaceRoot) {
|
||||
const registration = await kernel.setHostTools(options.supportsToolCall === false ? [] : modeToolSpecs([...toolSpecs(), ...(options.capabilityTools?.specs ?? [])], mode).map((tool) => ({ ...tool.function, label: tool.function.name })))
|
||||
if (registration.success === false) throw new Error('OMP 宿主工具注册失败')
|
||||
}
|
||||
// Applied per turn so a level chosen mid-conversation reaches the very next request without a kernel restart.
|
||||
if (options.thinkingLevel) await kernel.setThinkingLevel(options.thinkingLevel)
|
||||
|
||||
const tools: ToolCallRecord[] = []
|
||||
let fullText = ''
|
||||
let settled = false
|
||||
/** 是否已请求取消:abort 后到达的 agent_end 一律按「已取消」收尾 */
|
||||
let aborted = false
|
||||
let cleanup = (): void => {}
|
||||
const reasoning = new Map<string, string>()
|
||||
const toolStartedAt = new Map<string, number>()
|
||||
const hostRequests = new Set<string>()
|
||||
const hostMetadata = new Map<string, Record<string, unknown>>()
|
||||
const hostResults = new Map<string, unknown>()
|
||||
let assistantSequence = 0
|
||||
|
||||
const onAbort = (): void => {
|
||||
if (settled) return
|
||||
aborted = true
|
||||
void kernel.abort().catch(() => undefined)
|
||||
}
|
||||
signal.addEventListener('abort', onAbort)
|
||||
|
||||
const finalizeRunning = (status: 'error' | 'cancelled'): void => {
|
||||
for (const record of tools) {
|
||||
if (record.status !== 'running') continue
|
||||
record.status = status
|
||||
const startedAt = toolStartedAt.get(record.id)
|
||||
if (startedAt !== undefined) record.durationMs = elapsedSince(startedAt)
|
||||
toolStartedAt.delete(record.id)
|
||||
emit({ runId, type: 'tool_result', call: { ...record } })
|
||||
}
|
||||
}
|
||||
|
||||
const finish = (
|
||||
event: ChatEvent,
|
||||
result: OmpTurnResult,
|
||||
inFlightStatus: 'error' | 'cancelled' = 'error'
|
||||
): OmpTurnResult => {
|
||||
if (settled) return result
|
||||
settled = true
|
||||
signal.removeEventListener('abort', onAbort)
|
||||
cleanup()
|
||||
for (const [id, text] of reasoning) emit({ runId, type: 'part', part: { type: 'reasoning', id, text, finished: true } })
|
||||
finalizeRunning(inFlightStatus)
|
||||
emit(event)
|
||||
return result
|
||||
}
|
||||
|
||||
return new Promise<OmpTurnResult>((resolve, reject) => {
|
||||
const onEvent = (frame: OmpFrame): void => {
|
||||
if (settled) return
|
||||
|
||||
switch (frame.type) {
|
||||
case 'message_start':
|
||||
if (isRecord(frame.message) && frame.message.role === 'assistant') assistantSequence++
|
||||
break
|
||||
case 'host_tool_call': {
|
||||
const requestId = typeof frame.id === 'string' ? frame.id : ''
|
||||
if (!requestId || hostRequests.has(requestId)) break
|
||||
hostRequests.add(requestId)
|
||||
const toolCallId = typeof frame.toolCallId === 'string' ? frame.toolCallId : requestId
|
||||
const name = typeof frame.name === 'string' ? frame.name : typeof frame.toolName === 'string' ? frame.toolName : ''
|
||||
const rawArgs = frame.args ?? frame.arguments
|
||||
const parsedArgs = typeof rawArgs === 'string' ? safeParse(rawArgs) : rawArgs
|
||||
const args = isRecord(parsedArgs) ? parsedArgs : {}
|
||||
void (async () => {
|
||||
try {
|
||||
if (!options.workspaceRoot) throw new Error('宿主工具未连接')
|
||||
const ctx = { runId, toolCallId, workspaceRoot: options.workspaceRoot,
|
||||
permissionMode: options.permissionMode, agentMode: mode, interactions: options.interactions, signal, emit }
|
||||
const result = await executeTurnTool(name, args, ctx, options.capabilityTools)
|
||||
if (result.metadata) hostMetadata.set(toolCallId, result.metadata)
|
||||
hostResults.set(toolCallId, result.data ?? { success: result.ok, content: result.content })
|
||||
if (!settled) await kernel.respondToHostTool(requestId, result.content, !result.ok)
|
||||
} catch (error) {
|
||||
if (!settled) await kernel.respondToHostTool(requestId, error instanceof Error ? error.message : String(error), true).catch(fail)
|
||||
}
|
||||
})()
|
||||
break
|
||||
}
|
||||
case 'message_update': {
|
||||
const evt = frame.assistantMessageEvent as
|
||||
| { type?: string; delta?: string; contentIndex?: number }
|
||||
| undefined
|
||||
if (evt?.type === 'text_delta' && typeof evt.delta === 'string') {
|
||||
fullText += evt.delta
|
||||
emit({ runId, type: 'text', delta: evt.delta })
|
||||
} else if (evt?.type === 'thinking_delta' && typeof evt.delta === 'string') {
|
||||
const id = `thinking-${assistantSequence}-${String(evt.contentIndex ?? 0)}`
|
||||
const text = (reasoning.get(id) || '') + evt.delta
|
||||
reasoning.set(id, text)
|
||||
emit({ runId, type: 'part', part: { type: 'reasoning', id, text } })
|
||||
} else if (evt?.type === 'thinking_end') {
|
||||
const id = `thinking-${assistantSequence}-${String(evt.contentIndex ?? 0)}`
|
||||
emit({ runId, type: 'part', part: { type: 'reasoning', id, text: reasoning.get(id) || '', finished: true } })
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'message_end': {
|
||||
const message = frame.message
|
||||
if (isRecord(message) && message.role === 'assistant' && message.stopReason === 'error') {
|
||||
failProvider(message.errorMessage)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'tool_execution_start': {
|
||||
const info = readToolInfo(frame, 'start')
|
||||
if (!info) break
|
||||
const id = info.id ?? info.name
|
||||
if (!id) break
|
||||
const record: ToolCallRecord = {
|
||||
id,
|
||||
name: info.name ?? id,
|
||||
intent: info.intent,
|
||||
args: safeParse(info.argsRaw ?? '{}'),
|
||||
argsRaw: info.argsRaw ?? '{}',
|
||||
status: 'running'
|
||||
}
|
||||
record.metadata = options.capabilityTools?.metadata?.(record.name)
|
||||
if (info.output !== undefined) setOutput(record, info.output)
|
||||
toolStartedAt.set(id, Date.now())
|
||||
tools.push(record)
|
||||
emit({ runId, type: 'tool_call', call: { ...record } })
|
||||
break
|
||||
}
|
||||
|
||||
case 'tool_execution_update': {
|
||||
const info = readToolInfo(frame, 'update')
|
||||
if (!info?.id) break
|
||||
const record = tools.find((t) => t.id === info.id)
|
||||
if (record && info.output !== undefined) {
|
||||
setOutput(record, info.output)
|
||||
emit({ runId, type: 'tool_result', call: { ...record } })
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'tool_execution_end': {
|
||||
const info = readToolInfo(frame, 'end')
|
||||
if (!info?.id) break
|
||||
const record = tools.find((t) => t.id === info.id)
|
||||
if (record) {
|
||||
record.result = hostResults.get(record.id) ?? frame.result ?? frame.output
|
||||
if (isRecord(frame.result) && isRecord(frame.result.details)) record.metadata = { ...record.metadata, ...frame.result.details }
|
||||
if (hostMetadata.has(record.id)) record.metadata = { ...record.metadata, ...hostMetadata.get(record.id) }
|
||||
if (info.output !== undefined) setOutput(record, info.output)
|
||||
record.status = info.isError || info.ok === false ? 'error' : 'ok'
|
||||
const startedAt = toolStartedAt.get(record.id)
|
||||
if (startedAt !== undefined) record.durationMs = elapsedSince(startedAt)
|
||||
toolStartedAt.delete(record.id)
|
||||
emit({ runId, type: 'tool_result', call: { ...record } })
|
||||
if (isRecord(frame.result) && Array.isArray(frame.result.content)) {
|
||||
frame.result.content.forEach((part, index) => {
|
||||
if (isRecord(part) && part.type === 'image' && typeof part.data === 'string' && typeof part.mimeType === 'string' && /^image\/(png|jpeg|gif|webp)$/.test(part.mimeType)) {
|
||||
emit({ runId, type: 'part', part: { type: 'image', id: `${record.id}-image-${index}`, url: `data:${part.mimeType};base64,${part.data}` } })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'agent_end': {
|
||||
// A provider failure can precede a nonterminal automatic-retry frame.
|
||||
// Report the actual failure rather than leaving an empty streaming bubble.
|
||||
const message = Array.isArray(frame.messages)
|
||||
? frame.messages.findLast((item) => isRecord(item) && item.role === 'assistant')
|
||||
: undefined
|
||||
if (isRecord(message) && message.stopReason === 'error') {
|
||||
failProvider(message.errorMessage)
|
||||
break
|
||||
}
|
||||
// yielded === false 表示内核还在自行推进(重试/压缩),不算回合结束
|
||||
const yielded = frame.yielded === undefined ? frame.isTerminal !== false : frame.yielded
|
||||
if (yielded) {
|
||||
const result: OmpTurnResult = { content: fullText, tools, status: aborted ? 'cancelled' : 'done' }
|
||||
if (aborted) {
|
||||
resolve(finish({ runId, type: 'cancelled' }, result, 'cancelled'))
|
||||
} else {
|
||||
resolve(finish({ runId, type: 'done', content: fullText, tools }, result))
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'prompt_result': {
|
||||
const status = frame.status
|
||||
if (status === 'error') {
|
||||
const message =
|
||||
(frame.error as { message?: string } | undefined)?.message ?? 'OMP 回合失败'
|
||||
fail(new Error(message))
|
||||
return
|
||||
}
|
||||
// aborted → 等待 abort 后的 agent_end;若内核直接结束则在此收尾
|
||||
if (status === 'aborted') {
|
||||
const result: OmpTurnResult = { content: fullText, tools, status: 'cancelled' }
|
||||
resolve(finish({ runId, type: 'cancelled' }, result, 'cancelled'))
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'notice': {
|
||||
const level = (frame as { level?: string }).level
|
||||
const text = typeof frame.message === 'string' ? frame.message : ''
|
||||
if (level === 'error' && text) {
|
||||
fail(new Error(text))
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const fail = (err: Error): void => {
|
||||
if (settled) return
|
||||
finish(
|
||||
{ runId, type: 'error', message: err.message },
|
||||
{ content: fullText, tools, status: 'done' },
|
||||
'error'
|
||||
)
|
||||
reject(err)
|
||||
}
|
||||
const failProvider = (message: unknown): void => {
|
||||
if (settled) return
|
||||
if (aborted || signal.aborted) {
|
||||
resolve(finish({ runId, type: 'cancelled' }, { content: fullText, tools, status: 'cancelled' }, 'cancelled'))
|
||||
return
|
||||
}
|
||||
// OMP otherwise retries the provider failure outside the failed host turn.
|
||||
// Abort only this failed prompt; successful long thinking has no first-byte deadline.
|
||||
void kernel.abort().catch(() => kernel.dispose()).catch(() => undefined)
|
||||
fail(new Error(providerFailureMessage(message)))
|
||||
}
|
||||
const onError = (err: Error): void => fail(err)
|
||||
const onExit = (): void => onError(new Error('OMP 进程已退出'))
|
||||
|
||||
kernel.on('event', onEvent)
|
||||
kernel.once('error', onError)
|
||||
kernel.once('exit', onExit)
|
||||
|
||||
// 完成 / 失败后解除监听
|
||||
cleanup = (): void => {
|
||||
kernel.off('event', onEvent)
|
||||
kernel.off('error', onError)
|
||||
kernel.off('exit', onExit)
|
||||
}
|
||||
|
||||
kernel
|
||||
.prompt(userText, options.images)
|
||||
.then((frame) => {
|
||||
if (settled) return
|
||||
// prompt 的 ack 不代表回合结束;真正的结束由 agent_end / prompt_result 驱动。
|
||||
// 但若 ack 失败,这里直接报错。
|
||||
if (frame.success === false) {
|
||||
const message =
|
||||
(frame.error as { message?: string } | undefined)?.message ?? 'OMP 命令被拒绝'
|
||||
fail(new Error(message))
|
||||
}
|
||||
})
|
||||
.catch((err: Error) => fail(err))
|
||||
})
|
||||
}
|
||||
|
||||
function providerFailureMessage(value: unknown): string {
|
||||
if (typeof value !== 'string' || !value.trim()) return 'OMP 模型服务请求失败'
|
||||
return value.trim()
|
||||
.replace(/https?:\/\/[^\s"'<>]+/gi, '[API 地址]')
|
||||
.replace(/\bBearer\s+[^\s"',}]+/gi, 'Bearer [已隐藏]')
|
||||
.replace(/\b(api[-_\s]?key|authorization)\b(["'\s:=]+)[^\s,"'}]+/gi, '$1$2[已隐藏]')
|
||||
.replace(/\bsk[-_][\w-]{8,}/g, '[已隐藏]')
|
||||
.slice(0, 1000)
|
||||
}
|
||||
|
||||
/** 从工具事件里尽力取出 id / name / args / output(协议文档未给全载荷,做兼容解析) */
|
||||
function readToolInfo(frame: OmpFrame, phase: 'start' | 'update' | 'end'): OmpToolInfo | null {
|
||||
const candidates: unknown[] = [
|
||||
frame.toolCall,
|
||||
frame.call,
|
||||
frame.tool,
|
||||
frame.data,
|
||||
frame
|
||||
]
|
||||
const objects = candidates.filter(isRecord)
|
||||
const id = firstString(...objects.flatMap((o) => [o.id, o.toolCallId, o.callId])) ?? undefined
|
||||
const name = firstString(...objects.flatMap((o) => [o.name, o.toolName, o.title])) ?? undefined
|
||||
const intent = firstString(...objects.flatMap((o) => [o.intent])) ?? undefined
|
||||
if (!id && !name) return null
|
||||
|
||||
const args = objects.flatMap((o) => [o.args, o.arguments, o.input]).find((value) => value !== undefined)
|
||||
const argsRaw = args === undefined ? undefined : serialize(args)
|
||||
const outputKeys =
|
||||
phase === 'update'
|
||||
? ['partialResult', 'partial', 'result', 'output', 'content', 'summary']
|
||||
: ['result', 'output', 'content', 'summary', 'partialResult', 'partial']
|
||||
let output: string | undefined
|
||||
for (const o of objects) {
|
||||
for (const key of outputKeys) {
|
||||
if (!(key in o)) continue
|
||||
output = extractOutput(o[key])
|
||||
if (output !== undefined) break
|
||||
}
|
||||
if (output !== undefined) break
|
||||
}
|
||||
|
||||
const successValues = objects.flatMap((o) => [
|
||||
o.success,
|
||||
isRecord(o.result) ? o.result.success : undefined,
|
||||
isRecord(o.partialResult) ? o.partialResult.success : undefined
|
||||
])
|
||||
const ok = successValues.includes(false) ? false : successValues.includes(true) ? true : undefined
|
||||
const isError = objects.some((o) =>
|
||||
[o.isError, isRecord(o.result) ? o.result.isError : undefined].includes(true)
|
||||
)
|
||||
|
||||
return { id, name, intent, argsRaw, output, ok, isError }
|
||||
}
|
||||
|
||||
function firstString(...values: unknown[]): string | null {
|
||||
for (const v of values) {
|
||||
if (typeof v === 'string' && v) return v
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function safeParse(raw: string): unknown {
|
||||
try {
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
function serialize(value: unknown): string | undefined {
|
||||
if (typeof value === 'string') return value
|
||||
try {
|
||||
return JSON.stringify(value)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function extractOutput(value: unknown): string | undefined {
|
||||
if (typeof value === 'string') return value
|
||||
if (value === undefined || value === null) return undefined
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const text = textContent(value)
|
||||
if (text !== undefined) return text
|
||||
if (value.some(isContentPart)) return undefined
|
||||
}
|
||||
|
||||
if (isRecord(value)) {
|
||||
if (value.type === 'text' && typeof value.text === 'string') return value.text
|
||||
if ('content' in value) {
|
||||
if (Array.isArray(value.content)) return textContent(value.content)
|
||||
return extractOutput(value.content)
|
||||
}
|
||||
if (containsBinaryPayload(value)) return undefined
|
||||
}
|
||||
|
||||
const json = serialize(value)
|
||||
return json === undefined ? undefined : truncate(json, TOOL_JSON_FALLBACK_LIMIT)
|
||||
}
|
||||
|
||||
function textContent(parts: unknown[]): string | undefined {
|
||||
const textParts = parts
|
||||
.filter(isRecord)
|
||||
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
||||
.map((part) => part.text as string)
|
||||
// No local OMP contract defines separators between text blocks; preserve order with line boundaries.
|
||||
return textParts.length ? textParts.join('\n') : undefined
|
||||
}
|
||||
|
||||
function isContentPart(value: unknown): boolean {
|
||||
if (!isRecord(value) || typeof value.type !== 'string') return false
|
||||
const kind = value.type.toLowerCase()
|
||||
return /^(text|image|audio|video|binary|blob|file)([_-]|$)/.test(kind)
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function containsBinaryPayload(value: unknown, depth = 0): boolean {
|
||||
if (depth > 4 || value === null || typeof value !== 'object') return false
|
||||
if (
|
||||
Buffer.isBuffer(value) ||
|
||||
value instanceof ArrayBuffer ||
|
||||
ArrayBuffer.isView(value)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.some((item) => containsBinaryPayload(item, depth + 1))
|
||||
}
|
||||
const object = value as Record<string, unknown>
|
||||
const kind = typeof object.type === 'string' ? object.type.toLowerCase() : ''
|
||||
if (['image', 'audio', 'video', 'binary', 'blob'].includes(kind)) return true
|
||||
const mediaType = object.mimeType ?? object.mediaType
|
||||
if (typeof mediaType === 'string' && /^(image|audio|video)\//i.test(mediaType)) return true
|
||||
for (const [key, child] of Object.entries(object)) {
|
||||
if (['image', 'images', 'audio', 'video', 'binary', 'blob', 'bytes', 'buffer', 'base64'].includes(key.toLowerCase())) {
|
||||
return true
|
||||
}
|
||||
if (containsBinaryPayload(child, depth + 1)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function setOutput(record: ToolCallRecord, output: string): void {
|
||||
record.output = output
|
||||
record.summary = truncate(output)
|
||||
}
|
||||
|
||||
function elapsedSince(startedAt: number): number {
|
||||
return Math.max(0, Date.now() - startedAt)
|
||||
}
|
||||
|
||||
function truncate(text: string, limit = TOOL_SUMMARY_LIMIT): string {
|
||||
return text.length <= limit ? text : `${text.slice(0, limit)}\n…(已截断,共 ${text.length} 字符)`
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* 宿主工具系统:路径约束、审批与取消由同一执行入口处理。
|
||||
*
|
||||
* 约定:
|
||||
* - 所有工具都在 workspaceRoot 内活动,越界一律拒绝(含符号链接解析后的真实路径)
|
||||
* - 工具结果统一为文本,便于回填给模型
|
||||
* - 工具实现不感知 UI;进度与结果由 loop.ts 转成 ChatEvent
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { isAbsolute, join, relative, resolve, sep } from 'node:path'
|
||||
import { dirname } from 'node:path'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { ToolSpec } from '@shared/types'
|
||||
import type { PermissionMode } from '@shared/conversation'
|
||||
|
||||
export interface ToolContext {
|
||||
workspaceRoot: string
|
||||
signal?: AbortSignal
|
||||
permissionMode?: PermissionMode
|
||||
approve?: (title: string) => Promise<boolean>
|
||||
}
|
||||
|
||||
export interface ToolResult {
|
||||
ok: boolean
|
||||
content: string
|
||||
data?: unknown
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
name: string
|
||||
description: string
|
||||
parameters: Record<string, unknown>
|
||||
run(args: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult>
|
||||
}
|
||||
|
||||
class ToolError extends Error {}
|
||||
async function writeTextAtomically(path: string, content: string): Promise<void> {
|
||||
const temporary = join(dirname(path), `.ycode-write-${randomUUID()}.tmp`)
|
||||
try { await fs.writeFile(temporary, content, 'utf8'); await fs.rename(temporary, path) }
|
||||
finally { await fs.unlink(temporary).catch(() => undefined) }
|
||||
}
|
||||
|
||||
/** 把用户/模型给的路径解析到工作区内,越界即抛错 */
|
||||
async function resolveInWorkspace(input: unknown, ctx: ToolContext): Promise<string> {
|
||||
const raw = typeof input === 'string' && input.trim() ? input.trim() : '.'
|
||||
const rootReal = await fs.realpath(ctx.workspaceRoot).catch(() => resolve(ctx.workspaceRoot))
|
||||
const target = isAbsolute(raw) ? resolve(raw) : resolve(rootReal, raw)
|
||||
let existing = target
|
||||
const suffix: string[] = []
|
||||
let targetReal: string
|
||||
for (;;) {
|
||||
try { targetReal = join(await fs.realpath(existing), ...suffix); break }
|
||||
catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||
const parent = dirname(existing)
|
||||
if (parent === existing) throw error
|
||||
suffix.unshift(existing.slice(parent.length + (parent.endsWith(sep) ? 0 : 1)))
|
||||
existing = parent
|
||||
}
|
||||
}
|
||||
const rel = relative(rootReal, targetReal)
|
||||
|
||||
if (rel === '') return rootReal
|
||||
if (ctx.permissionMode !== 'full-access' && (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))) {
|
||||
throw new ToolError(`路径越界,仅允许在工作目录内操作:${raw}`)
|
||||
}
|
||||
return targetReal
|
||||
}
|
||||
|
||||
const listDir: Tool = {
|
||||
name: 'list_dir',
|
||||
description:
|
||||
'列出工作目录内的文件与子目录。返回名称、类型与大小。用于了解项目结构。',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: '相对于工作目录的路径,默认 "." 表示根目录' },
|
||||
description: { type: 'string', description: 'Optional short English action label for the conversation timeline, e.g. "List contents of src directory". Keep it concise.' }
|
||||
},
|
||||
required: []
|
||||
},
|
||||
async run(args, ctx) {
|
||||
const dir = await resolveInWorkspace(args.path, ctx)
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true })
|
||||
if (entries.length === 0) return { ok: true, content: '(空目录)' }
|
||||
|
||||
const lines: string[] = []
|
||||
for (const entry of entries.slice(0, 500)) {
|
||||
if (entry.name === 'node_modules' || entry.name === '.git') continue
|
||||
const full = join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
lines.push(`[dir] ${entry.name}/`)
|
||||
} else {
|
||||
const stat = await fs.stat(full).catch(() => null)
|
||||
const size = stat ? `${stat.size}B` : '?'
|
||||
lines.push(`[file] ${entry.name} ${size}`)
|
||||
}
|
||||
}
|
||||
if (entries.length > 500) lines.push(`... 共 ${entries.length} 项,已截断`)
|
||||
return { ok: true, content: lines.join('\n') || '(无内容)' }
|
||||
}
|
||||
}
|
||||
|
||||
const readFile: Tool = {
|
||||
name: 'read_file',
|
||||
description:
|
||||
'读取工作目录内某个文本文件的内容。大文件请配合 offset/limit 分段读取。返回带行号的文本。',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: '相对于工作目录的文件路径' },
|
||||
description: { type: 'string', description: 'Optional short English action label for the conversation timeline, e.g. "Read file package.json". Keep it concise.' },
|
||||
offset: { type: 'number', description: '起始行号,从 1 开始,默认 1' },
|
||||
limit: { type: 'number', description: '读取行数,默认 400,最大 2000' }
|
||||
},
|
||||
required: ['path']
|
||||
},
|
||||
async run(args, ctx) {
|
||||
const file = await resolveInWorkspace(args.path, ctx)
|
||||
const stat = await fs.stat(file)
|
||||
if (stat.isDirectory()) throw new ToolError(`${args.path} 是目录,请用 list_dir`)
|
||||
if (stat.size > 4 * 1024 * 1024) throw new ToolError('文本文件超过 4 MB,请先缩小文件或使用其他读取方式')
|
||||
|
||||
const offset = Math.max(1, Number(args.offset) || 1)
|
||||
const limit = Math.min(2000, Math.max(1, Number(args.limit) || 400))
|
||||
|
||||
const text = await fs.readFile(file, 'utf8')
|
||||
const all = text.split(/\r?\n/)
|
||||
const slice = all.slice(offset - 1, offset - 1 + limit)
|
||||
const numbered = slice
|
||||
.map((line, i) => `${String(offset + i).padStart(5, ' ')}| ${line}`)
|
||||
.join('\n')
|
||||
|
||||
const header = `# ${args.path}(共 ${all.length} 行,显示 ${offset}-${offset + slice.length - 1})`
|
||||
return { ok: true, content: `${header}\n${numbered}`, data: { path: file, content: slice.join('\n'), totalLines: all.length, startLine: offset, endLine: offset + slice.length - 1 } }
|
||||
}
|
||||
}
|
||||
|
||||
async function authorizeWrite(ctx: ToolContext, description: string): Promise<void> {
|
||||
if (ctx.signal?.aborted) throw Object.assign(new Error('已取消'), { name: 'AbortError' })
|
||||
if (ctx.permissionMode !== 'full-access' && !(await ctx.approve?.(description))) throw new ToolError('用户未批准此操作')
|
||||
}
|
||||
|
||||
const writeFile: Tool = {
|
||||
name: 'write_file', description: '创建或更新文本文件;默认权限下会请求用户确认。',
|
||||
parameters: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] },
|
||||
async run(args, ctx) {
|
||||
if (typeof args.path !== 'string' || typeof args.content !== 'string') throw new ToolError('文件参数无效')
|
||||
let path = await resolveInWorkspace(args.path, ctx)
|
||||
const oldContent = await fs.readFile(path, 'utf8').catch((error) => {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null
|
||||
throw error
|
||||
})
|
||||
await authorizeWrite(ctx, `写入文件 ${args.path}`)
|
||||
path = await resolveInWorkspace(args.path, ctx)
|
||||
const current = await fs.readFile(path, 'utf8').catch((error) => {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null
|
||||
throw error
|
||||
})
|
||||
if (current !== oldContent) throw new ToolError('文件在等待审批时已变化,请重新读取后写入')
|
||||
await fs.mkdir(dirname(path), { recursive: true })
|
||||
path = await resolveInWorkspace(args.path, ctx)
|
||||
await writeTextAtomically(path, args.content)
|
||||
return { ok: true, content: `已写入 ${args.path}`, data: { path, oldContent, newContent: args.content } }
|
||||
}
|
||||
}
|
||||
const editFile: Tool = {
|
||||
name: 'edit_file', description: '精确替换文本文件中的唯一匹配内容;默认权限下会请求用户确认。',
|
||||
parameters: { type: 'object', properties: { path: { type: 'string' }, oldText: { type: 'string' }, newText: { type: 'string' } }, required: ['path', 'oldText', 'newText'] },
|
||||
async run(args, ctx) {
|
||||
if (typeof args.path !== 'string' || typeof args.oldText !== 'string' || !args.oldText || typeof args.newText !== 'string') throw new ToolError('编辑参数无效')
|
||||
let path = await resolveInWorkspace(args.path, ctx)
|
||||
const oldContent = await fs.readFile(path, 'utf8')
|
||||
if (oldContent.split(args.oldText).length !== 2) throw new ToolError('待替换内容必须唯一匹配')
|
||||
await authorizeWrite(ctx, `编辑文件 ${args.path}`)
|
||||
path = await resolveInWorkspace(args.path, ctx)
|
||||
if (await fs.readFile(path, 'utf8') !== oldContent) throw new ToolError('文件已发生变化,请重新读取后编辑')
|
||||
const newContent = oldContent.replace(args.oldText, args.newText)
|
||||
await writeTextAtomically(path, newContent)
|
||||
return { ok: true, content: `已编辑 ${args.path}`, data: { path, oldContent, newContent } }
|
||||
}
|
||||
}
|
||||
const command: Tool = {
|
||||
name: 'execute_command', description: '在工作目录执行 shell 命令;需要用户开启完全访问。',
|
||||
parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] },
|
||||
async run(args, ctx) {
|
||||
if (ctx.permissionMode !== 'full-access') throw new ToolError('当前系统沙盒不可用,命令执行需要完全访问权限')
|
||||
if (typeof args.command !== 'string' || !args.command.trim()) throw new ToolError('命令为空')
|
||||
return new Promise((resolveResult, reject) => {
|
||||
const child = spawn(args.command as string, { shell: true, cwd: ctx.workspaceRoot, windowsHide: true, detached: process.platform !== 'win32' })
|
||||
const onAbort = (): void => {
|
||||
if (!child.pid) return
|
||||
if (process.platform === 'win32') {
|
||||
const killer = spawn('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' })
|
||||
killer.once('error', () => child.kill())
|
||||
} else {
|
||||
try { process.kill(-child.pid, 'SIGTERM') } catch { child.kill() }
|
||||
}
|
||||
}
|
||||
ctx.signal?.addEventListener('abort', onAbort, { once: true })
|
||||
if (ctx.signal?.aborted) onAbort()
|
||||
let stdout = '', stderr = ''
|
||||
const retain = (current: string, chunk: Buffer): string => (current + chunk.toString()).slice(-2 * 1024 * 1024)
|
||||
child.stdout?.on('data', (chunk: Buffer) => { stdout = retain(stdout, chunk) })
|
||||
child.stderr?.on('data', (chunk: Buffer) => { stderr = retain(stderr, chunk) })
|
||||
child.once('error', (error) => { ctx.signal?.removeEventListener('abort', onAbort); reject(error) })
|
||||
child.once('close', (exitCode) => { ctx.signal?.removeEventListener('abort', onAbort); resolveResult({ ok: exitCode === 0, content: [stdout, stderr].filter(Boolean).join('\n'), data: { stdout, stderr, exitCode } }) })
|
||||
})
|
||||
}
|
||||
}
|
||||
const askQuestion: Tool = { name: 'ask_user_question', description: '暂停并向用户提出选项或自由文本问题。', parameters: { type: 'object', properties: { title: { type: 'string' }, questions: { type: 'array', items: { type: 'object', properties: { id: { type: 'string' }, question: { type: 'string' }, options: { type: 'array', items: { type: 'string' } }, multiSelect: { type: 'boolean' } }, required: ['question'] } } }, required: ['questions'] }, async run() { return { ok: false, content: '问答宿主未连接' } } }
|
||||
const followupQuestion: Tool = { ...askQuestion, name: 'ask_followup_question', parameters: { type: 'object', properties: { question: { type: 'string' }, follow_up: { type: 'array', items: { type: 'string' } } }, required: ['question'] } }
|
||||
const plan: Tool = { name: 'todo_write', description: '创建或更新任务计划和步骤进度。', parameters: { type: 'object', properties: { title: { type: 'string' }, todos: { type: 'array', items: { type: 'object', properties: { id: { type: 'string' }, content: { type: 'string' }, status: { type: 'string', enum: ['pending', 'in_progress', 'completed', 'cancelled'] } }, required: ['content'] } } }, required: ['todos'] }, async run() { return { ok: false, content: '计划宿主未连接' } } }
|
||||
|
||||
export const ALL_TOOLS: Tool[] = [listDir, readFile, writeFile, editFile, command, askQuestion, followupQuestion, plan]
|
||||
|
||||
export function toolSpecs(): ToolSpec[] {
|
||||
return ALL_TOOLS.map((t) => ({
|
||||
type: 'function' as const,
|
||||
function: { name: t.name, description: t.description, parameters: t.parameters }
|
||||
}))
|
||||
}
|
||||
|
||||
export function findTool(name: string): Tool | undefined {
|
||||
return ALL_TOOLS.find((t) => t.name === name)
|
||||
}
|
||||
|
||||
/** 统一执行入口:把异常收敛成 ToolResult,避免中断整个回合 */
|
||||
export async function executeTool(
|
||||
name: string,
|
||||
args: Record<string, unknown>,
|
||||
ctx: ToolContext
|
||||
): Promise<ToolResult> {
|
||||
const tool = findTool(name)
|
||||
if (!tool) return { ok: false, content: `未知工具:${name}` }
|
||||
try {
|
||||
return await tool.run(args, ctx)
|
||||
} catch (err) {
|
||||
return { ok: false, content: `工具执行失败:${(err as Error).message}` }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { app } from 'electron'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { basename, extname, join, relative, resolve, sep } from 'node:path'
|
||||
import type { ChatAttachment } from '@shared/conversation'
|
||||
|
||||
const MAX_BYTES = 20 * 1024 * 1024
|
||||
const MIME: Record<string, string> = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml', '.pdf': 'application/pdf', '.txt': 'text/plain', '.md': 'text/markdown', '.json': 'application/json', '.csv': 'text/csv' }
|
||||
function root(): string { return join(app.getPath('userData'), 'attachments') }
|
||||
function validId(id: string): boolean { return /^[0-9a-f-]{36}$/.test(id) }
|
||||
const committedIds = new Set<string>()
|
||||
|
||||
export async function stageAttachment(name: string, mimeType: string, bytes: Buffer): Promise<ChatAttachment> {
|
||||
if (!bytes.length || bytes.length > MAX_BYTES) throw new Error('附件为空或超过 20 MB')
|
||||
const id = randomUUID()
|
||||
const safeName = basename(name.replace(/\\/g, '/')).replace(/[<>:"|?*\x00-\x1f]/g, '_') || 'attachment'
|
||||
if (['.', '..', 'metadata.json'].includes(safeName.toLowerCase())) throw new Error('附件文件名无效')
|
||||
const mime = MIME[extname(safeName).toLowerCase()] ?? mimeType ?? 'application/octet-stream'
|
||||
const directory = join(root(), id)
|
||||
await fs.mkdir(directory, { recursive: true })
|
||||
const path = join(directory, safeName)
|
||||
const attachment: ChatAttachment = { id, name: safeName, mimeType: mime, size: bytes.length, path,
|
||||
previewUrl: mime.startsWith('image/') ? `data:${mime};base64,${bytes.toString('base64')}` : undefined }
|
||||
try {
|
||||
await fs.writeFile(path, bytes)
|
||||
await fs.writeFile(join(directory, 'metadata.json'), JSON.stringify(attachment))
|
||||
} catch (error) { await removeAttachment(id); throw error }
|
||||
return attachment
|
||||
}
|
||||
export async function stageAttachmentPath(path: string): Promise<ChatAttachment> {
|
||||
const handle = await fs.open(path, 'r')
|
||||
try {
|
||||
const stat = await handle.stat()
|
||||
if (!stat.isFile() || stat.size > MAX_BYTES) throw new Error('不支持该附件或超过 20 MB')
|
||||
const bytes = Buffer.alloc(stat.size + 1)
|
||||
let length = 0
|
||||
while (length < bytes.length) {
|
||||
const result = await handle.read(bytes, length, bytes.length - length, null)
|
||||
if (!result.bytesRead) break
|
||||
length += result.bytesRead
|
||||
}
|
||||
if (length > MAX_BYTES) throw new Error('附件超过 20 MB')
|
||||
return stageAttachment(basename(path), '', bytes.subarray(0, length))
|
||||
} finally { await handle.close() }
|
||||
}
|
||||
export async function getAttachment(id: string): Promise<ChatAttachment> {
|
||||
if (!validId(id)) throw new Error('附件标识无效')
|
||||
const attachment = JSON.parse(await fs.readFile(join(root(), id, 'metadata.json'), 'utf8')) as ChatAttachment
|
||||
if (typeof attachment.name !== 'string' || basename(attachment.name) !== attachment.name || attachment.name === 'metadata.json') throw new Error('附件元数据无效')
|
||||
const path = join(root(), id, attachment.name)
|
||||
return { ...attachment, id, path }
|
||||
}
|
||||
export async function commitAttachments(attachments: ChatAttachment[]): Promise<void> {
|
||||
for (const attachment of attachments) committedIds.add(attachment.id)
|
||||
for (const attachment of attachments) {
|
||||
const stored = await getAttachment(attachment.id)
|
||||
await fs.writeFile(join(root(), attachment.id, 'metadata.json'), JSON.stringify({ ...stored, committed: true }))
|
||||
}
|
||||
}
|
||||
export async function removeAttachment(id: string): Promise<void> {
|
||||
if (!validId(id)) throw new Error('附件标识无效')
|
||||
if (committedIds.has(id)) throw new Error('附件已用于会话,不能从草稿中删除原文件')
|
||||
const metadata = await fs.readFile(join(root(), id, 'metadata.json'), 'utf8').catch((error) => {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return '{}'
|
||||
throw error
|
||||
})
|
||||
if ((JSON.parse(metadata) as { committed?: boolean }).committed) throw new Error('附件已用于会话,不能从草稿中删除原文件')
|
||||
const target = resolve(root(), id)
|
||||
const rel = relative(resolve(root()), target)
|
||||
if (!rel || rel.startsWith(`..${sep}`) || rel === '..' || rel.includes(sep)) throw new Error('附件目录无效')
|
||||
await fs.rm(target, { recursive: true, force: true })
|
||||
}
|
||||
export async function attachmentPrompt(attachments: ChatAttachment[]): Promise<{ text: string; images: Array<{ type: 'image'; data: string; mimeType: string }> }> {
|
||||
const text: string[] = []
|
||||
const images: Array<{ type: 'image'; data: string; mimeType: string }> = []
|
||||
for (const reference of attachments) {
|
||||
const attachment = await getAttachment(reference.id)
|
||||
const bytes = await fs.readFile(attachment.path!)
|
||||
if (attachment.mimeType.startsWith('image/') && attachment.mimeType !== 'image/svg+xml') images.push({ type: 'image', data: bytes.toString('base64'), mimeType: attachment.mimeType })
|
||||
else if (!bytes.includes(0) && bytes.length < 2 * 1024 * 1024) text.push(`附件 ${attachment.name}:\n${bytes.toString('utf8')}`)
|
||||
else text.push(`附件 ${attachment.name}(${attachment.mimeType},${attachment.size} 字节),本地路径:${attachment.path}`)
|
||||
}
|
||||
return { text: text.join('\n\n'), images }
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { WebContentsView, type BrowserWindow } from 'electron'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { clampBrowserHostBounds, normalizeBrowserHostUrl, type BrowserHostBounds, type BrowserHostView } from '../shared/browser-host'
|
||||
|
||||
interface BrowserRecord { id: string; owner: BrowserWindow; sessionId?: string; view: WebContentsView; visible: boolean; error?: string; navigationVersion: number }
|
||||
const browsers = new Map<string, BrowserRecord>()
|
||||
const owners = new Set<number>()
|
||||
function requireBrowser(owner: BrowserWindow, id: string, sessionId?: string): BrowserRecord {
|
||||
const browser = browsers.get(id)
|
||||
if (!browser || browser.owner !== owner || browser.sessionId !== sessionId || owner.isDestroyed() || browser.view.webContents.isDestroyed()) throw new Error('浏览器标签不存在或不属于当前任务')
|
||||
return browser
|
||||
}
|
||||
function snapshot(browser: BrowserRecord): BrowserHostView {
|
||||
const contents = browser.view.webContents
|
||||
return { id: browser.id, sessionId: browser.sessionId, url: contents.getURL() || 'about:blank',
|
||||
title: contents.getTitle() || '新标签页', loading: contents.isLoading(), canGoBack: contents.navigationHistory.canGoBack(),
|
||||
canGoForward: contents.navigationHistory.canGoForward(), visible: browser.visible, error: browser.error }
|
||||
}
|
||||
function release(browser: BrowserRecord): void {
|
||||
browsers.delete(browser.id)
|
||||
browser.visible = false
|
||||
if (!browser.owner.isDestroyed()) browser.owner.contentView.removeChildView(browser.view)
|
||||
if (!browser.view.webContents.isDestroyed()) browser.view.webContents.close({ waitForBeforeUnload: false })
|
||||
}
|
||||
function guardNavigation(browser: BrowserRecord, event: { preventDefault(): void }, url: string): void {
|
||||
try { normalizeBrowserHostUrl(url) } catch { event.preventDefault(); browser.error = '仅支持 HTTP 或 HTTPS 网页地址' }
|
||||
}
|
||||
function aborted(error: unknown): boolean { return !!error && typeof error === 'object' && 'code' in error && error.code === 'ERR_ABORTED' }
|
||||
async function load(browser: BrowserRecord, url: string): Promise<void> {
|
||||
const version = ++browser.navigationVersion
|
||||
browser.error = undefined
|
||||
try { await browser.view.webContents.loadURL(url) }
|
||||
catch (error) { if (version === browser.navigationVersion && !browser.view.webContents.isDestroyed() && !aborted(error)) browser.error = error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
export function createBrowser(owner: BrowserWindow, sessionId?: string, initialUrl = 'about:blank'): BrowserHostView {
|
||||
if (owner.isDestroyed()) throw new Error('宿主窗口已关闭')
|
||||
if (sessionId !== undefined && (typeof sessionId !== 'string' || sessionId.length > 256 || !sessionId)) throw new Error('任务标识无效')
|
||||
const url = normalizeBrowserHostUrl(initialUrl)
|
||||
const id = randomUUID()
|
||||
const view = new WebContentsView({ webPreferences: { sandbox: true, contextIsolation: true, nodeIntegration: false,
|
||||
webSecurity: true, allowRunningInsecureContent: false, partition: `ycode-browser-${id}` } })
|
||||
const browser: BrowserRecord = { id, owner, sessionId, view, visible: false, navigationVersion: 0 }
|
||||
browsers.set(id, browser)
|
||||
const contents = view.webContents
|
||||
contents.session.setPermissionRequestHandler((_contents, _permission, callback) => callback(false))
|
||||
contents.session.setPermissionCheckHandler(() => false)
|
||||
contents.session.on('will-download', event => event.preventDefault())
|
||||
contents.on('will-navigate', (event, nextUrl) => guardNavigation(browser, event, nextUrl))
|
||||
contents.on('will-redirect', (event, nextUrl) => guardNavigation(browser, event, nextUrl))
|
||||
// No unmanaged popup escapes the owner/session. Exact original popup routing is unverified.
|
||||
contents.setWindowOpenHandler(({ url: nextUrl }) => {
|
||||
try { void load(browser, normalizeBrowserHostUrl(nextUrl)) }
|
||||
catch { browser.error = '仅支持 HTTP 或 HTTPS 网页地址' }
|
||||
return { action: 'deny' }
|
||||
})
|
||||
contents.on('did-start-navigation', (_event, _nextUrl, _isInPlace, isMainFrame) => { if (isMainFrame) browser.error = undefined })
|
||||
contents.on('did-fail-load', (_event, code, description, _url, isMainFrame) => { if (isMainFrame && code !== -3) browser.error = description })
|
||||
contents.on('render-process-gone', (_event, details) => { browser.error = `浏览器进程已结束:${details.reason}` })
|
||||
contents.on('destroyed', () => browsers.delete(id))
|
||||
owner.contentView.addChildView(view)
|
||||
view.setVisible(false)
|
||||
if (!owners.has(owner.id)) {
|
||||
owners.add(owner.id)
|
||||
owner.once('closed', () => { owners.delete(owner.id); for (const record of [...browsers.values()]) if (record.owner === owner) release(record) })
|
||||
owner.on('resize', () => { if (!owner.isDestroyed()) for (const record of browsers.values()) if (record.owner === owner && record.visible) {
|
||||
const [width, height] = owner.getContentSize(); const bounds = clampBrowserHostBounds(record.view.getBounds(), width, height)
|
||||
record.view.setBounds(bounds)
|
||||
} })
|
||||
}
|
||||
if (url !== 'about:blank') void load(browser, url)
|
||||
return snapshot(browser)
|
||||
}
|
||||
export function getBrowserState(owner: BrowserWindow, id: string, sessionId?: string): BrowserHostView { return snapshot(requireBrowser(owner, id, sessionId)) }
|
||||
/** The IPC caller validates the destination YCode session before transferring a draft tab. */
|
||||
export function rebindBrowser(owner: BrowserWindow, id: string, previousSessionId: string | undefined, nextSessionId: string): BrowserHostView {
|
||||
const browser = requireBrowser(owner, id, previousSessionId)
|
||||
if (typeof nextSessionId !== 'string' || !nextSessionId || nextSessionId.length > 256) throw new Error('目标任务标识无效')
|
||||
browser.sessionId = nextSessionId
|
||||
return snapshot(browser)
|
||||
}
|
||||
export async function navigateBrowser(owner: BrowserWindow, id: string, url: string, sessionId?: string): Promise<BrowserHostView> {
|
||||
const browser = requireBrowser(owner, id, sessionId)
|
||||
const target = normalizeBrowserHostUrl(url)
|
||||
await load(browser, target)
|
||||
return snapshot(browser)
|
||||
}
|
||||
export function backBrowser(owner: BrowserWindow, id: string, sessionId?: string): BrowserHostView {
|
||||
const browser = requireBrowser(owner, id, sessionId)
|
||||
if (browser.view.webContents.navigationHistory.canGoBack()) browser.view.webContents.navigationHistory.goBack()
|
||||
return snapshot(browser)
|
||||
}
|
||||
export function forwardBrowser(owner: BrowserWindow, id: string, sessionId?: string): BrowserHostView {
|
||||
const browser = requireBrowser(owner, id, sessionId)
|
||||
if (browser.view.webContents.navigationHistory.canGoForward()) browser.view.webContents.navigationHistory.goForward()
|
||||
return snapshot(browser)
|
||||
}
|
||||
export function reloadBrowser(owner: BrowserWindow, id: string, sessionId?: string): BrowserHostView {
|
||||
const browser = requireBrowser(owner, id, sessionId)
|
||||
browser.error = undefined; browser.view.webContents.reload()
|
||||
return snapshot(browser)
|
||||
}
|
||||
export function setBrowserBounds(owner: BrowserWindow, id: string, bounds: BrowserHostBounds | null, sessionId?: string): void {
|
||||
// Component teardown can finish after the tab has already released its native view.
|
||||
if (bounds === null && !browsers.has(id)) return
|
||||
const browser = requireBrowser(owner, id, sessionId)
|
||||
if (bounds === null) { browser.view.setVisible(false); browser.visible = false; return }
|
||||
const [width, height] = owner.getContentSize()
|
||||
const clamped = clampBrowserHostBounds(bounds, width, height)
|
||||
browser.view.setBounds(clamped)
|
||||
browser.visible = clamped.width > 0 && clamped.height > 0
|
||||
browser.view.setVisible(browser.visible)
|
||||
}
|
||||
export function hideBrowser(owner: BrowserWindow, id: string, sessionId?: string): void { setBrowserBounds(owner, id, null, sessionId) }
|
||||
export function closeBrowser(owner: BrowserWindow, id: string, sessionId?: string): void {
|
||||
if (!browsers.has(id)) return
|
||||
release(requireBrowser(owner, id, sessionId))
|
||||
}
|
||||
export function disposeOwnerBrowsers(owner: BrowserWindow): void { for (const browser of [...browsers.values()]) if (browser.owner === owner) release(browser) }
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"files": {
|
||||
".codebuddy-plugin/plugin.json": "{\n \"name\": \"skill-expert-manager\",\n \"version\": \"5.8.2-wb.41111488.gb8f6c953.haebd5991e0e2\",\n \"description\": \"WorkBuddy bundled skill resource: expert-manager.\",\n \"author\": {\n \"name\": \"WorkBuddy\"\n },\n \"category\": \"skill\",\n \"keywords\": [\n \"workbuddy\",\n \"builtin-skill\",\n \"expert-manager\"\n ],\n \"workbuddy\": {\n \"kind\": \"builtin-skill\",\n \"legacyResourceName\": \"expert-manager\",\n \"bundleSegments\": [\n \"plugins\",\n \"workbuddy-builtin\",\n \"skills\",\n \"expert-manager\"\n ]\n }\n}\n",
|
||||
"SKILL.md": "---\nname: expert-manager\ndescription: |\n 专家包的全生命周期运营:转化(从开源仓库/本地项目创建专家包)、修改已有专家、合规检查、批量更新、质量审查。\n 触发词:创建专家、转化专家、转成专家、生成专家包、导入专家、convert expert、修改专家、编辑专家、更新专家、modify expert、检查专家、审查专家包、专家合规、专家运营、expert ops。\ndescription_en: |\n Manage the full lifecycle of WorkBuddy Expert packages: create or convert an Expert from an open-source repository or local project, edit existing Experts, run compliance checks, apply batch updates, and review quality. Use for requests to create, import, modify, inspect, or maintain an Expert or Expert Team package.\n---\n\n# WorkBuddy 专家包管理器\n\n> ⚠️ **执行前必读**:当需要使用本 skill 时,你必须先从头到尾完整阅读本 SKILL.md 全文并严格遵守(包括所有规则、流程、References 列表),然后再开始执行任务。禁止跳读或仅凭部分段落就开始行动。\n\n你是 WorkBuddy 专家包管理器,帮助用户按照 WorkBuddy 专家开发规范(v2.0)创建和维护完整的、可提交审核的专家文件包。\n\n支持两种专家类型:\n- **Agent 型**(`expertType: \"agent\"`):单个 AI 专家\n- **Team 型**(`expertType: \"team\"`):多角色协作团队\n\n支持两种输入模式:\n1. **交互模式**:用户描述需求,通过提问收集信息后生成\n2. **资料转化模式**:用户提供现有资料(文档、流程、提示词等),从中提取信息直接转化\n\n---\n\n## 关键展示字段对应关系\n\n以下字段决定了专家在产物面板中的展示,**所有场景(创建/转化/修改)均需遵守**:\n\n| 展示 | 对应字段 | 变更规则 |\n|---------|---------|---------|\n| **名字(职业)** | `profession`(`{en, zh}`) | 卡片标题,应体现专家职业定位 |\n| **花名** | `displayName`(`{en, zh}`) | 可根据用户要求自由修改 |\n| **类型** | `expertType`(`\"agent\"` → 显示\"专家\",`\"team\"` → 显示\"专家团\") | **不可随意指定/变更**,必须根据实际结构判断:单角色 = `\"agent\"`,多角色协作 = `\"team\"` |\n| **行业分类** | `categoryId` | **不可随意指定/变更**,必须根据专家核心能力领域从分类列表中选择,并向用户说明选择理由 |\n| **能力介绍** | `displayDescription`(`{en, zh}`) | 中文 40-50 字,突出核心能力 |\n| **擅长领域** | `tags`(`{en, zh}[]`,固定 3 个) | 展示专家擅长的领域标签;已满 3 个时必须提示用户替换或删除已有标签,禁止继续新增 |\n| **试试这样问我** | `quickPrompts`(`{en, zh}[]`,固定 3 个) | 推荐提示词,第一条同时作为 `defaultInitPrompt` |\n\n---\n\n## 一、工作流程\n\n### 整体流程\n\n```\n1. 收集信息(交互 or 资料转化)\n2. 初始化目录 → scripts/init_expert.py\n3. 生成文件内容 → 参考 references/\n4. 生成头像 → 参考 references/avatar-spec.md\n5. 校验 → scripts/validate_expert.py\n6. 注册 → scripts/register_expert.py\n7. 打包 → scripts/package_expert.py\n```\n\n> **批量创建**:多个专家时串行重复上述流程,参考 `scripts/batch_create.py`。\n\n### 场景 A:交互模式\n\n**专家目录(固定)**:由环境变量 `WORKBUDDY_CONFIG_DIR` 决定,完整路径为 `$WORKBUDDY_CONFIG_DIR/plugins/marketplaces/my-experts/plugins`(未设置时默认 `~/.workbuddy/plugins/marketplaces/my-experts/plugins`)。**禁止**将专家生成到其他目录。如果用户要求创建到其他路径,必须拒绝并说明:\"专家必须生成到专家目录才能被检测到,其他目录生成后将无法使用。\"然后使用专家目录继续执行。\n\n**必须明确的信息:**\n1. **专家类型(expertType)**:Agent 还是 Team?(判定规则见上方\"关键展示字段对应关系\")\n2. **专家领域**:擅长什么?\n\n**Agent 型还需要:**\n- 名字(中英文)、职业头衔(中英文)\n- 详细能力描述(中英文,中文 40-50 字)\n- 首次对话问候语(中英文)\n- 行业分类(见下方列表)\n- 擅长领域标签(固定 3 个,中英文)\n- 推荐提示词(3 个,中英文,第一条同时作为 defaultInitPrompt)\n- 是否需要附带 Skill / bin/\n\n**Team 型还需要:**\n- 团队名称(中英文)、团队职业头衔(中英文,须与团队名称一致)\n- 主理人名字、职责\n- 每个团员的名字、职业头衔、职责\n- 团队 SOP 工作流程\n- 首次对话问候语、行业分类、标签、推荐提示词\n\n### 场景 B:资料转化模式\n\n当用户提供文件路径或粘贴内容时:\n\n1. **读取分析** — 从资料中提取角色定义、核心能力、SOP、输出规范、约束、脚本Line truncated
|
||||
"references/plugin-json-spec.md": "# plugin.json 字段规范\n\n## 基础字段(必填)\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `name` | string | 唯一标识,小写字母+连字符,也是技能命名空间前缀 |\n| `version` | string | 语义化版本号(MAJOR.MINOR.PATCH) |\n| `description` | string | 英文一句话描述 |\n\n## 可选基础字段\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `author` | `{name, email}` | 作者信息 |\n| `homepage` | string 或 `{url, type}` | 项目主页 |\n| `license` | string | 许可证 |\n| `keywords` | string[] | 搜索标签 |\n\n## 类型字段\n\n| 字段 | 说明 |\n|------|------|\n| `expertType` | `\"agent\"` / `\"team\"` |\n| `agentName` | 主 Agent 名称(对应 agents/ 下 MD 文件名,不含 .md)。**必须有业务语义**,不能使用 `team-lead` 等通用名 |\n| `teamInfo` | team 时必填:`{leadAgent, memberAgents[]}` |\n\n## 资源声明\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `agents` | string[] | Agent 定义文件路径列表(如 `[\"./agents/my-expert.md\"]`) |\n| `skills` | string[] | Skill 目录路径列表(如 `[\"./skills/my-skill\"]`) |\n\n## 展示字段(agent/team 上架市场时必填)\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `displayName` | `{en, zh}` | 展示名称 |\n| `profession` | `{en, zh}` | 职业/定位。**Team 型须与 displayName 一致** |\n| `displayDescription` | `{en, zh}` | 展示描述。**中文 40-50 字,突出核心能力** |\n| `avatar` | string | 头像相对路径 |\n| `categoryId` | string | 行业分类 ID |\n| `defaultInitPrompt` | `{en, zh}` | 默认引导语。**须与 quickPrompts 第一条一致** |\n| `tags` | `{en, zh}[]` | 擅长领域标签(**固定 3 个**) |\n| `quickPrompts` | `{en, zh}[]` | 推荐提示词(**固定 3 个**) |\n| `plugin` | string | 值与 `name` 一致 |\n\n## Team 专用字段\n\n| 字段 | 说明 |\n|------|------|\n| `members[]` | 每个成员含 `{id, name:{en,zh}, profession:{en,zh}, avatar, role}` |\n\n- `role` 取值:`\"lead\"` 或 `\"member\"`\n- 主理人也必须在 members 中,role 为 `\"lead\"`\n- `teamInfo.memberAgents` **不含**主理人\n\n---\n\n## 模板:Agent 型\n\n```json\n{\n \"name\": \"{kebab-case-name}\",\n \"version\": \"1.0.0\",\n \"description\": \"{English one-line description}\",\n \"author\": {\n \"name\": \"{author-name}\",\n \"email\": \"{author-email}\"\n },\n \"agents\": [\"./agents/{agent-name}.md\"],\n \"skills\": [\"./skills/{skill-name}\"],\n\n \"expertType\": \"agent\",\n \"agentName\": \"{agent-name}\",\n\n \"displayName\": {\n \"en\": \"{English display name}\",\n \"zh\": \"{中文显示名称}\"\n },\n \"profession\": {\n \"en\": \"{English profession}\",\n \"zh\": \"{中文职业头衔}\"\n },\n \"displayDescription\": {\n \"en\": \"{English detailed description}\",\n \"zh\": \"{中文详细描述,40-50字}\"\n },\n \"avatar\": \"avatars/expert.png\",\n \"categoryId\": \"{XX-CategoryName}\",\n \"defaultInitPrompt\": {\n \"zh\": \"{中文首次对话提示}\",\n \"en\": \"{English first prompt}\"\n },\n \"plugin\": \"{kebab-case-name}\",\n \"tags\": [\n { \"en\": \"{Tag1 EN}\", \"zh\": \"{标签1}\" },\n { \"en\": \"{Tag2 EN}\", \"zh\": \"{标签2}\" },\n { \"en\": \"{Tag3 EN}\", \"zh\": \"{标签3}\" }\n ],\n \"quickPrompts\": [\n { \"en\": \"{Prompt1 EN}\", \"zh\": \"{提示词1}\" },\n { \"en\": \"{Prompt2 EN}\", \"zh\": \"{提示词2}\" },\n { \"en\": \"{Prompt3 EN}\", \"zh\": \"{提示词3}\" }\n ]\n}\n```\n\n> 如果没有 skills,则省略 `\"skills\"` 字段。\n\n## 模板:Team 型\n\n```json\n{\n \"name\": \"{kebab-case-name}\",\n \"version\": \"1.0.0\",\n \"description\": \"{English one-line description}\",\n \"author\": {\n \"name\": \"{author-name}\",\n \"email\": \"{author-email}\"\n },\n \"agents\": [\n \"./agents/{team}-team-lead.md\",\n \"./agents/{member-a}.md\",\n \"./agents/{member-b}.md\"\n ],\n \"skills\": [\"./skills/{skill-name}\"],\n\n \"expertType\": \"team\",\n \"agentName\": \"{team}-team-lead\",\n \"teamInfo\": {\n \"leadAgent\": \"{team}-team-lead\",\n \"memberAgents\": [\"{member-a}\", \"{member-b}\"]\n },\n\n \"displayName\": {\n \"en\": \"{English team name}\",\n \"zh\": \"{中文团队名称}\"\n },\n \"profession\": {\n \"en\": \"{English team name}\",\n \"zh\": \"{中文团队名称}\"\n },\n \"displayDescription\": {\n \"en\": \"{English team description}\",\n \"zh\": \"{中文团队描述,40-50字}\"\n },\n \"avatar\": \"avatars/team.png\",\n \"categoryId\": \"{XX-CategoryName}\",\n \"defaultInitPrompt\": {\n \"zh\": \"{中文首次问候}\",\n \"en\": \"{English first prompt}\"\n },\n \"plugin\": \"{kebab-case-name}\",\n \"tags\": [\n { \"en\": \"{Tag1 EN}\", \"zh\": \"{标签1}\" },\n { \"en\": \"{Tag2 EN}\", \"zh\": \Line truncated
|
||||
"references/agent-md-spec.md": "# Agent MD 规范\n\n## Frontmatter\n\n### ⚠️ 重要:frontmatter 中不可添加 tools 字段\n\n所有工具权限由系统统一分配,开发者无需也不能在 frontmatter 中声明 `tools` 字段。\n\n### 必填字段\n\n```yaml\n---\nname: {与文件名一致,必须有业务语义}\ndescription: {英文描述,AI 用来判断何时激活}\ndisplayName:\n en: \"{English display name}\"\n zh: \"{中文显示名称}\"\nprofession:\n en: \"{English profession title}\"\n zh: \"{中文职业头衔}\"\nmaxTurns: {默认50}\n---\n```\n\n### 可选字段\n\n```yaml\nskills: [{skill-name}] # 启动时预加载的 Skill\n```\n\n---\n\n## 正文结构:普通 Agent / 团员\n\n```markdown\n# {角色名称} - {人名}\n\n{角色描述,说明这是谁,擅长什么}\n\n## 核心能力\n1. **{能力1}**:{描述}\n2. **{能力2}**:{描述}\n3. **{能力3}**:{描述}\n\n## 工作流程\n1. {步骤1}\n2. {步骤2}\n3. {步骤3}\n\n## 输出规范\n- {规范1}\n- {规范2}\n- {规范3}\n\n## 注意事项\n- {约束或边界条件}\n```\n\n### 团队成员额外要求\n\n- 不要在 frontmatter 中限制 tools 范围\n- 由主理人通过 Agent 工具 spawn 为正式 teammate\n- 分析完成后必须通过 SendMessage 将结果回传给主理人\n- Prompt 中须明确写出 SendMessage 回传要求\n\n### 成员 Prompt 必备结构\n\n1. **角色定义**:一句话说清\"你是谁\"\n2. **擅长领域**:3-5 个具体能力点\n3. **分析框架**:内嵌的分析能力,分步骤流程\n4. **数据获取方式**:具体的查询命令或工具调用\n5. **结构化输出模板**:表格/分段格式\n6. **SendMessage 回传要求**\n\n---\n\n## 正文结构:主理人\n\n> ⚠️ 文件名不可使用通用的 `team-lead.md`,必须加上专家团前缀,如 `trading-team-lead.md`\n\n### Frontmatter\n\n```yaml\n---\nname: {team}-team-lead\ndescription: {English description}\ndisplayName:\n en: \"{English display name}\"\n zh: \"{中文显示名称}\"\nprofession:\n en: \"{English profession title}\"\n zh: \"{中文职业头衔}\"\nmaxTurns: {turns, 建议150-200}\n---\n```\n\n### 正文模板\n\n```markdown\n# {团队名称} - 主理人\n\n{主理人角色描述,负责协调团队完成什么任务}\n\n## 团队成员\n\n### {分组名称}\n| 成员 | 名字 | 职责 |\n|------|------|------|\n| {member-id} | {name} | {职责描述} |\n\n## 标准工作流程(SOP)\n\n### Phase 1: {阶段名}\n{调用哪些成员、输入输出说明}\n\n### Phase 2: {阶段名}\n{...}\n\n### Phase N: 最终报告\n综合所有分析结果,生成最终报告返回用户。\n\n## 团队协作机制(铁律)\n\n你必须走正式的**团队协作流程**,严禁简化或跳过:\n\n1. **建立团队**:任务开始时由主理人亲自创建团队(TeamCreate),明确协作边界。**团队创建必须且只能由主理人执行,严禁委派任何成员创建团队**\n2. **调度成员**:按 SOP 阶段将成员拉入协作、下发独立任务;成员作为独立协作方输出专业产出,不得由主理人代写\n3. **消息中转**:成员产出回传给主理人,由主理人汇总、转交下一阶段;所有跨成员信息流必须经主理人中转,不得互相直连\n4. **成员结论为准**:任何专业产出必须由对应成员输出后再采信,主理人只做编排与汇编\n\n### 严禁行为\n- ❌ 禁止跳过 TeamCreate,直接自己模拟成员发言或并行写出多角色内容\n- ❌ 禁止自己代写任何团队成员的专业产出\n- ❌ 禁止未完成前序阶段就跳到后续阶段\n- ❌ 禁止让成员互相直连通信,所有跨成员信息流必须经主理人中转\n- ❌ 禁止 spawn 主理人自己\n\n## 协作规则\n1. 所有成员调度必须经过\"建立团队 → 调度成员 → 成员回传\"流程\n2. 每阶段结束后,将完整产出原文传递给下一阶段成员\n3. 每完成一个阶段向用户简要通报\n4. 所有输出使用与用户原始需求相同的语言\n5. 调度成员时,Agent 工具的 `name` 参数传入成员的 **Agent ID**(MD 文件名,不含 .md),`subagent_type` 也传入相同值。禁止使用中文名或自创名称\n```\n\n---\n\n## 主理人额外要求\n\n### 成员能力清单\n\n主理人 prompt 中必须列出每个成员的:\n- Agent ID\n- 擅长领域(3-5 个具体能力点)\n- 典型问法(什么问题该调它)\n\n### 预设 Workflow\n\n针对高频综合性问题设计预设 Workflow,每个 Workflow 写明:\n- **触发条件**:什么类型的问法匹配\n- **Phase 编排**:分 Phase 的串并行调度\n- **输入输出依赖**:每个 Phase 的输入来自哪里、输出传给谁\n\n### 单 agent 直调路由表\n\n| 问法类型 | 直接调谁 |\n|---------|---------|\n| 单一维度问题 | 对应成员 |\n| 综合性问题 | 走预设 Workflow |\n\n---\n\n## 能独立成 agent 的标准\n\n判断标准:Line truncated
|
||||
"references/team-spec.md": "# Team 型专用规范\n\n## 成员命名规范\n\nTeam 型专家团中,每个成员有三个外露字段:`name`(名字)、`profession`(职业头衔)、`nickname`(昵称,可选)。三者不应重复。\n\n### name:谐音花名风格\n\n| 规则 | 说明 |\n|------|------|\n| 建议三个字 | 姓+名,建议三个字,两个字也可 |\n| 先是正常人名 | 姓+名结构,不解释也能当名字用 |\n| 暗含职能巧思 | 谐音、拆字、典故均可,但不刻意 |\n| 不与 profession 重复 | name 是\"谁\",profession 是\"做什么\" |\n| 中英文都自然 | 中文用花名,英文用拼音姓氏 |\n\n**示例(软件开发团队):**\n\n| profession | name(zh) | 巧思 | name(en) |\n|-----------|----------|------|----------|\n| 交付总监 | 齐活林 | 齐活了(交付完成) | Qi |\n| 产品经理 | 许清楚 | 需求要说清楚 | Xu |\n| 架构师 | 高见远 | 高见(架构视野) | Gao |\n| 项目经理 | 毕达成 | 必达(交付必达) | Bi |\n| 工程师 | 寇豆码 | code(代码) | Kou |\n| QA工程师 | 严过关 | 严格过关 | Yan |\n\n**禁止:**\n- 叠字谐音(领码码、需求求、蓝图图)\n- 一个字的 name 或纯职能词\n- 和 profession 重复\n- 无意义随机名(张三、John Doe)\n- 英文 name 用 Agent ID\n- 读起来是短语而非人名\n\n### 主理人 profession 规范\n\n主理人的 `profession` 不能用通用 title(团长、主理人、Team Lead),应体现该团队的调度风格和业务定位。\n\n**示例:**\n\n| 专家团 | 主理人 name | profession(zh) | profession(en) |\n|--------|-----------|---------------|----------------|\n| 软件开发团队 | 齐活林 | 交付总监 | Delivery Director |\n| 交易分析团队 | 何执舟 | 首席策略官 | Chief Strategist |\n| 营销战役团队 | 营销总监 | 增长操盘手 | Growth Operator |\n| 深度研究团队 | 顾全之 | 研究主编 | Research Editor |\n\n---\n\n## 协作铁律\n\n### 4 条正则\n\n1. **建立团队**:任务开始时由主理人亲自创建团队(TeamCreate),明确协作边界。**团队创建必须且只能由主理人执行,严禁委派任何成员创建团队**\n2. **调度成员**:按 SOP 阶段将成员拉入协作、下发独立任务;成员作为独立协作方输出专业产出,不得由主理人代写\n3. **消息中转**:成员产出回传给主理人,由主理人汇总、转交下一阶段;所有跨成员信息流必须经主理人中转,不得互相直连\n4. **成员结论为准**:任何专业产出必须由对应成员输出后再采信,主理人只做编排与汇编\n\n### 5 条红线\n\n- ❌ 禁止跳过 TeamCreate,直接自己模拟成员发言或并行写出多角色内容\n- ❌ 禁止自己代写任何团队成员的专业产出\n- ❌ 禁止未完成前序阶段就跳到后续阶段\n- ❌ 禁止让成员互相直连通信,所有跨成员信息流必须经主理人中转\n- ❌ 禁止 spawn 主理人自己(编排、汇总、决策由主理人亲自完成)\n\n### 协作规则\n\n1. 所有成员调度必须经过\"TeamCreate → Agent spawn → SendMessage 回传\"正式流程\n2. 每阶段结束后,将完整产出原文传递给下一阶段成员\n3. 调度成员时,在 Agent 工具的 `name` 参数中传入该成员的 **Agent ID**(agents/ 下的 MD 文件名,不含 .md),`subagent_type` 也传入相同值。**禁止**使用中文名或自创名称\n4. 裁决型角色(如研究主管、风险主管)必须给出明确结论,不得回避决策\n5. 每完成一个阶段向用户简要通报进度\n\n---\n\n## SOP 工作流编排\n\n### 并行 vs 串行\n\n- **并行 Phase**:同一条消息中 spawn 多个成员(使用 TeamCreate),适用于成员间无数据依赖\n- **串行 Phase**:等前一 Phase 全部回传后,将结论传入下一 Phase 的成员 prompt\n\n### 示例 SOP\n\n```\nPhase 1(并行):\n stock-researcher → 基本面+财务\n valuation-pricer → 估值判断\n money-tracker → 资金态度\n\nPhase 2(串行,Phase 1 结论传入):\n risk-doctor → 综合风险诊断\n\n主理人汇编 → 输出\n```\n\n### 预设 Workflow 设计原则\n\n每个 Workflow 写明:\n- **触发条件**:什么类型的问法匹配此 Workflow\n- **Phase 编排**:分 Phase 的串并行调度\n- **输入输出依赖**:每个 Phase 的输入来自哪里、输出传给谁\n\nWorkflow 数量根据用户实际高频场景设计,不是越多越好。\n\n---\n\n## settings.json\n\nTeam 型必须有 `settings.json`:\n\n```json\n{\n \"agent\": \"{team}-team-lead\"\n}\n```\n\n值必须与 plugin.json 的 `agentName` 和主理人 MD 文件的 `name` 一致。\n",
|
||||
"references/avatar-spec.md": "# 头像生成规范\n\n## 基本要求\n\n| 项目 | 要求 |\n|------|------|\n| 格式 | PNG(推荐)或 JPG |\n| 尺寸 | 512×512 px(正方形) |\n| 大小 | 单张不超过 500KB |\n| 风格 | 统一漫画/插画风格,专业自然 |\n| 内容 | 符合角色定位,不含违规内容 |\n\n> 使用 `ImageGen` 工具生成,`size` 参数设为 `\"1024x1024\"`(高清大图,市场缩放为 512×512 展示)。\n\n## 生成策略\n\n**Agent 型**:1 张头像\n- `avatars/expert.png`\n\n**Team 型**:N+1 张头像\n- `avatars/team.png` — 团队整体头像\n- `avatars/{team}-team-lead.png` — 主理人头像\n- `avatars/{member-name}.png` — 每个团员头像\n\n---\n\n## Prompt 构建核心原则\n\n**每个头像的 prompt 必须从对应的 MD 文件描述中提取角色特征,不使用通用模板硬编码。**\n\n### 提取步骤\n\n1. **读取 Agent MD 文件**\n2. **提取角色身份**:从标题和首段提取\n3. **提取专业特征**:从\"核心能力\"章节提取关键词,转化为视觉元素\n4. **推断工作风格**:从\"工作流程\"和\"注意事项\"推断性格气质\n5. **推断人物属性**:从 name 字段推断性别和风格基调\n\n---\n\n## 个人头像 Prompt 组装\n\n```\n[风格前缀] + [角色身份] + [外观特征] + [表情气质] + [背景元素] + [质量后缀]\n```\n\n| 部分 | 说明 | 示例 |\n|------|------|------|\n| 风格前缀 | 统一漫画/插画风格 | `Professional cartoon-style illustration avatar,` |\n| 角色身份 | 从 MD 标题/首段提取 | `a female design system document architect` |\n| 外观特征 | 从核心能力推断穿着/配饰 | `wearing stylish glasses, holding a design specification document` |\n| 表情气质 | 从工作风格推断 | `confident and meticulous expression` |\n| 背景元素 | 从专业领域提取视觉符号 | `subtle design tokens and color palette swatches in background` |\n| 质量后缀 | 固定 | `Bust shot, facing forward. Clean simple background. High quality, professional, natural.` |\n\n### 示例 1:设计系统架构师\n\nMD 核心内容:角色=设计系统文档架构师,能力=9大标准章节、AI可读格式,输出=Markdown+HEX+CSS\n\n```\nProfessional cartoon-style illustration avatar, a female design system document architect,\nwearing stylish glasses, holding a design guideline document, modern creative smart casual attire,\nconfident and meticulous expression with a creative yet precise aura,\nsubtle color palette swatches, typography samples and design token symbols in the background.\nBust shot, facing forward. Clean simple warm-toned background. High quality, professional, natural.\n```\n\n### 示例 2:技术分析师\n\nMD 核心内容:角色=技术分析师,能力=K线形态、均线分析、MACD/RSI/KDJ\n\n```\nProfessional cartoon-style illustration avatar, a male technical stock market analyst named Marco,\nwearing a sharp vest over dress shirt, looking at holographic candlestick charts,\nfocused and analytical expression with sharp observant eyes,\nK-line charts, moving average lines and MACD indicators floating in the background.\nBust shot, facing forward. Clean simple blue-toned background. High quality, professional, natural.\n```\n\n---\n\n## 团队头像 Prompt 构建\n\n**输入来源**:plugin.json 的 `displayDescription` + 主理人 MD 的团队描述\n\n### 提取步骤\n\n1. **团队定位**:从 displayDescription 提取团队做什么\n2. **协作模式**:从主理人 MD 的 SOP 提取工作阶段\n3. **成员构成**:从成员列表提取角色类型的多样性\n4. **视觉表达**:将以上信息转化为体现团队协作的场景\n\n### Prompt 组装\n\n```\n[风格前缀] + [团队场景描述] + [协作元素] + [成员象征] + [质量后缀]\n```\n\n### 示例:交易分析团队\n\n```\nProfessional cartoon-style illustration, a dynamic stock trading analysis team scene,\nmultiple diverse analysts gathered around a central holographic display showing candlestick charts,\na bull figure and a bear figure debating on opposite sides symbolizing bull-bear debate,\nrisk gauges and decision dashboards floating around,\nwarm collaborative atmosphere with focused professional energy.\nClean simple multi-tone gradient background. High quality, professional, team composition.\n```\n\n---\n\n## 同一团队风格统一规则\n\nTeam 型的所有头像必须在 prompt 中保持一致的**风格锚定词**:\n\n**固定风格前缀(每个 prompt 开头):**\n```\nProfessional cartoon-style illustration avatar, consistent art style with warm lighting and soft shadows,\n```\n\n**固定质量后缀(每个 prompt 结尾):**\n```\nBust shot, facing forward. Clean simple {color}-toned background. High quality, professional, natural.\n```\n\n### 背景色调映射\n\n| categoryId | 背景色调 |\n|------------|---------|\n| 01-ProductDesign | warm orange-coral |\n| 02-EngineerinLine truncated
|
||||
"scripts/register_expert.py": "#!/usr/bin/env python3\n\"\"\"\nExpert Register - Validates and registers an expert into marketplace.json\n\nThis script should be called AFTER the expert package is fully created and validated.\nOnly registered experts will be visible in the WorkBuddy expert center.\n\nUsage:\n register_expert.py <expert-dir> [--marketplace-dir <marketplace-dir>]\n\nExamples:\n python3 register_expert.py <expert-dir>\n python3 register_expert.py ./my-expert --marketplace-dir <marketplace-dir>\n\nThe creating session id is read from CODEBUDDY_SESSION_ID when present.\n\"\"\"\n\nimport json\nimport os\nimport sys\nfrom pathlib import Path\nfrom typing import List, Optional, Tuple\n\nSESSION_ID_ENV_VAR = 'CODEBUDDY_SESSION_ID'\nUNEXPANDED_SESSION_ID_ARGS = {'$CODEBUDDY_SESSION_ID', '${CODEBUDDY_SESSION_ID}'}\n\n\ndef get_expert_base_dir():\n \"\"\"获取专家 marketplace 根目录,优先读取 WORKBUDDY_CONFIG_DIR 环境变量。\"\"\"\n config_dir = os.environ.get('WORKBUDDY_CONFIG_DIR', '').strip()\n if not config_dir:\n config_dir = str(Path.home() / '.workbuddy')\n return Path(config_dir) / 'plugins' / 'marketplaces' / 'my-experts'\n\n\ndef find_plugin_json(expert_dir: Path) -> Optional[Path]:\n \"\"\"Find plugin.json in the expert directory.\"\"\"\n for meta_dir in ['.codebuddy-plugin', '.workbuddy-plugin']:\n candidate = expert_dir / meta_dir / 'plugin.json'\n if candidate.exists():\n return candidate\n return None\n\n\ndef validate_expert_completeness(expert_dir: Path) -> Tuple[bool, List[str]]:\n \"\"\"\n Validate that the expert package is complete and ready for registration.\n Returns (is_valid, list_of_errors).\n \"\"\"\n errors = []\n\n # 1. Check plugin.json exists\n plugin_json_path = find_plugin_json(expert_dir)\n if not plugin_json_path:\n errors.append(\"Missing plugin.json (checked .codebuddy-plugin/ and .workbuddy-plugin/)\")\n return False, errors\n\n # 2. Parse plugin.json\n try:\n with open(plugin_json_path, 'r', encoding='utf-8') as f:\n plugin_data = json.load(f)\n except json.JSONDecodeError as e:\n errors.append(f\"plugin.json is not valid JSON: {e}\")\n return False, errors\n\n # 3. Check required fields are not [TODO]\n required_fields = ['name', 'description', 'expertType']\n for field in required_fields:\n value = plugin_data.get(field, '')\n if not value or '[TODO' in str(value):\n errors.append(f\"Field '{field}' is missing or contains [TODO] placeholder\")\n\n # 4. Check displayName is filled\n display_name = plugin_data.get('displayName', {})\n if isinstance(display_name, dict):\n zh_name = display_name.get('zh', '')\n en_name = display_name.get('en', '')\n if (not zh_name or '[TODO' in zh_name) and (not en_name or '[TODO' in en_name):\n errors.append(\"displayName (zh or en) is missing or contains [TODO]\")\n elif not display_name or '[TODO' in str(display_name):\n errors.append(\"displayName is missing or contains [TODO]\")\n\n # 5. Check agents directory has at least one non-template .md file\n agents_dir = expert_dir / 'agents'\n if agents_dir.exists():\n md_files = list(agents_dir.glob('*.md'))\n if not md_files:\n errors.append(\"No .md files found in agents/ directory\")\n else:\n # Check at least one agent file has real content (not all [TODO])\n has_real_content = False\n for md_file in md_files:\n content = md_file.read_text(encoding='utf-8')\n # Count [TODO] occurrences - if more than 3, likely still a template\n todo_count = content.count('[TODO')\n if todo_count <= 2:\n has_real_content = True\n break\n if not has_real_content:\n errors.append(\"All agent .md files still contain many [TODO] placeholders\")\n else:\n errors.append(\"Missing agents/ directory\")\n\n return len(errors) == 0, errors\n\n\ndef _write_session_marker(expert_dir: Path, session_id: str) -> None:\n \"\"\"Write .created-by-session marker file into the expert directory.\"\"\"\n marker_path = expert_dir / '.created-by-session'\n try:\n marker_path.write_text(session_id, encoding='utf-8')\n except OSError as e:\n print(f\"⚠️ Warning: could not write session marker: {e}\")\n\n\ndef normalize_session_id(value: Optional[str]) -> Optional[str]:\n \"\"\"Return a usable session id, ignoring empty or unexpanded shell placeholders.\"\"\"\n session_id = (value or '').strip()\n if not session_id or session_id in UNEXPANDED_SESSION_ID_ARGS:\n return None\n return session_id\n\n\ndef get_session_id(explicit_session_id: Optional[str]) -> Optional[str]:\n \"\"\"Resolve the creating session id from CLI arg oLine truncated
|
||||
"scripts/init_expert.py": "#!/usr/bin/env python3\n\"\"\"\nExpert Initializer - Creates a new expert package from template\n\nUsage:\n init_expert.py <expert-name> --type agent|team --path <output-dir>\n\nExamples:\n init_expert.py my-expert --type agent --path ./output\n init_expert.py my-team --type team --path ./output\n\"\"\"\n\nimport sys\nimport json\nimport os\nimport re\nfrom pathlib import Path\n\n\ndef get_expert_plugins_dir():\n \"\"\"获取专家目录,优先读取 WORKBUDDY_CONFIG_DIR 环境变量。\"\"\"\n config_dir = os.environ.get('WORKBUDDY_CONFIG_DIR', '').strip()\n if not config_dir:\n config_dir = str(Path.home() / '.workbuddy')\n return Path(config_dir) / 'plugins' / 'marketplaces' / 'my-experts' / 'plugins'\n\n\ndef title_case(name):\n \"\"\"Convert kebab-case to Title Case.\"\"\"\n return ' '.join(word.capitalize() for word in name.split('-'))\n\n\nAGENT_PLUGIN_JSON = \"\"\"{\n \"name\": \"%(name)s\",\n \"version\": \"1.0.0\",\n \"description\": \"[TODO: English one-line description]\",\n \"author\": {\n \"name\": \"[TODO: author name]\",\n \"email\": \"[TODO: author email]\"\n },\n \"agents\": [\"./agents/%(agent_name)s.md\"],\n\n \"expertType\": \"agent\",\n \"agentName\": \"%(agent_name)s\",\n\n \"displayName\": {\n \"en\": \"[TODO: English display name]\",\n \"zh\": \"[TODO: 中文显示名称]\"\n },\n \"profession\": {\n \"en\": \"[TODO: English profession]\",\n \"zh\": \"[TODO: 中文职业头衔]\"\n },\n \"displayDescription\": {\n \"en\": \"[TODO: English detailed description]\",\n \"zh\": \"[TODO: 中文详细描述,40-50字]\"\n },\n \"avatar\": \"avatars/expert.png\",\n \"categoryId\": \"[TODO: XX-CategoryName]\",\n \"defaultInitPrompt\": {\n \"zh\": \"[TODO: 中文首次对话提示]\",\n \"en\": \"[TODO: English first prompt]\"\n },\n \"plugin\": \"%(name)s\",\n \"tags\": [\n { \"en\": \"[TODO: Tag1]\", \"zh\": \"[TODO: 标签1]\" },\n { \"en\": \"[TODO: Tag2]\", \"zh\": \"[TODO: 标签2]\" },\n { \"en\": \"[TODO: Tag3]\", \"zh\": \"[TODO: 标签3]\" }\n ],\n \"quickPrompts\": [\n { \"en\": \"[TODO: Prompt1, same as defaultInitPrompt]\", \"zh\": \"[TODO: 提示词1,同defaultInitPrompt]\" },\n { \"en\": \"[TODO: Prompt2]\", \"zh\": \"[TODO: 提示词2]\" },\n { \"en\": \"[TODO: Prompt3]\", \"zh\": \"[TODO: 提示词3]\" }\n ]\n}\n\"\"\"\n\nTEAM_PLUGIN_JSON = \"\"\"{\n \"name\": \"%(name)s\",\n \"version\": \"1.0.0\",\n \"description\": \"[TODO: English one-line description]\",\n \"author\": {\n \"name\": \"[TODO: author name]\",\n \"email\": \"[TODO: author email]\"\n },\n \"agents\": [\n \"./agents/%(team)s-team-lead.md\",\n \"./agents/[TODO: member-a].md\"\n ],\n\n \"expertType\": \"team\",\n \"agentName\": \"%(team)s-team-lead\",\n \"teamInfo\": {\n \"leadAgent\": \"%(team)s-team-lead\",\n \"memberAgents\": [\"[TODO: member-a]\"]\n },\n\n \"displayName\": {\n \"en\": \"[TODO: English team name]\",\n \"zh\": \"[TODO: 中文团队名称]\"\n },\n \"profession\": {\n \"en\": \"[TODO: same as displayName.en]\",\n \"zh\": \"[TODO: 同displayName.zh]\"\n },\n \"displayDescription\": {\n \"en\": \"[TODO: English team description]\",\n \"zh\": \"[TODO: 中文团队描述,40-50字]\"\n },\n \"avatar\": \"avatars/team.png\",\n \"categoryId\": \"[TODO: XX-CategoryName]\",\n \"defaultInitPrompt\": {\n \"zh\": \"[TODO: 中文首次问候]\",\n \"en\": \"[TODO: English first prompt]\"\n },\n \"plugin\": \"%(name)s\",\n \"tags\": [\n { \"en\": \"[TODO: Tag1]\", \"zh\": \"[TODO: 标签1]\" },\n { \"en\": \"[TODO: Tag2]\", \"zh\": \"[TODO: 标签2]\" },\n { \"en\": \"[TODO: Tag3]\", \"zh\": \"[TODO: 标签3]\" }\n ],\n \"quickPrompts\": [\n { \"en\": \"[TODO: Prompt1, same as defaultInitPrompt]\", \"zh\": \"[TODO: 提示词1,同defaultInitPrompt]\" },\n { \"en\": \"[TODO: Prompt2]\", \"zh\": \"[TODO: 提示词2]\" },\n { \"en\": \"[TODO: Prompt3]\", \"zh\": \"[TODO: 提示词3]\" }\n ],\n \"members\": [\n {\n \"id\": \"%(team)s-team-lead\",\n \"displayName\": { \"en\": \"[TODO]\", \"zh\": \"[TODO]\" },\n \"profession\": { \"en\": \"[TODO]\", \"zh\": \"[TODO]\" },\n \"avatar\": \"avatars/%(team)s-team-lead.png\",\n \"role\": \"lead\"\n },\n {\n \"id\": \"[TODO: member-a]\",\n \"displayName\": { \"en\": \"[TODO]\", \"zh\": \"[TODO]\" },\n \"profession\": { \"en\": \"[TODO]\", \"zh\": \"[TODO]\" },\n \"avatar\": \"avatars/[TODO: member-a].png\",\n \"role\": \"member\"\n }\n ]\n}\n\"\"\"\n\nAGENT_MD_TEMPLATE = \"\"\"---\nname: %(agent_name)s\ndescription: \"[TODO: English description for AI to determine when to activate]\"\ndisplayName:\n en: \"[TODO: English display name]\"\n zh: \"[TODO: 中文显示名称]\"\nprofession:\n en: \"[TODO: English profession title]\"\n zh: \"[TODO: 中文职业头衔]\"\nmaxTurns: 50\Line truncated
|
||||
"scripts/validate_expert.py": "#!/usr/bin/env python3\n\"\"\"\nExpert Validator - Validates an expert package against the WorkBuddy specification.\n\nUsage:\n validate_expert.py <path/to/expert-dir>\n\nExample:\n python3 validate_expert.py plugins/my-expert\n\"\"\"\n\nimport sys\nimport json\nimport os\nimport re\nfrom pathlib import Path\n\n\nVALID_CATEGORY_IDS = {\n '01-ProductDesign', '02-Engineering', '03-GameSpatial', '04-DataAI',\n '05-MarketingGrowth', '06-ContentCreative', '07-SalesCommerce',\n '08-FinanceInvestment', '09-OperationsHR', '10-ProjectQuality',\n '11-SecurityCompliance', '12-IndustryConsultant',\n}\n\nVALID_EXPERT_TYPES = {'agent', 'team'}\n\n\nclass ValidationResult:\n def __init__(self):\n self.errors = []\n self.warnings = []\n\n def error(self, msg):\n self.errors.append(msg)\n\n def warn(self, msg):\n self.warnings.append(msg)\n\n @property\n def is_valid(self):\n return len(self.errors) == 0\n\n def summary(self):\n lines = []\n if self.errors:\n lines.append(f\"❌ {len(self.errors)} error(s):\")\n for e in self.errors:\n lines.append(f\" • {e}\")\n if self.warnings:\n lines.append(f\"⚠️ {len(self.warnings)} warning(s):\")\n for w in self.warnings:\n lines.append(f\" • {w}\")\n if self.is_valid:\n lines.append(\"✅ Expert package is valid!\")\n return '\\n'.join(lines)\n\n\ndef parse_md_frontmatter(md_path):\n \"\"\"Parse YAML frontmatter from a Markdown file. Returns dict or None.\"\"\"\n try:\n content = md_path.read_text(encoding='utf-8')\n except Exception as e:\n return None, f\"Cannot read {md_path}: {e}\"\n\n if not content.startswith('---'):\n return None, f\"{md_path.name}: No YAML frontmatter found\"\n\n match = re.match(r'^---\\n(.*?)\\n---', content, re.DOTALL)\n if not match:\n return None, f\"{md_path.name}: Invalid frontmatter format\"\n\n # LIMITATION: Only parses simple top-level \"key: value\" pairs.\n # Does NOT handle nested objects, lists, or multi-line values.\n # Sufficient for extracting 'name', 'description', and detecting 'tools'.\n fm_text = match.group(1)\n result = {}\n\n # Extract simple key-value pairs\n for line in fm_text.split('\\n'):\n line = line.strip()\n if ':' in line and not line.startswith('-') and not line.startswith('#'):\n key, _, value = line.partition(':')\n key = key.strip()\n value = value.strip().strip('\"').strip(\"'\")\n if value:\n result[key] = value\n\n return result, None\n\n\ndef check_i18n_field(obj, field_name, result, context=\"plugin.json\"):\n \"\"\"Check that a field has both 'en' and 'zh' values.\"\"\"\n if field_name not in obj:\n result.error(f\"{context}: missing '{field_name}'\")\n return False\n\n val = obj[field_name]\n if not isinstance(val, dict):\n result.error(f\"{context}: '{field_name}' must be an object with 'en' and 'zh'\")\n return False\n\n if not val.get('en'):\n result.error(f\"{context}: '{field_name}.en' is empty\")\n if not val.get('zh'):\n result.error(f\"{context}: '{field_name}.zh' is empty\")\n\n return True\n\n\ndef validate_plugin_json(plugin_json, expert_dir, result):\n \"\"\"Validate plugin.json fields.\"\"\"\n # Required fields\n for field in ('name', 'version', 'description'):\n if field not in plugin_json:\n result.error(f\"plugin.json: missing required field '{field}'\")\n\n name = plugin_json.get('name', '')\n if name and (len(name) < 2 or not re.match(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$', name)):\n result.error(f\"plugin.json: 'name' must be kebab-case (at least 2 chars), got '{name}'\")\n\n expert_type = plugin_json.get('expertType', '')\n if expert_type not in VALID_EXPERT_TYPES:\n result.error(f\"plugin.json: 'expertType' must be one of {VALID_EXPERT_TYPES}, got '{expert_type}'\")\n return expert_type\n\n # plugin field consistency\n plugin_field = plugin_json.get('plugin', '')\n if plugin_field and plugin_field != name:\n result.error(f\"plugin.json: 'plugin' ({plugin_field}) must equal 'name' ({name})\")\n\n # Agent/Team specific\n if expert_type in ('agent', 'team'):\n agent_name = plugin_json.get('agentName', '')\n if not agent_name:\n result.error(\"plugin.json: missing 'agentName'\")\n else:\n # Check agentName matches an MD file\n md_file = expert_dir / 'agents' / f'{agent_name}.md'\n if not md_file.exists():\n result.error(f\"plugin.json: agentName '{agent_name}' has no matching file agents/{agent_name}.md\")\n\n # agentName must not be generic \"team-lead\"\n if agent_name == 'team-lead':Line truncated
|
||||
"scripts/package_expert.py": "#!/usr/bin/env python3\n\"\"\"\nExpert Packager - Validates and packages an expert directory into a zip file.\n\nUsage:\n package_expert.py <path/to/expert-dir> [output-dir]\n\nExample:\n python3 package_expert.py plugins/my-expert\n python3 package_expert.py plugins/my-expert ./dist\n\"\"\"\n\nimport sys\nimport zipfile\nfrom pathlib import Path\n\n# Import validate_expert from sibling script\nscript_dir = Path(__file__).parent\nsys.path.insert(0, str(script_dir))\nfrom validate_expert import validate_expert\n\n\ndef package_expert(expert_path, output_dir=None):\n \"\"\"\n Validate and package an expert directory into a .zip file.\n\n Args:\n expert_path: Path to the expert directory\n output_dir: Optional output directory (defaults to current directory)\n\n Returns:\n Path to the created zip file, or None if error\n \"\"\"\n expert_path = Path(expert_path).resolve()\n\n if not expert_path.exists():\n print(f\"❌ Error: Expert directory not found: {expert_path}\")\n return None\n\n if not expert_path.is_dir():\n print(f\"❌ Error: Path is not a directory: {expert_path}\")\n return None\n\n # Validate first\n print(\"🔍 Validating expert package...\\n\")\n result = validate_expert(expert_path)\n print(result.summary())\n\n if not result.is_valid:\n print(\"\\n❌ Packaging aborted. Please fix validation errors first.\")\n return None\n\n print()\n\n # Determine output location\n expert_name = expert_path.name\n if output_dir:\n out = Path(output_dir).resolve()\n out.mkdir(parents=True, exist_ok=True)\n else:\n out = Path.cwd()\n\n zip_filename = out / f\"{expert_name}.zip\"\n\n # Create the zip file\n try:\n file_count = 0\n with zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:\n for file_path in sorted(expert_path.rglob('*')):\n if file_path.is_file():\n # Skip hidden files except .codebuddy-plugin\n rel = file_path.relative_to(expert_path)\n parts = rel.parts\n if any(p.startswith('.') and p != '.codebuddy-plugin' for p in parts):\n continue\n # Skip common junk files/directories\n if any(p in ('__pycache__', 'node_modules') for p in parts):\n continue\n if file_path.name in ('.gitkeep', '.DS_Store', 'Thumbs.db'):\n continue\n\n arcname = str(Path(expert_name) / rel)\n zipf.write(file_path, arcname)\n print(f\" 📄 {arcname}\")\n file_count += 1\n\n print(f\"\\n✅ Packaged {file_count} files to: {zip_filename}\")\n print(f\" Size: {zip_filename.stat().st_size / 1024:.1f} KB\")\n return zip_filename\n\n except Exception as e:\n print(f\"❌ Error creating zip: {e}\")\n return None\n\n\ndef main():\n if len(sys.argv) < 2:\n print(\"Usage: python3 package_expert.py <path/to/expert-dir> [output-dir]\")\n print(\"\\nExample:\")\n print(\" python3 package_expert.py plugins/my-expert\")\n print(\" python3 package_expert.py plugins/my-expert ./dist\")\n sys.exit(1)\n\n expert_path = sys.argv[1]\n output_dir = sys.argv[2] if len(sys.argv) > 2 else None\n\n print(f\"📦 Packaging expert: {expert_path}\")\n if output_dir:\n print(f\" Output directory: {output_dir}\")\n print()\n\n result = package_expert(expert_path, output_dir)\n\n if result:\n sys.exit(0)\n else:\n sys.exit(1)\n\n\nif __name__ == \"__main__\":\n main()\n",
|
||||
"scripts/batch_create.py": "#!/usr/bin/env python3\n\"\"\"\n批量创建专家 — 每个专家串行走完整标准流程(init → validate → register)。\n\nUsage:\n python3 scripts/batch_create.py <batch-config.json> [--session-id <id>]\n\nbatch-config.json:\n {\n \"path\": \"<expert-plugins-dir>\",\n \"experts\": [\n { \"name\": \"my-expert\", \"type\": \"agent\" },\n { \"name\": \"my-team\", \"type\": \"team\" }\n ]\n }\n\"\"\"\n\nimport sys\nimport json\nimport os\nimport subprocess\nimport time\nfrom pathlib import Path\n\nSCRIPT_DIR = Path(__file__).parent.resolve()\nREGISTER_MAX_RETRIES = 2\nREGISTER_RETRY_DELAY = 1 # seconds\n\n\ndef run_step(script_name: str, args: list) -> bool:\n \"\"\"运行单个标准流程脚本,返回是否成功。\"\"\"\n cmd = [sys.executable, str(SCRIPT_DIR / script_name)] + args\n result = subprocess.run(cmd, capture_output=True, text=True)\n if result.stdout.strip():\n print(result.stdout.strip())\n if result.returncode != 0 and result.stderr.strip():\n print(result.stderr.strip())\n return result.returncode == 0\n\n\ndef process_one_expert(name: str, expert_type: str, output_dir: Path, session_id: str = None) -> bool:\n \"\"\"\n 单个专家的完整标准流程。\n 每个专家必须串行经过:init → validate → register,缺一不可。\n \"\"\"\n expert_dir = output_dir / name\n expert_path = str(expert_dir)\n\n # Step 1: 初始化目录\n print(f\"\\n{'─'*40}\")\n print(f\"📦 [{name}] Step 1/3: 初始化\")\n if not run_step('init_expert.py', [name, '--type', expert_type, '--path', str(output_dir)]):\n print(f\" ❌ [{name}] 初始化失败,跳过该专家\")\n return False\n\n # ═══════════════════════════════════════════════════════\n # Step 2: AI 填充内容(在实际使用中,这里由 AI 写入文件)\n # 本脚本仅做流程示例,不包含内容生成逻辑。\n # AI 在此步骤应写入 plugin.json、agents/*.md、头像等文件。\n # ═══════════════════════════════════════════════════════\n\n # Step 3: 校验(失败提醒修复后重试)\n print(f\"📋 [{name}] Step 2/3: 校验\")\n if not run_step('validate_expert.py', [expert_path]):\n print(f\" ❌ [{name}] 校验失败,请修复内容后重新校验: python3 scripts/validate_expert.py {expert_path}\")\n return False\n\n # Step 4: 注册(含重试,失败则提醒)\n print(f\"📝 [{name}] Step 3/3: 注册\")\n register_args = [expert_path]\n if session_id:\n register_args.extend(['--session-id', session_id])\n\n for attempt in range(1, REGISTER_MAX_RETRIES + 1):\n if run_step('register_expert.py', register_args):\n print(f\" ✅ [{name}] 完成\")\n return True\n if attempt < REGISTER_MAX_RETRIES:\n print(f\" ⚠️ 注册失败,{REGISTER_RETRY_DELAY}s 后重试 ({attempt}/{REGISTER_MAX_RETRIES})...\")\n time.sleep(REGISTER_RETRY_DELAY)\n\n # 重试耗尽:保留目录(内容已生成),提醒手动重试注册\n print(f\" ❌ [{name}] 注册失败(已重试 {REGISTER_MAX_RETRIES} 次),目录已保留: {expert_path}\")\n print(f\" 💡 请稍后手动重试: python3 scripts/register_expert.py {expert_path} --session-id <session-id>\")\n return False\n\n\ndef main():\n if len(sys.argv) < 2:\n print(__doc__)\n sys.exit(1)\n\n config_path = Path(sys.argv[1]).resolve()\n session_id = None\n\n i = 2\n while i < len(sys.argv):\n if sys.argv[i] == '--session-id' and i + 1 < len(sys.argv):\n session_id = sys.argv[i + 1]\n i += 2\n else:\n i += 1\n\n with open(config_path, 'r', encoding='utf-8') as f:\n config = json.load(f)\n\n default_config = os.environ.get('WORKBUDDY_CONFIG_DIR', '').strip() or str(Path.home() / '.workbuddy')\n default_plugins_path = str(Path(default_config) / 'plugins' / 'marketplaces' / 'my-experts' / 'plugins')\n output_dir = Path(config.get('path', default_plugins_path)).expanduser().resolve()\n experts = config.get('experts', [])\n\n if not experts:\n print(\"❌ 配置中无专家列表\")\n sys.exit(1)\n\n print(f\"🚀 批量创建 {len(experts)} 个专家 → {output_dir}\\n\")\n\n passed = []\n failed = []\n\n # ⚠️ 串行执行:逐个专家依次走完整流程,禁止并行/异步。\n for expert in experts:\n name = expert.get('name', '')\n expert_type = expert.get('type', 'agent')\n if not name:\n continue\n if process_one_expert(name, expert_type, output_dir, session_Line truncated
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "skill-expert-manager",
|
||||
"version": "5.8.2-wb.41111488.gb8f6c953.haebd5991e0e2",
|
||||
"description": "WorkBuddy bundled skill resource: expert-manager.",
|
||||
"author": {
|
||||
"name": "WorkBuddy"
|
||||
},
|
||||
"category": "skill",
|
||||
"keywords": [
|
||||
"workbuddy",
|
||||
"builtin-skill",
|
||||
"expert-manager"
|
||||
],
|
||||
"workbuddy": {
|
||||
"kind": "builtin-skill",
|
||||
"legacyResourceName": "expert-manager",
|
||||
"bundleSegments": [
|
||||
"plugins",
|
||||
"workbuddy-builtin",
|
||||
"skills",
|
||||
"expert-manager"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
---
|
||||
name: expert-manager
|
||||
description: |
|
||||
专家包的全生命周期运营:转化(从开源仓库/本地项目创建专家包)、修改已有专家、合规检查、批量更新、质量审查。
|
||||
触发词:创建专家、转化专家、转成专家、生成专家包、导入专家、convert expert、修改专家、编辑专家、更新专家、modify expert、检查专家、审查专家包、专家合规、专家运营、expert ops。
|
||||
description_en: |
|
||||
Manage the full lifecycle of WorkBuddy Expert packages: create or convert an Expert from an open-source repository or local project, edit existing Experts, run compliance checks, apply batch updates, and review quality. Use for requests to create, import, modify, inspect, or maintain an Expert or Expert Team package.
|
||||
---
|
||||
|
||||
# WorkBuddy 专家包管理器
|
||||
|
||||
> ⚠️ **执行前必读**:当需要使用本 skill 时,你必须先从头到尾完整阅读本 SKILL.md 全文并严格遵守(包括所有规则、流程、References 列表),然后再开始执行任务。禁止跳读或仅凭部分段落就开始行动。
|
||||
|
||||
你是 WorkBuddy 专家包管理器,帮助用户按照 WorkBuddy 专家开发规范(v2.0)创建和维护完整的、可提交审核的专家文件包。
|
||||
|
||||
支持两种专家类型:
|
||||
- **Agent 型**(`expertType: "agent"`):单个 AI 专家
|
||||
- **Team 型**(`expertType: "team"`):多角色协作团队
|
||||
|
||||
支持两种输入模式:
|
||||
1. **交互模式**:用户描述需求,通过提问收集信息后生成
|
||||
2. **资料转化模式**:用户提供现有资料(文档、流程、提示词等),从中提取信息直接转化
|
||||
|
||||
---
|
||||
|
||||
## 关键展示字段对应关系
|
||||
|
||||
以下字段决定了专家在产物面板中的展示,**所有场景(创建/转化/修改)均需遵守**:
|
||||
|
||||
| 展示 | 对应字段 | 变更规则 |
|
||||
|---------|---------|---------|
|
||||
| **名字(职业)** | `profession`(`{en, zh}`) | 卡片标题,应体现专家职业定位 |
|
||||
| **花名** | `displayName`(`{en, zh}`) | 可根据用户要求自由修改 |
|
||||
| **类型** | `expertType`(`"agent"` → 显示"专家",`"team"` → 显示"专家团") | **不可随意指定/变更**,必须根据实际结构判断:单角色 = `"agent"`,多角色协作 = `"team"` |
|
||||
| **行业分类** | `categoryId` | **不可随意指定/变更**,必须根据专家核心能力领域从分类列表中选择,并向用户说明选择理由 |
|
||||
| **能力介绍** | `displayDescription`(`{en, zh}`) | 中文 40-50 字,突出核心能力 |
|
||||
| **擅长领域** | `tags`(`{en, zh}[]`,固定 3 个) | 展示专家擅长的领域标签;已满 3 个时必须提示用户替换或删除已有标签,禁止继续新增 |
|
||||
| **试试这样问我** | `quickPrompts`(`{en, zh}[]`,固定 3 个) | 推荐提示词,第一条同时作为 `defaultInitPrompt` |
|
||||
|
||||
---
|
||||
|
||||
## 一、工作流程
|
||||
|
||||
### 整体流程
|
||||
|
||||
```
|
||||
1. 收集信息(交互 or 资料转化)
|
||||
2. 初始化目录 → scripts/init_expert.py
|
||||
3. 生成文件内容 → 参考 references/
|
||||
4. 生成头像 → 参考 references/avatar-spec.md
|
||||
5. 校验 → scripts/validate_expert.py
|
||||
6. 注册 → scripts/register_expert.py
|
||||
7. 打包 → scripts/package_expert.py
|
||||
```
|
||||
|
||||
> **批量创建**:多个专家时串行重复上述流程,参考 `scripts/batch_create.py`。
|
||||
|
||||
### 场景 A:交互模式
|
||||
|
||||
**专家目录(固定)**:由环境变量 `WORKBUDDY_CONFIG_DIR` 决定,完整路径为 `$WORKBUDDY_CONFIG_DIR/plugins/marketplaces/my-experts/plugins`(未设置时默认 `~/.workbuddy/plugins/marketplaces/my-experts/plugins`)。**禁止**将专家生成到其他目录。如果用户要求创建到其他路径,必须拒绝并说明:"专家必须生成到专家目录才能被检测到,其他目录生成后将无法使用。"然后使用专家目录继续执行。
|
||||
|
||||
**必须明确的信息:**
|
||||
1. **专家类型(expertType)**:Agent 还是 Team?(判定规则见上方"关键展示字段对应关系")
|
||||
2. **专家领域**:擅长什么?
|
||||
|
||||
**Agent 型还需要:**
|
||||
- 名字(中英文)、职业头衔(中英文)
|
||||
- 详细能力描述(中英文,中文 40-50 字)
|
||||
- 首次对话问候语(中英文)
|
||||
- 行业分类(见下方列表)
|
||||
- 擅长领域标签(固定 3 个,中英文)
|
||||
- 推荐提示词(3 个,中英文,第一条同时作为 defaultInitPrompt)
|
||||
- 是否需要附带 Skill / bin/
|
||||
|
||||
**Team 型还需要:**
|
||||
- 团队名称(中英文)、团队职业头衔(中英文,须与团队名称一致)
|
||||
- 主理人名字、职责
|
||||
- 每个团员的名字、职业头衔、职责
|
||||
- 团队 SOP 工作流程
|
||||
- 首次对话问候语、行业分类、标签、推荐提示词
|
||||
|
||||
### 场景 B:资料转化模式
|
||||
|
||||
当用户提供文件路径或粘贴内容时:
|
||||
|
||||
1. **读取分析** — 从资料中提取角色定义、核心能力、SOP、输出规范、约束、脚本、参考资料、角色分工
|
||||
2. **推断 expertType 和 categoryId** — 按"关键展示字段对应关系"中的规则判断,向用户说明理由
|
||||
3. **确认补全** — 向用户确认推断结果(displayName、expertType、categoryId),补全展示信息(名称、头衔、描述、标签等)
|
||||
4. **生成** — 执行后续的初始化 + 文件生成流程
|
||||
|
||||
### 场景 C:修改已有专家
|
||||
|
||||
当用户要求修改/编辑/更新某个专家时:
|
||||
|
||||
**专家目录**:`$WORKBUDDY_CONFIG_DIR/plugins/marketplaces/my-experts/plugins`(默认 `~/.workbuddy/...`)
|
||||
|
||||
**流程:**
|
||||
|
||||
1. **定位专家** — 在专家目录下找到用户指定名称的专家目录(如 `electron-dev`),读取 `.codebuddy-plugin/plugin.json` 和相关文件了解现有内容
|
||||
2. **确认修改范围** — 向用户确认要修改什么(如:提示词、描述、能力、标签、头像、新增/删除团队成员等)
|
||||
3. **执行修改** — 直接编辑对应文件,保持与现有内容风格一致
|
||||
4. **校验** — `python3 scripts/validate_expert.py <expert-dir>`
|
||||
5. **重新注册** — 无论修改了什么字段,都必须重新运行注册脚本:`python3 scripts/register_expert.py <expert-dir>`
|
||||
|
||||
**注意事项:**
|
||||
- 修改前先完整读取现有文件,避免丢失已有内容
|
||||
- 仅修改用户要求变更的部分,不要重写整个文件
|
||||
- 如果用户要修改的专家不存在,提示用户确认【专家中心-我的专家】已有该专家
|
||||
- 修改 `displayName`、`expertType`、`categoryId` 时须遵守"关键展示字段对应关系"中的变更规则
|
||||
- **可以修改的字段**:`displayName`、`profession`、`displayDescription`、`description`、`tags`、`quickPrompts`、`defaultInitPrompt`、`avatar`、`categoryId`(需说明理由)、`members` 的展示信息、Agent MD 的正文内容等
|
||||
- **严禁修改以下字段和文件名**(它们是专家的唯一标识,修改会导致专家丢失):
|
||||
- `plugin.json` 中的 `name` 字段(kebab-case 标识符)
|
||||
- `plugin.json` 中的 `agentName` 字段
|
||||
- 专家目录名(如 `react-dev/`)
|
||||
- `agents/` 目录下的 `.md` 文件名(因为 `agentName` = MD 文件名)
|
||||
- 如果用户要求改 name/目录名,应告知:改名需要重新创建专家,不支持原地改名
|
||||
|
||||
### 第二步:初始化目录
|
||||
|
||||
```bash
|
||||
python3 scripts/init_expert.py <expert-name> --type agent|team --path $WORKBUDDY_CONFIG_DIR/plugins/marketplaces/my-experts/plugins
|
||||
```
|
||||
|
||||
> `--path` 固定为专家目录(由 `WORKBUDDY_CONFIG_DIR` 环境变量决定),禁止指定其他路径。
|
||||
|
||||
生成的模板文件带 `[TODO]` 占位符,后续由 AI 填充实际内容。
|
||||
|
||||
### 第三步:生成文件内容
|
||||
|
||||
参考以下 references 编写各文件:
|
||||
- `@references/plugin-json-spec.md` — plugin.json 字段规范和模板
|
||||
- `@references/agent-md-spec.md` — Agent MD frontmatter 和正文结构
|
||||
- `@references/team-spec.md` — Team 型协作铁律、成员命名、SOP 编排
|
||||
|
||||
### 第四步:生成头像
|
||||
|
||||
参考 `@references/avatar-spec.md` 使用 `ImageGen` 工具为每个角色自动生成头像。
|
||||
|
||||
### 第五步:校验
|
||||
|
||||
```bash
|
||||
python3 scripts/validate_expert.py <path/to/expert-dir>
|
||||
```
|
||||
|
||||
### 第六步:注册
|
||||
|
||||
校验通过后,将专家注册到 `marketplace.json`:
|
||||
|
||||
```bash
|
||||
python3 scripts/register_expert.py <path/to/expert-dir>
|
||||
```
|
||||
|
||||
> 此脚本会:1) 再次校验关键字段不含 `[TODO]`;2) 将专家信息写入 `marketplace.json`;3) 自动从 Bash runtime 注入的 `CODEBUDDY_SESSION_ID` 读取当前会话并写入 `.created-by-session`。如果校验不通过会报错并拒绝注册。
|
||||
|
||||
### 第七步:打包
|
||||
|
||||
```bash
|
||||
python3 scripts/package_expert.py <path/to/expert-dir> [output-dir]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、行业分类(categoryId)
|
||||
|
||||
**判定规则:** 根据专家的核心能力和主要服务领域来选择分类,不可凭直觉或用户随意指定。判定优先级:
|
||||
1. 专家的 **主要输出物** 属于哪个领域(如:输出代码/架构设计 → 技术工程;输出营销方案 → 营销增长)
|
||||
2. 专家的 **服务对象** 是谁(如:服务开发者 → 技术工程;服务投资者 → 金融投资)
|
||||
3. 如有跨领域情况,选择 **最核心** 的一个分类
|
||||
|
||||
| categoryId | 分类名称 | 适用场景举例 |
|
||||
|---|---|---|
|
||||
| 01-ProductDesign | 产品设计 | UI/UX 设计、产品规划、原型设计、交互设计 |
|
||||
| 02-Engineering | 技术工程 | 编程开发、架构设计、DevOps、技术选型 |
|
||||
| 03-GameSpatial | 游戏空间 | 游戏开发、3D 建模、虚拟现实、游戏设计 |
|
||||
| 04-DataAI | 数据智能 | 数据分析、机器学习、大模型应用、BI |
|
||||
| 05-MarketingGrowth | 营销增长 | 品牌营销、用户增长、广告投放、SEO |
|
||||
| 06-ContentCreative | 内容创作 | 文案写作、视频脚本、创意策划、翻译 |
|
||||
| 07-SalesCommerce | 销售商务 | 销售策略、商务谈判、客户管理、电商 |
|
||||
| 08-FinanceInvestment | 金融投资 | 投资分析、财务管理、风控、量化交易 |
|
||||
| 09-OperationsHR | 运营人力 | 项目运营、人力资源、组织管理、培训 |
|
||||
| 10-ProjectQuality | 项目质量 | 项目管理、质量保障、测试、流程优化 |
|
||||
| 11-SecurityCompliance | 法务安全 | 信息安全、合规审查、法务咨询、隐私保护 |
|
||||
| 12-IndustryConsultant | 行业顾问 | 跨行业咨询、战略规划、不属于以上明确分类的 |
|
||||
|
||||
---
|
||||
|
||||
## 三、资料转化策略
|
||||
|
||||
| 资料中的内容 | 转化为 | 放在哪里 |
|
||||
|-------------|--------|---------|
|
||||
| 角色描述、专家人设 | Agent MD 的角色定义和核心能力 | `agents/{name}.md` |
|
||||
| 工作流程、操作步骤 | Agent MD 的工作流程章节 | `agents/{name}.md` |
|
||||
| 输出格式要求 | Agent MD 的输出规范章节 | `agents/{name}.md` |
|
||||
| API 文档、字段定义 | SKILL.md + references/ | `skills/{name}/references/` |
|
||||
| 可执行脚本代码 | scripts/ | `skills/{name}/scripts/` |
|
||||
| 流程模板、报告模板 | templates/ | `skills/{name}/templates/` |
|
||||
| 通用 CLI 工具 | bin/ | `bin/` |
|
||||
| 多角色分工描述 | Team 型主理人 + 各团员 MD | `agents/` 多个 MD |
|
||||
| SOP/阶段性流程 | 主理人 MD 的 SOP 章节 | `agents/{team}-team-lead.md` |
|
||||
| 示例对话 | quickPrompts + defaultInitPrompt | `plugin.json` |
|
||||
|
||||
**转化质量要求:**
|
||||
1. 不丢信息 — 资料中每条有价值信息都体现在生成文件中
|
||||
2. 结构化整理 — 零散信息按标准结构重新组织
|
||||
3. 专业术语保留 — 原样保留不简化
|
||||
4. 大段参考资料放 `references/` — 不要塞进 Agent MD 正文
|
||||
|
||||
---
|
||||
|
||||
## 四、关键规则(铁律)
|
||||
|
||||
1. **name 字段 kebab-case**:如 `my-expert`
|
||||
2. **agentName = MD 文件名**:如 `design-expert` → `agents/design-expert.md`
|
||||
3. **agents 字段是路径数组**:如 `["./agents/my-expert.md"]`
|
||||
4. **tags 固定且只能 3 个、quickPrompts 固定 3 个**:新增前必须检查数量,超过 3 个必须提示用户替换或删除,第一条 quickPrompt = defaultInitPrompt
|
||||
5. **displayDescription 中文 40-50 字**
|
||||
6. **Agent MD frontmatter 禁止声明 tools 字段**
|
||||
7. **主理人文件名必须加专家团前缀**:如 `trading-team-lead.md`,不可用通用 `team-lead.md`
|
||||
8. **Team 型 members 数组含主理人**(role=lead),teamInfo.memberAgents 不含主理人
|
||||
9. **Team 型 profession 须与 displayName 一致**
|
||||
10. **头像自动生成后放入 avatars/**,同一团队头像风格统一
|
||||
11. **同名专家已存在时必须重新校验 + 注册**:用户要求创建专家但目标目录已存在同名专家时,如果不需要初始化目录和创建内容,也要执行后续的校验 + 注册流程,保证专家可用
|
||||
12. **批量创建必须遵循标准流程**:批量创建/转化多个专家时,每个专家必须完整串行经过 `init → validate → register`,禁止跳过校验或注册。参考 `scripts/batch_create.py`,核心模式:
|
||||
```python
|
||||
for expert in experts:
|
||||
# Step 1: init_expert.py(初始化目录)
|
||||
# Step 2: AI 填充内容(plugin.json、agents/*.md、头像 等)
|
||||
# Step 3: validate_expert.py(校验,失败则停止)
|
||||
# Step 4: register_expert.py(注册,校验通过才执行)
|
||||
```
|
||||
**禁止**:只批量写文件而跳过 validate/register;直接写入 `marketplace.json` 绕过注册脚本
|
||||
|
||||
---
|
||||
|
||||
## 五、收尾提醒
|
||||
|
||||
生成完毕后告知用户:
|
||||
1. 🎨 头像已通过 `ImageGen` 生成在 `avatars/`,可手动替换(PNG/JPG,512×512,≤500KB)
|
||||
2. 📦 打包分享:`python3 scripts/package_expert.py <expert-dir>`
|
||||
3. 📋 请核对内容是否准确
|
||||
|
||||
## References
|
||||
|
||||
- `references/plugin-json-spec.md` — plugin.json 完整字段规范和模板
|
||||
- `references/agent-md-spec.md` — Agent MD 结构模板(普通 Agent / 主理人)
|
||||
- `references/team-spec.md` — Team 型协作规范(铁律、命名、SOP)
|
||||
- `references/avatar-spec.md` — 头像生成规范和 prompt 构建策略
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"targetVersion": "WorkBuddy 5.8.2",
|
||||
"rootSha256": "035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918",
|
||||
"scope": "exact retained expert-manager files; no original initialization or Python execution",
|
||||
"files": [
|
||||
{
|
||||
"file": ".codebuddy-plugin/plugin.json",
|
||||
"originalMember": "resources/plugins/workbuddy-builtin/skills/expert-manager/.codebuddy-plugin/plugin.json",
|
||||
"sha256": "6908b9cd71687525e342c2500fa9d32a847f72e3e263190655ca9347402cb389",
|
||||
"bytes": 519,
|
||||
"evidenceId": "ev_c3df29dcb19c5fcaa9e458b1b9acff92e8c75ac935791e0c81900733e61b6282",
|
||||
"retainedExport": ".tmp/rea-expert-package-ev_c3df29dcb19c5fcaa9e458b1b9acff92e8c75ac935791e0c81900733e61b6282.json"
|
||||
},
|
||||
{
|
||||
"file": "SKILL.md",
|
||||
"originalMember": "resources/plugins/workbuddy-builtin/skills/expert-manager/SKILL.md",
|
||||
"sha256": "18b0655c3e7573fe9402fd00a605d2dc94918c9eecd028f9f3923e74da17bc45",
|
||||
"bytes": 13860,
|
||||
"evidenceId": "ev_3cba49fcb15fefc59f54c7691500ad1121a937edb55e606addae17e4186ebfef",
|
||||
"retainedExport": ".tmp/rea-expert-package-ev_3cba49fcb15fefc59f54c7691500ad1121a937edb55e606addae17e4186ebfef.json"
|
||||
},
|
||||
{
|
||||
"file": "references/plugin-json-spec.md",
|
||||
"originalMember": "resources/plugins/workbuddy-builtin/skills/expert-manager/references/plugin-json-spec.md",
|
||||
"sha256": "14e1a6b99c74815c22772a54725ba6d34750a6c9db4c75746c1188e31d2504f3",
|
||||
"bytes": 5226,
|
||||
"evidenceId": "ev_a4b0f399496f62f4dc41998e8327fb219ab84f3dca536ec6554553605b106735",
|
||||
"retainedExport": ".tmp/rea-expert-package-ev_a4b0f399496f62f4dc41998e8327fb219ab84f3dca536ec6554553605b106735.json"
|
||||
},
|
||||
{
|
||||
"file": "references/agent-md-spec.md",
|
||||
"originalMember": "resources/plugins/workbuddy-builtin/skills/expert-manager/references/agent-md-spec.md",
|
||||
"sha256": "ec7373578a5f84d73ba16dcc71e73755e57aa4170cb07cd9bf379627c0f9c007",
|
||||
"bytes": 5021,
|
||||
"evidenceId": "ev_a4b0f399496f62f4dc41998e8327fb219ab84f3dca536ec6554553605b106735",
|
||||
"retainedExport": ".tmp/rea-expert-package-ev_a4b0f399496f62f4dc41998e8327fb219ab84f3dca536ec6554553605b106735.json"
|
||||
},
|
||||
{
|
||||
"file": "references/team-spec.md",
|
||||
"originalMember": "resources/plugins/workbuddy-builtin/skills/expert-manager/references/team-spec.md",
|
||||
"sha256": "1400af007c6b1179429c6d8c856dc1d45ae25dae63645097ddbc801d25d324f9",
|
||||
"bytes": 4605,
|
||||
"evidenceId": "ev_a4b0f399496f62f4dc41998e8327fb219ab84f3dca536ec6554553605b106735",
|
||||
"retainedExport": ".tmp/rea-expert-package-ev_a4b0f399496f62f4dc41998e8327fb219ab84f3dca536ec6554553605b106735.json"
|
||||
},
|
||||
{
|
||||
"file": "references/avatar-spec.md",
|
||||
"originalMember": "resources/plugins/workbuddy-builtin/skills/expert-manager/references/avatar-spec.md",
|
||||
"sha256": "0b42762e05e311632b2f89af35f34458f851a03cce3fcc0300ca0a75e9f8890e",
|
||||
"bytes": 6350,
|
||||
"evidenceId": "ev_a4b0f399496f62f4dc41998e8327fb219ab84f3dca536ec6554553605b106735",
|
||||
"retainedExport": ".tmp/rea-expert-package-ev_a4b0f399496f62f4dc41998e8327fb219ab84f3dca536ec6554553605b106735.json"
|
||||
},
|
||||
{
|
||||
"file": "scripts/register_expert.py",
|
||||
"originalMember": "resources/plugins/workbuddy-builtin/skills/expert-manager/scripts/register_expert.py",
|
||||
"sha256": "b2dd1a0141c2dc88ed7203246335fd261eca97a17f91f4e280d3ce8b10a65b1e",
|
||||
"bytes": 10116,
|
||||
"evidenceId": "ev_25ded03a70790361a3f4fef21a3bd2466d5f7d3d118b7b7aada33b07efcf5cf8",
|
||||
"retainedExport": ".tmp/rea-expert-package-ev_25ded03a70790361a3f4fef21a3bd2466d5f7d3d118b7b7aada33b07efcf5cf8.json"
|
||||
},
|
||||
{
|
||||
"file": "scripts/init_expert.py",
|
||||
"originalMember": "resources/plugins/workbuddy-builtin/skills/expert-manager/scripts/init_expert.py",
|
||||
"sha256": "742fe9b70085a31850caf327d83f416ff19e9ee7482053e7aa99bd9b780206e4",
|
||||
"bytes": 14149,
|
||||
"evidenceId": "ev_25ded03a70790361a3f4fef21a3bd2466d5f7d3d118b7b7aada33b07efcf5cf8",
|
||||
"retainedExport": ".tmp/rea-expert-package-ev_25ded03a70790361a3f4fef21a3bd2466d5f7d3d118b7b7aada33b07efcf5cf8.json"
|
||||
},
|
||||
{
|
||||
"file": "scripts/validate_expert.py",
|
||||
"originalMember": "resources/plugins/workbuddy-builtin/skills/expert-manager/scripts/validate_expert.py",
|
||||
"sha256": "6e85c716af94d6ee2ffc86a24a7230d15cd675a0fbaaf45a8980fd51d9c1c6fd",
|
||||
"bytes": 14346,
|
||||
"evidenceId": "ev_25ded03a70790361a3f4fef21a3bd2466d5f7d3d118b7b7aada33b07efcf5cf8",
|
||||
"retainedExport": ".tmp/rea-expert-package-ev_25ded03a70790361a3f4fef21a3bd2466d5f7d3d118b7b7aada33b07efcf5cf8.json"
|
||||
},
|
||||
{
|
||||
"file": "scripts/package_expert.py",
|
||||
"originalMember": "resources/plugins/workbuddy-builtin/skills/expert-manager/scripts/package_expert.py",
|
||||
"sha256": "e3d82044fd5fc7b749d65c178af0bed18bbc0375a952b878cfa019b09e04d8fb",
|
||||
"bytes": 3631,
|
||||
"evidenceId": "ev_25ded03a70790361a3f4fef21a3bd2466d5f7d3d118b7b7aada33b07efcf5cf8",
|
||||
"retainedExport": ".tmp/rea-expert-package-ev_25ded03a70790361a3f4fef21a3bd2466d5f7d3d118b7b7aada33b07efcf5cf8.json"
|
||||
},
|
||||
{
|
||||
"file": "scripts/batch_create.py",
|
||||
"originalMember": "resources/plugins/workbuddy-builtin/skills/expert-manager/scripts/batch_create.py",
|
||||
"sha256": "c926d7b8721c4ec4179a3d481271bdb01d41764c3301e01cdafd9e96c37e154c",
|
||||
"bytes": 5106,
|
||||
"evidenceId": "ev_25ded03a70790361a3f4fef21a3bd2466d5f7d3d118b7b7aada33b07efcf5cf8",
|
||||
"retainedExport": ".tmp/rea-expert-package-ev_25ded03a70790361a3f4fef21a3bd2466d5f7d3d118b7b7aada33b07efcf5cf8.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
# Agent MD 规范
|
||||
|
||||
## Frontmatter
|
||||
|
||||
### ⚠️ 重要:frontmatter 中不可添加 tools 字段
|
||||
|
||||
所有工具权限由系统统一分配,开发者无需也不能在 frontmatter 中声明 `tools` 字段。
|
||||
|
||||
### 必填字段
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: {与文件名一致,必须有业务语义}
|
||||
description: {英文描述,AI 用来判断何时激活}
|
||||
displayName:
|
||||
en: "{English display name}"
|
||||
zh: "{中文显示名称}"
|
||||
profession:
|
||||
en: "{English profession title}"
|
||||
zh: "{中文职业头衔}"
|
||||
maxTurns: {默认50}
|
||||
---
|
||||
```
|
||||
|
||||
### 可选字段
|
||||
|
||||
```yaml
|
||||
skills: [{skill-name}] # 启动时预加载的 Skill
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 正文结构:普通 Agent / 团员
|
||||
|
||||
```markdown
|
||||
# {角色名称} - {人名}
|
||||
|
||||
{角色描述,说明这是谁,擅长什么}
|
||||
|
||||
## 核心能力
|
||||
1. **{能力1}**:{描述}
|
||||
2. **{能力2}**:{描述}
|
||||
3. **{能力3}**:{描述}
|
||||
|
||||
## 工作流程
|
||||
1. {步骤1}
|
||||
2. {步骤2}
|
||||
3. {步骤3}
|
||||
|
||||
## 输出规范
|
||||
- {规范1}
|
||||
- {规范2}
|
||||
- {规范3}
|
||||
|
||||
## 注意事项
|
||||
- {约束或边界条件}
|
||||
```
|
||||
|
||||
### 团队成员额外要求
|
||||
|
||||
- 不要在 frontmatter 中限制 tools 范围
|
||||
- 由主理人通过 Agent 工具 spawn 为正式 teammate
|
||||
- 分析完成后必须通过 SendMessage 将结果回传给主理人
|
||||
- Prompt 中须明确写出 SendMessage 回传要求
|
||||
|
||||
### 成员 Prompt 必备结构
|
||||
|
||||
1. **角色定义**:一句话说清"你是谁"
|
||||
2. **擅长领域**:3-5 个具体能力点
|
||||
3. **分析框架**:内嵌的分析能力,分步骤流程
|
||||
4. **数据获取方式**:具体的查询命令或工具调用
|
||||
5. **结构化输出模板**:表格/分段格式
|
||||
6. **SendMessage 回传要求**
|
||||
|
||||
---
|
||||
|
||||
## 正文结构:主理人
|
||||
|
||||
> ⚠️ 文件名不可使用通用的 `team-lead.md`,必须加上专家团前缀,如 `trading-team-lead.md`
|
||||
|
||||
### Frontmatter
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: {team}-team-lead
|
||||
description: {English description}
|
||||
displayName:
|
||||
en: "{English display name}"
|
||||
zh: "{中文显示名称}"
|
||||
profession:
|
||||
en: "{English profession title}"
|
||||
zh: "{中文职业头衔}"
|
||||
maxTurns: {turns, 建议150-200}
|
||||
---
|
||||
```
|
||||
|
||||
### 正文模板
|
||||
|
||||
```markdown
|
||||
# {团队名称} - 主理人
|
||||
|
||||
{主理人角色描述,负责协调团队完成什么任务}
|
||||
|
||||
## 团队成员
|
||||
|
||||
### {分组名称}
|
||||
| 成员 | 名字 | 职责 |
|
||||
|------|------|------|
|
||||
| {member-id} | {name} | {职责描述} |
|
||||
|
||||
## 标准工作流程(SOP)
|
||||
|
||||
### Phase 1: {阶段名}
|
||||
{调用哪些成员、输入输出说明}
|
||||
|
||||
### Phase 2: {阶段名}
|
||||
{...}
|
||||
|
||||
### Phase N: 最终报告
|
||||
综合所有分析结果,生成最终报告返回用户。
|
||||
|
||||
## 团队协作机制(铁律)
|
||||
|
||||
你必须走正式的**团队协作流程**,严禁简化或跳过:
|
||||
|
||||
1. **建立团队**:任务开始时由主理人亲自创建团队(TeamCreate),明确协作边界。**团队创建必须且只能由主理人执行,严禁委派任何成员创建团队**
|
||||
2. **调度成员**:按 SOP 阶段将成员拉入协作、下发独立任务;成员作为独立协作方输出专业产出,不得由主理人代写
|
||||
3. **消息中转**:成员产出回传给主理人,由主理人汇总、转交下一阶段;所有跨成员信息流必须经主理人中转,不得互相直连
|
||||
4. **成员结论为准**:任何专业产出必须由对应成员输出后再采信,主理人只做编排与汇编
|
||||
|
||||
### 严禁行为
|
||||
- ❌ 禁止跳过 TeamCreate,直接自己模拟成员发言或并行写出多角色内容
|
||||
- ❌ 禁止自己代写任何团队成员的专业产出
|
||||
- ❌ 禁止未完成前序阶段就跳到后续阶段
|
||||
- ❌ 禁止让成员互相直连通信,所有跨成员信息流必须经主理人中转
|
||||
- ❌ 禁止 spawn 主理人自己
|
||||
|
||||
## 协作规则
|
||||
1. 所有成员调度必须经过"建立团队 → 调度成员 → 成员回传"流程
|
||||
2. 每阶段结束后,将完整产出原文传递给下一阶段成员
|
||||
3. 每完成一个阶段向用户简要通报
|
||||
4. 所有输出使用与用户原始需求相同的语言
|
||||
5. 调度成员时,Agent 工具的 `name` 参数传入成员的 **Agent ID**(MD 文件名,不含 .md),`subagent_type` 也传入相同值。禁止使用中文名或自创名称
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 主理人额外要求
|
||||
|
||||
### 成员能力清单
|
||||
|
||||
主理人 prompt 中必须列出每个成员的:
|
||||
- Agent ID
|
||||
- 擅长领域(3-5 个具体能力点)
|
||||
- 典型问法(什么问题该调它)
|
||||
|
||||
### 预设 Workflow
|
||||
|
||||
针对高频综合性问题设计预设 Workflow,每个 Workflow 写明:
|
||||
- **触发条件**:什么类型的问法匹配
|
||||
- **Phase 编排**:分 Phase 的串并行调度
|
||||
- **输入输出依赖**:每个 Phase 的输入来自哪里、输出传给谁
|
||||
|
||||
### 单 agent 直调路由表
|
||||
|
||||
| 问法类型 | 直接调谁 |
|
||||
|---------|---------|
|
||||
| 单一维度问题 | 对应成员 |
|
||||
| 综合性问题 | 走预设 Workflow |
|
||||
|
||||
---
|
||||
|
||||
## 能独立成 agent 的标准
|
||||
|
||||
判断标准:**有没有用户会直接问它的问题?**
|
||||
- 有 → 独立成 agent
|
||||
- 没有 → 归入其他 agent
|
||||
|
||||
每个 agent 覆盖一个完整的"分析域",域内多个能力归并进来,但不跨域。跨域协作由主理人通过 Workflow 编排。
|
||||
@@ -0,0 +1,162 @@
|
||||
# 头像生成规范
|
||||
|
||||
## 基本要求
|
||||
|
||||
| 项目 | 要求 |
|
||||
|------|------|
|
||||
| 格式 | PNG(推荐)或 JPG |
|
||||
| 尺寸 | 512×512 px(正方形) |
|
||||
| 大小 | 单张不超过 500KB |
|
||||
| 风格 | 统一漫画/插画风格,专业自然 |
|
||||
| 内容 | 符合角色定位,不含违规内容 |
|
||||
|
||||
> 使用 `ImageGen` 工具生成,`size` 参数设为 `"1024x1024"`(高清大图,市场缩放为 512×512 展示)。
|
||||
|
||||
## 生成策略
|
||||
|
||||
**Agent 型**:1 张头像
|
||||
- `avatars/expert.png`
|
||||
|
||||
**Team 型**:N+1 张头像
|
||||
- `avatars/team.png` — 团队整体头像
|
||||
- `avatars/{team}-team-lead.png` — 主理人头像
|
||||
- `avatars/{member-name}.png` — 每个团员头像
|
||||
|
||||
---
|
||||
|
||||
## Prompt 构建核心原则
|
||||
|
||||
**每个头像的 prompt 必须从对应的 MD 文件描述中提取角色特征,不使用通用模板硬编码。**
|
||||
|
||||
### 提取步骤
|
||||
|
||||
1. **读取 Agent MD 文件**
|
||||
2. **提取角色身份**:从标题和首段提取
|
||||
3. **提取专业特征**:从"核心能力"章节提取关键词,转化为视觉元素
|
||||
4. **推断工作风格**:从"工作流程"和"注意事项"推断性格气质
|
||||
5. **推断人物属性**:从 name 字段推断性别和风格基调
|
||||
|
||||
---
|
||||
|
||||
## 个人头像 Prompt 组装
|
||||
|
||||
```
|
||||
[风格前缀] + [角色身份] + [外观特征] + [表情气质] + [背景元素] + [质量后缀]
|
||||
```
|
||||
|
||||
| 部分 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| 风格前缀 | 统一漫画/插画风格 | `Professional cartoon-style illustration avatar,` |
|
||||
| 角色身份 | 从 MD 标题/首段提取 | `a female design system document architect` |
|
||||
| 外观特征 | 从核心能力推断穿着/配饰 | `wearing stylish glasses, holding a design specification document` |
|
||||
| 表情气质 | 从工作风格推断 | `confident and meticulous expression` |
|
||||
| 背景元素 | 从专业领域提取视觉符号 | `subtle design tokens and color palette swatches in background` |
|
||||
| 质量后缀 | 固定 | `Bust shot, facing forward. Clean simple background. High quality, professional, natural.` |
|
||||
|
||||
### 示例 1:设计系统架构师
|
||||
|
||||
MD 核心内容:角色=设计系统文档架构师,能力=9大标准章节、AI可读格式,输出=Markdown+HEX+CSS
|
||||
|
||||
```
|
||||
Professional cartoon-style illustration avatar, a female design system document architect,
|
||||
wearing stylish glasses, holding a design guideline document, modern creative smart casual attire,
|
||||
confident and meticulous expression with a creative yet precise aura,
|
||||
subtle color palette swatches, typography samples and design token symbols in the background.
|
||||
Bust shot, facing forward. Clean simple warm-toned background. High quality, professional, natural.
|
||||
```
|
||||
|
||||
### 示例 2:技术分析师
|
||||
|
||||
MD 核心内容:角色=技术分析师,能力=K线形态、均线分析、MACD/RSI/KDJ
|
||||
|
||||
```
|
||||
Professional cartoon-style illustration avatar, a male technical stock market analyst named Marco,
|
||||
wearing a sharp vest over dress shirt, looking at holographic candlestick charts,
|
||||
focused and analytical expression with sharp observant eyes,
|
||||
K-line charts, moving average lines and MACD indicators floating in the background.
|
||||
Bust shot, facing forward. Clean simple blue-toned background. High quality, professional, natural.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 团队头像 Prompt 构建
|
||||
|
||||
**输入来源**:plugin.json 的 `displayDescription` + 主理人 MD 的团队描述
|
||||
|
||||
### 提取步骤
|
||||
|
||||
1. **团队定位**:从 displayDescription 提取团队做什么
|
||||
2. **协作模式**:从主理人 MD 的 SOP 提取工作阶段
|
||||
3. **成员构成**:从成员列表提取角色类型的多样性
|
||||
4. **视觉表达**:将以上信息转化为体现团队协作的场景
|
||||
|
||||
### Prompt 组装
|
||||
|
||||
```
|
||||
[风格前缀] + [团队场景描述] + [协作元素] + [成员象征] + [质量后缀]
|
||||
```
|
||||
|
||||
### 示例:交易分析团队
|
||||
|
||||
```
|
||||
Professional cartoon-style illustration, a dynamic stock trading analysis team scene,
|
||||
multiple diverse analysts gathered around a central holographic display showing candlestick charts,
|
||||
a bull figure and a bear figure debating on opposite sides symbolizing bull-bear debate,
|
||||
risk gauges and decision dashboards floating around,
|
||||
warm collaborative atmosphere with focused professional energy.
|
||||
Clean simple multi-tone gradient background. High quality, professional, team composition.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 同一团队风格统一规则
|
||||
|
||||
Team 型的所有头像必须在 prompt 中保持一致的**风格锚定词**:
|
||||
|
||||
**固定风格前缀(每个 prompt 开头):**
|
||||
```
|
||||
Professional cartoon-style illustration avatar, consistent art style with warm lighting and soft shadows,
|
||||
```
|
||||
|
||||
**固定质量后缀(每个 prompt 结尾):**
|
||||
```
|
||||
Bust shot, facing forward. Clean simple {color}-toned background. High quality, professional, natural.
|
||||
```
|
||||
|
||||
### 背景色调映射
|
||||
|
||||
| categoryId | 背景色调 |
|
||||
|------------|---------|
|
||||
| 01-ProductDesign | warm orange-coral |
|
||||
| 02-Engineering | blue-purple |
|
||||
| 03-GameSpatial | purple-red gradient |
|
||||
| 04-DataAI | cyan-teal |
|
||||
| 05-MarketingGrowth | red-orange |
|
||||
| 06-ContentCreative | pink-magenta |
|
||||
| 07-SalesCommerce | golden-amber |
|
||||
| 08-FinanceInvestment | dark blue with gold accent |
|
||||
| 09-OperationsHR | navy slate-blue |
|
||||
| 10-ProjectQuality | green-emerald |
|
||||
| 11-SecurityCompliance | dark grey-blue |
|
||||
| 12-IndustryConsultant | deep teal with silver accent |
|
||||
|
||||
---
|
||||
|
||||
## 执行流程
|
||||
|
||||
1. **读取 Agent MD** — 逐个读取 agents/ 下每个 MD
|
||||
2. **提取角色特征** — 从角色定义、核心能力、工作流程中提取
|
||||
3. **构建个人 prompt** — 按上述步骤将特征转化为视觉描述
|
||||
4. **构建团队 prompt**(Team 型)— 从 displayDescription 和主理人 MD 提取
|
||||
5. **统一风格锚定** — 确保所有 prompt 使用相同的风格前缀和后缀
|
||||
6. **调用 ImageGen** — 逐张生成,`output_dir` 为专家包的 `avatars/`,`size` 为 `"1024x1024"`
|
||||
7. **重命名文件** — 将生成的图片重命名为 plugin.json 中声明的文件名
|
||||
8. **验证** — 确认所有头像文件已存在于 avatars/
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **必须基于 MD 描述生成**:不要使用通用 prompt
|
||||
2. **团队头像体现协作**:不是简单人物剪影
|
||||
3. **同一团队画风一致**:共用风格锚定词和背景色调
|
||||
4. **生成失败处理**:在 README.md 中标注需手动补充的头像,附推荐 prompt
|
||||
5. **用户可替换**:提醒自动生成的头像可手动替换(512×512,PNG/JPG,≤500KB)
|
||||
@@ -0,0 +1,184 @@
|
||||
# plugin.json 字段规范
|
||||
|
||||
## 基础字段(必填)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `name` | string | 唯一标识,小写字母+连字符,也是技能命名空间前缀 |
|
||||
| `version` | string | 语义化版本号(MAJOR.MINOR.PATCH) |
|
||||
| `description` | string | 英文一句话描述 |
|
||||
|
||||
## 可选基础字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `author` | `{name, email}` | 作者信息 |
|
||||
| `homepage` | string 或 `{url, type}` | 项目主页 |
|
||||
| `license` | string | 许可证 |
|
||||
| `keywords` | string[] | 搜索标签 |
|
||||
|
||||
## 类型字段
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `expertType` | `"agent"` / `"team"` |
|
||||
| `agentName` | 主 Agent 名称(对应 agents/ 下 MD 文件名,不含 .md)。**必须有业务语义**,不能使用 `team-lead` 等通用名 |
|
||||
| `teamInfo` | team 时必填:`{leadAgent, memberAgents[]}` |
|
||||
|
||||
## 资源声明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `agents` | string[] | Agent 定义文件路径列表(如 `["./agents/my-expert.md"]`) |
|
||||
| `skills` | string[] | Skill 目录路径列表(如 `["./skills/my-skill"]`) |
|
||||
|
||||
## 展示字段(agent/team 上架市场时必填)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `displayName` | `{en, zh}` | 展示名称 |
|
||||
| `profession` | `{en, zh}` | 职业/定位。**Team 型须与 displayName 一致** |
|
||||
| `displayDescription` | `{en, zh}` | 展示描述。**中文 40-50 字,突出核心能力** |
|
||||
| `avatar` | string | 头像相对路径 |
|
||||
| `categoryId` | string | 行业分类 ID |
|
||||
| `defaultInitPrompt` | `{en, zh}` | 默认引导语。**须与 quickPrompts 第一条一致** |
|
||||
| `tags` | `{en, zh}[]` | 擅长领域标签(**固定 3 个**) |
|
||||
| `quickPrompts` | `{en, zh}[]` | 推荐提示词(**固定 3 个**) |
|
||||
| `plugin` | string | 值与 `name` 一致 |
|
||||
|
||||
## Team 专用字段
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `members[]` | 每个成员含 `{id, name:{en,zh}, profession:{en,zh}, avatar, role}` |
|
||||
|
||||
- `role` 取值:`"lead"` 或 `"member"`
|
||||
- 主理人也必须在 members 中,role 为 `"lead"`
|
||||
- `teamInfo.memberAgents` **不含**主理人
|
||||
|
||||
---
|
||||
|
||||
## 模板:Agent 型
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "{kebab-case-name}",
|
||||
"version": "1.0.0",
|
||||
"description": "{English one-line description}",
|
||||
"author": {
|
||||
"name": "{author-name}",
|
||||
"email": "{author-email}"
|
||||
},
|
||||
"agents": ["./agents/{agent-name}.md"],
|
||||
"skills": ["./skills/{skill-name}"],
|
||||
|
||||
"expertType": "agent",
|
||||
"agentName": "{agent-name}",
|
||||
|
||||
"displayName": {
|
||||
"en": "{English display name}",
|
||||
"zh": "{中文显示名称}"
|
||||
},
|
||||
"profession": {
|
||||
"en": "{English profession}",
|
||||
"zh": "{中文职业头衔}"
|
||||
},
|
||||
"displayDescription": {
|
||||
"en": "{English detailed description}",
|
||||
"zh": "{中文详细描述,40-50字}"
|
||||
},
|
||||
"avatar": "avatars/expert.png",
|
||||
"categoryId": "{XX-CategoryName}",
|
||||
"defaultInitPrompt": {
|
||||
"zh": "{中文首次对话提示}",
|
||||
"en": "{English first prompt}"
|
||||
},
|
||||
"plugin": "{kebab-case-name}",
|
||||
"tags": [
|
||||
{ "en": "{Tag1 EN}", "zh": "{标签1}" },
|
||||
{ "en": "{Tag2 EN}", "zh": "{标签2}" },
|
||||
{ "en": "{Tag3 EN}", "zh": "{标签3}" }
|
||||
],
|
||||
"quickPrompts": [
|
||||
{ "en": "{Prompt1 EN}", "zh": "{提示词1}" },
|
||||
{ "en": "{Prompt2 EN}", "zh": "{提示词2}" },
|
||||
{ "en": "{Prompt3 EN}", "zh": "{提示词3}" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> 如果没有 skills,则省略 `"skills"` 字段。
|
||||
|
||||
## 模板:Team 型
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "{kebab-case-name}",
|
||||
"version": "1.0.0",
|
||||
"description": "{English one-line description}",
|
||||
"author": {
|
||||
"name": "{author-name}",
|
||||
"email": "{author-email}"
|
||||
},
|
||||
"agents": [
|
||||
"./agents/{team}-team-lead.md",
|
||||
"./agents/{member-a}.md",
|
||||
"./agents/{member-b}.md"
|
||||
],
|
||||
"skills": ["./skills/{skill-name}"],
|
||||
|
||||
"expertType": "team",
|
||||
"agentName": "{team}-team-lead",
|
||||
"teamInfo": {
|
||||
"leadAgent": "{team}-team-lead",
|
||||
"memberAgents": ["{member-a}", "{member-b}"]
|
||||
},
|
||||
|
||||
"displayName": {
|
||||
"en": "{English team name}",
|
||||
"zh": "{中文团队名称}"
|
||||
},
|
||||
"profession": {
|
||||
"en": "{English team name}",
|
||||
"zh": "{中文团队名称}"
|
||||
},
|
||||
"displayDescription": {
|
||||
"en": "{English team description}",
|
||||
"zh": "{中文团队描述,40-50字}"
|
||||
},
|
||||
"avatar": "avatars/team.png",
|
||||
"categoryId": "{XX-CategoryName}",
|
||||
"defaultInitPrompt": {
|
||||
"zh": "{中文首次问候}",
|
||||
"en": "{English first prompt}"
|
||||
},
|
||||
"plugin": "{kebab-case-name}",
|
||||
"tags": [
|
||||
{ "en": "{Tag1 EN}", "zh": "{标签1}" },
|
||||
{ "en": "{Tag2 EN}", "zh": "{标签2}" },
|
||||
{ "en": "{Tag3 EN}", "zh": "{标签3}" }
|
||||
],
|
||||
"quickPrompts": [
|
||||
{ "en": "{Prompt1 EN}", "zh": "{提示词1}" },
|
||||
{ "en": "{Prompt2 EN}", "zh": "{提示词2}" },
|
||||
{ "en": "{Prompt3 EN}", "zh": "{提示词3}" }
|
||||
],
|
||||
"members": [
|
||||
{
|
||||
"id": "{team}-team-lead",
|
||||
"displayName": { "en": "{EN}", "zh": "{ZH}" },
|
||||
"profession": { "en": "{EN}", "zh": "{ZH}" },
|
||||
"avatar": "avatars/{team}-team-lead.png",
|
||||
"role": "lead"
|
||||
},
|
||||
{
|
||||
"id": "{member-name}",
|
||||
"displayName": { "en": "{EN}", "zh": "{ZH}" },
|
||||
"profession": { "en": "{EN}", "zh": "{ZH}" },
|
||||
"avatar": "avatars/{member-name}.png",
|
||||
"role": "member"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# Team 型专用规范
|
||||
|
||||
## 成员命名规范
|
||||
|
||||
Team 型专家团中,每个成员有三个外露字段:`name`(名字)、`profession`(职业头衔)、`nickname`(昵称,可选)。三者不应重复。
|
||||
|
||||
### name:谐音花名风格
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 建议三个字 | 姓+名,建议三个字,两个字也可 |
|
||||
| 先是正常人名 | 姓+名结构,不解释也能当名字用 |
|
||||
| 暗含职能巧思 | 谐音、拆字、典故均可,但不刻意 |
|
||||
| 不与 profession 重复 | name 是"谁",profession 是"做什么" |
|
||||
| 中英文都自然 | 中文用花名,英文用拼音姓氏 |
|
||||
|
||||
**示例(软件开发团队):**
|
||||
|
||||
| profession | name(zh) | 巧思 | name(en) |
|
||||
|-----------|----------|------|----------|
|
||||
| 交付总监 | 齐活林 | 齐活了(交付完成) | Qi |
|
||||
| 产品经理 | 许清楚 | 需求要说清楚 | Xu |
|
||||
| 架构师 | 高见远 | 高见(架构视野) | Gao |
|
||||
| 项目经理 | 毕达成 | 必达(交付必达) | Bi |
|
||||
| 工程师 | 寇豆码 | code(代码) | Kou |
|
||||
| QA工程师 | 严过关 | 严格过关 | Yan |
|
||||
|
||||
**禁止:**
|
||||
- 叠字谐音(领码码、需求求、蓝图图)
|
||||
- 一个字的 name 或纯职能词
|
||||
- 和 profession 重复
|
||||
- 无意义随机名(张三、John Doe)
|
||||
- 英文 name 用 Agent ID
|
||||
- 读起来是短语而非人名
|
||||
|
||||
### 主理人 profession 规范
|
||||
|
||||
主理人的 `profession` 不能用通用 title(团长、主理人、Team Lead),应体现该团队的调度风格和业务定位。
|
||||
|
||||
**示例:**
|
||||
|
||||
| 专家团 | 主理人 name | profession(zh) | profession(en) |
|
||||
|--------|-----------|---------------|----------------|
|
||||
| 软件开发团队 | 齐活林 | 交付总监 | Delivery Director |
|
||||
| 交易分析团队 | 何执舟 | 首席策略官 | Chief Strategist |
|
||||
| 营销战役团队 | 营销总监 | 增长操盘手 | Growth Operator |
|
||||
| 深度研究团队 | 顾全之 | 研究主编 | Research Editor |
|
||||
|
||||
---
|
||||
|
||||
## 协作铁律
|
||||
|
||||
### 4 条正则
|
||||
|
||||
1. **建立团队**:任务开始时由主理人亲自创建团队(TeamCreate),明确协作边界。**团队创建必须且只能由主理人执行,严禁委派任何成员创建团队**
|
||||
2. **调度成员**:按 SOP 阶段将成员拉入协作、下发独立任务;成员作为独立协作方输出专业产出,不得由主理人代写
|
||||
3. **消息中转**:成员产出回传给主理人,由主理人汇总、转交下一阶段;所有跨成员信息流必须经主理人中转,不得互相直连
|
||||
4. **成员结论为准**:任何专业产出必须由对应成员输出后再采信,主理人只做编排与汇编
|
||||
|
||||
### 5 条红线
|
||||
|
||||
- ❌ 禁止跳过 TeamCreate,直接自己模拟成员发言或并行写出多角色内容
|
||||
- ❌ 禁止自己代写任何团队成员的专业产出
|
||||
- ❌ 禁止未完成前序阶段就跳到后续阶段
|
||||
- ❌ 禁止让成员互相直连通信,所有跨成员信息流必须经主理人中转
|
||||
- ❌ 禁止 spawn 主理人自己(编排、汇总、决策由主理人亲自完成)
|
||||
|
||||
### 协作规则
|
||||
|
||||
1. 所有成员调度必须经过"TeamCreate → Agent spawn → SendMessage 回传"正式流程
|
||||
2. 每阶段结束后,将完整产出原文传递给下一阶段成员
|
||||
3. 调度成员时,在 Agent 工具的 `name` 参数中传入该成员的 **Agent ID**(agents/ 下的 MD 文件名,不含 .md),`subagent_type` 也传入相同值。**禁止**使用中文名或自创名称
|
||||
4. 裁决型角色(如研究主管、风险主管)必须给出明确结论,不得回避决策
|
||||
5. 每完成一个阶段向用户简要通报进度
|
||||
|
||||
---
|
||||
|
||||
## SOP 工作流编排
|
||||
|
||||
### 并行 vs 串行
|
||||
|
||||
- **并行 Phase**:同一条消息中 spawn 多个成员(使用 TeamCreate),适用于成员间无数据依赖
|
||||
- **串行 Phase**:等前一 Phase 全部回传后,将结论传入下一 Phase 的成员 prompt
|
||||
|
||||
### 示例 SOP
|
||||
|
||||
```
|
||||
Phase 1(并行):
|
||||
stock-researcher → 基本面+财务
|
||||
valuation-pricer → 估值判断
|
||||
money-tracker → 资金态度
|
||||
|
||||
Phase 2(串行,Phase 1 结论传入):
|
||||
risk-doctor → 综合风险诊断
|
||||
|
||||
主理人汇编 → 输出
|
||||
```
|
||||
|
||||
### 预设 Workflow 设计原则
|
||||
|
||||
每个 Workflow 写明:
|
||||
- **触发条件**:什么类型的问法匹配此 Workflow
|
||||
- **Phase 编排**:分 Phase 的串并行调度
|
||||
- **输入输出依赖**:每个 Phase 的输入来自哪里、输出传给谁
|
||||
|
||||
Workflow 数量根据用户实际高频场景设计,不是越多越好。
|
||||
|
||||
---
|
||||
|
||||
## settings.json
|
||||
|
||||
Team 型必须有 `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent": "{team}-team-lead"
|
||||
}
|
||||
```
|
||||
|
||||
值必须与 plugin.json 的 `agentName` 和主理人 MD 文件的 `name` 一致。
|
||||
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
批量创建专家 — 每个专家串行走完整标准流程(init → validate → register)。
|
||||
|
||||
Usage:
|
||||
python3 scripts/batch_create.py <batch-config.json> [--session-id <id>]
|
||||
|
||||
batch-config.json:
|
||||
{
|
||||
"path": "<expert-plugins-dir>",
|
||||
"experts": [
|
||||
{ "name": "my-expert", "type": "agent" },
|
||||
{ "name": "my-team", "type": "team" }
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||
REGISTER_MAX_RETRIES = 2
|
||||
REGISTER_RETRY_DELAY = 1 # seconds
|
||||
|
||||
|
||||
def run_step(script_name: str, args: list) -> bool:
|
||||
"""运行单个标准流程脚本,返回是否成功。"""
|
||||
cmd = [sys.executable, str(SCRIPT_DIR / script_name)] + args
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.stdout.strip():
|
||||
print(result.stdout.strip())
|
||||
if result.returncode != 0 and result.stderr.strip():
|
||||
print(result.stderr.strip())
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def process_one_expert(name: str, expert_type: str, output_dir: Path, session_id: str = None) -> bool:
|
||||
"""
|
||||
单个专家的完整标准流程。
|
||||
每个专家必须串行经过:init → validate → register,缺一不可。
|
||||
"""
|
||||
expert_dir = output_dir / name
|
||||
expert_path = str(expert_dir)
|
||||
|
||||
# Step 1: 初始化目录
|
||||
print(f"\n{'─'*40}")
|
||||
print(f"📦 [{name}] Step 1/3: 初始化")
|
||||
if not run_step('init_expert.py', [name, '--type', expert_type, '--path', str(output_dir)]):
|
||||
print(f" ❌ [{name}] 初始化失败,跳过该专家")
|
||||
return False
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# Step 2: AI 填充内容(在实际使用中,这里由 AI 写入文件)
|
||||
# 本脚本仅做流程示例,不包含内容生成逻辑。
|
||||
# AI 在此步骤应写入 plugin.json、agents/*.md、头像等文件。
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
# Step 3: 校验(失败提醒修复后重试)
|
||||
print(f"📋 [{name}] Step 2/3: 校验")
|
||||
if not run_step('validate_expert.py', [expert_path]):
|
||||
print(f" ❌ [{name}] 校验失败,请修复内容后重新校验: python3 scripts/validate_expert.py {expert_path}")
|
||||
return False
|
||||
|
||||
# Step 4: 注册(含重试,失败则提醒)
|
||||
print(f"📝 [{name}] Step 3/3: 注册")
|
||||
register_args = [expert_path]
|
||||
if session_id:
|
||||
register_args.extend(['--session-id', session_id])
|
||||
|
||||
for attempt in range(1, REGISTER_MAX_RETRIES + 1):
|
||||
if run_step('register_expert.py', register_args):
|
||||
print(f" ✅ [{name}] 完成")
|
||||
return True
|
||||
if attempt < REGISTER_MAX_RETRIES:
|
||||
print(f" ⚠️ 注册失败,{REGISTER_RETRY_DELAY}s 后重试 ({attempt}/{REGISTER_MAX_RETRIES})...")
|
||||
time.sleep(REGISTER_RETRY_DELAY)
|
||||
|
||||
# 重试耗尽:保留目录(内容已生成),提醒手动重试注册
|
||||
print(f" ❌ [{name}] 注册失败(已重试 {REGISTER_MAX_RETRIES} 次),目录已保留: {expert_path}")
|
||||
print(f" 💡 请稍后手动重试: python3 scripts/register_expert.py {expert_path} --session-id <session-id>")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
config_path = Path(sys.argv[1]).resolve()
|
||||
session_id = None
|
||||
|
||||
i = 2
|
||||
while i < len(sys.argv):
|
||||
if sys.argv[i] == '--session-id' and i + 1 < len(sys.argv):
|
||||
session_id = sys.argv[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
default_config = os.environ.get('WORKBUDDY_CONFIG_DIR', '').strip() or str(Path.home() / '.workbuddy')
|
||||
default_plugins_path = str(Path(default_config) / 'plugins' / 'marketplaces' / 'my-experts' / 'plugins')
|
||||
output_dir = Path(config.get('path', default_plugins_path)).expanduser().resolve()
|
||||
experts = config.get('experts', [])
|
||||
|
||||
if not experts:
|
||||
print("❌ 配置中无专家列表")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"🚀 批量创建 {len(experts)} 个专家 → {output_dir}\n")
|
||||
|
||||
passed = []
|
||||
failed = []
|
||||
|
||||
# ⚠️ 串行执行:逐个专家依次走完整流程,禁止并行/异步。
|
||||
for expert in experts:
|
||||
name = expert.get('name', '')
|
||||
expert_type = expert.get('type', 'agent')
|
||||
if not name:
|
||||
continue
|
||||
if process_one_expert(name, expert_type, output_dir, session_id):
|
||||
passed.append(name)
|
||||
else:
|
||||
failed.append(name)
|
||||
|
||||
# 汇总
|
||||
print(f"\n{'═'*40}")
|
||||
print(f"📊 结果: {len(passed)} 成功, {len(failed)} 失败")
|
||||
if failed:
|
||||
print(f" 失败: {', '.join(failed)}")
|
||||
sys.exit(1 if failed else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,485 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Expert Initializer - Creates a new expert package from template
|
||||
|
||||
Usage:
|
||||
init_expert.py <expert-name> --type agent|team --path <output-dir>
|
||||
|
||||
Examples:
|
||||
init_expert.py my-expert --type agent --path ./output
|
||||
init_expert.py my-team --type team --path ./output
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_expert_plugins_dir():
|
||||
"""获取专家目录,优先读取 WORKBUDDY_CONFIG_DIR 环境变量。"""
|
||||
config_dir = os.environ.get('WORKBUDDY_CONFIG_DIR', '').strip()
|
||||
if not config_dir:
|
||||
config_dir = str(Path.home() / '.workbuddy')
|
||||
return Path(config_dir) / 'plugins' / 'marketplaces' / 'my-experts' / 'plugins'
|
||||
|
||||
|
||||
def title_case(name):
|
||||
"""Convert kebab-case to Title Case."""
|
||||
return ' '.join(word.capitalize() for word in name.split('-'))
|
||||
|
||||
|
||||
AGENT_PLUGIN_JSON = """{
|
||||
"name": "%(name)s",
|
||||
"version": "1.0.0",
|
||||
"description": "[TODO: English one-line description]",
|
||||
"author": {
|
||||
"name": "[TODO: author name]",
|
||||
"email": "[TODO: author email]"
|
||||
},
|
||||
"agents": ["./agents/%(agent_name)s.md"],
|
||||
|
||||
"expertType": "agent",
|
||||
"agentName": "%(agent_name)s",
|
||||
|
||||
"displayName": {
|
||||
"en": "[TODO: English display name]",
|
||||
"zh": "[TODO: 中文显示名称]"
|
||||
},
|
||||
"profession": {
|
||||
"en": "[TODO: English profession]",
|
||||
"zh": "[TODO: 中文职业头衔]"
|
||||
},
|
||||
"displayDescription": {
|
||||
"en": "[TODO: English detailed description]",
|
||||
"zh": "[TODO: 中文详细描述,40-50字]"
|
||||
},
|
||||
"avatar": "avatars/expert.png",
|
||||
"categoryId": "[TODO: XX-CategoryName]",
|
||||
"defaultInitPrompt": {
|
||||
"zh": "[TODO: 中文首次对话提示]",
|
||||
"en": "[TODO: English first prompt]"
|
||||
},
|
||||
"plugin": "%(name)s",
|
||||
"tags": [
|
||||
{ "en": "[TODO: Tag1]", "zh": "[TODO: 标签1]" },
|
||||
{ "en": "[TODO: Tag2]", "zh": "[TODO: 标签2]" },
|
||||
{ "en": "[TODO: Tag3]", "zh": "[TODO: 标签3]" }
|
||||
],
|
||||
"quickPrompts": [
|
||||
{ "en": "[TODO: Prompt1, same as defaultInitPrompt]", "zh": "[TODO: 提示词1,同defaultInitPrompt]" },
|
||||
{ "en": "[TODO: Prompt2]", "zh": "[TODO: 提示词2]" },
|
||||
{ "en": "[TODO: Prompt3]", "zh": "[TODO: 提示词3]" }
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
TEAM_PLUGIN_JSON = """{
|
||||
"name": "%(name)s",
|
||||
"version": "1.0.0",
|
||||
"description": "[TODO: English one-line description]",
|
||||
"author": {
|
||||
"name": "[TODO: author name]",
|
||||
"email": "[TODO: author email]"
|
||||
},
|
||||
"agents": [
|
||||
"./agents/%(team)s-team-lead.md",
|
||||
"./agents/[TODO: member-a].md"
|
||||
],
|
||||
|
||||
"expertType": "team",
|
||||
"agentName": "%(team)s-team-lead",
|
||||
"teamInfo": {
|
||||
"leadAgent": "%(team)s-team-lead",
|
||||
"memberAgents": ["[TODO: member-a]"]
|
||||
},
|
||||
|
||||
"displayName": {
|
||||
"en": "[TODO: English team name]",
|
||||
"zh": "[TODO: 中文团队名称]"
|
||||
},
|
||||
"profession": {
|
||||
"en": "[TODO: same as displayName.en]",
|
||||
"zh": "[TODO: 同displayName.zh]"
|
||||
},
|
||||
"displayDescription": {
|
||||
"en": "[TODO: English team description]",
|
||||
"zh": "[TODO: 中文团队描述,40-50字]"
|
||||
},
|
||||
"avatar": "avatars/team.png",
|
||||
"categoryId": "[TODO: XX-CategoryName]",
|
||||
"defaultInitPrompt": {
|
||||
"zh": "[TODO: 中文首次问候]",
|
||||
"en": "[TODO: English first prompt]"
|
||||
},
|
||||
"plugin": "%(name)s",
|
||||
"tags": [
|
||||
{ "en": "[TODO: Tag1]", "zh": "[TODO: 标签1]" },
|
||||
{ "en": "[TODO: Tag2]", "zh": "[TODO: 标签2]" },
|
||||
{ "en": "[TODO: Tag3]", "zh": "[TODO: 标签3]" }
|
||||
],
|
||||
"quickPrompts": [
|
||||
{ "en": "[TODO: Prompt1, same as defaultInitPrompt]", "zh": "[TODO: 提示词1,同defaultInitPrompt]" },
|
||||
{ "en": "[TODO: Prompt2]", "zh": "[TODO: 提示词2]" },
|
||||
{ "en": "[TODO: Prompt3]", "zh": "[TODO: 提示词3]" }
|
||||
],
|
||||
"members": [
|
||||
{
|
||||
"id": "%(team)s-team-lead",
|
||||
"displayName": { "en": "[TODO]", "zh": "[TODO]" },
|
||||
"profession": { "en": "[TODO]", "zh": "[TODO]" },
|
||||
"avatar": "avatars/%(team)s-team-lead.png",
|
||||
"role": "lead"
|
||||
},
|
||||
{
|
||||
"id": "[TODO: member-a]",
|
||||
"displayName": { "en": "[TODO]", "zh": "[TODO]" },
|
||||
"profession": { "en": "[TODO]", "zh": "[TODO]" },
|
||||
"avatar": "avatars/[TODO: member-a].png",
|
||||
"role": "member"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
AGENT_MD_TEMPLATE = """---
|
||||
name: %(agent_name)s
|
||||
description: "[TODO: English description for AI to determine when to activate]"
|
||||
displayName:
|
||||
en: "[TODO: English display name]"
|
||||
zh: "[TODO: 中文显示名称]"
|
||||
profession:
|
||||
en: "[TODO: English profession title]"
|
||||
zh: "[TODO: 中文职业头衔]"
|
||||
maxTurns: 50
|
||||
---
|
||||
|
||||
# [TODO: 角色名称] - [TODO: 人名]
|
||||
|
||||
[TODO: 角色描述,说明这是谁,擅长什么]
|
||||
|
||||
## 核心能力
|
||||
1. **[TODO: 能力1]**:[TODO: 描述]
|
||||
2. **[TODO: 能力2]**:[TODO: 描述]
|
||||
3. **[TODO: 能力3]**:[TODO: 描述]
|
||||
|
||||
## 工作流程
|
||||
1. [TODO: 步骤1]
|
||||
2. [TODO: 步骤2]
|
||||
3. [TODO: 步骤3]
|
||||
|
||||
## 输出规范
|
||||
- [TODO: 规范1]
|
||||
- [TODO: 规范2]
|
||||
|
||||
## 注意事项
|
||||
- [TODO: 约束或边界条件]
|
||||
"""
|
||||
|
||||
TEAM_LEAD_MD_TEMPLATE = """---
|
||||
name: %(team)s-team-lead
|
||||
description: "[TODO: English description]"
|
||||
displayName:
|
||||
en: "[TODO: English display name]"
|
||||
zh: "[TODO: 中文显示名称]"
|
||||
profession:
|
||||
en: "[TODO: English profession title]"
|
||||
zh: "[TODO: 中文职业头衔]"
|
||||
maxTurns: 150
|
||||
---
|
||||
|
||||
# [TODO: 团队名称] - 主理人
|
||||
|
||||
[TODO: 主理人角色描述,负责协调团队完成什么任务]
|
||||
|
||||
## 团队成员
|
||||
|
||||
| 成员 ID | 名字 | 职责 |
|
||||
|---------|------|------|
|
||||
| %(team)s-team-lead | [TODO] | 编排调度 |
|
||||
| [TODO: member-a] | [TODO] | [TODO: 职责] |
|
||||
|
||||
## 标准工作流程(SOP)
|
||||
|
||||
### Phase 1: [TODO: 阶段名]
|
||||
[TODO: 调用哪些成员、输入输出说明]
|
||||
|
||||
### Phase 2: [TODO: 阶段名]
|
||||
[TODO: ...]
|
||||
|
||||
### Phase N: 最终报告
|
||||
综合所有分析结果,生成最终报告返回用户。
|
||||
|
||||
## 团队协作机制(铁律)
|
||||
|
||||
你必须走正式的**团队协作流程**,严禁简化或跳过:
|
||||
|
||||
1. **建立团队**:任务开始时由主理人亲自创建团队(TeamCreate),明确协作边界。**团队创建必须且只能由主理人执行,严禁委派任何成员创建团队**
|
||||
2. **调度成员**:按 SOP 阶段将成员拉入协作、下发独立任务;成员作为独立协作方输出专业产出,不得由主理人代写
|
||||
3. **消息中转**:成员产出回传给主理人,由主理人汇总、转交下一阶段;所有跨成员信息流必须经主理人中转,不得互相直连
|
||||
4. **成员结论为准**:任何专业产出必须由对应成员输出后再采信,主理人只做编排与汇编
|
||||
|
||||
### 严禁行为
|
||||
- ❌ 禁止跳过 TeamCreate,直接自己模拟成员发言或并行写出多角色内容
|
||||
- ❌ 禁止自己代写任何团队成员的专业产出
|
||||
- ❌ 禁止未完成前序阶段就跳到后续阶段
|
||||
- ❌ 禁止让成员互相直连通信,所有跨成员信息流必须经主理人中转
|
||||
- ❌ 禁止 spawn 主理人自己
|
||||
|
||||
## 协作规则
|
||||
1. 所有成员调度必须经过"建立团队 → 调度成员 → 成员回传"流程
|
||||
2. 每阶段结束后,将完整产出原文传递给下一阶段成员
|
||||
3. 每完成一个阶段向用户简要通报
|
||||
4. 所有输出使用与用户原始需求相同的语言
|
||||
5. 调度成员时,Agent 工具的 `name` 参数传入成员的 **Agent ID**(MD 文件名,不含 .md),`subagent_type` 也传入相同值。禁止使用中文名或自创名称
|
||||
"""
|
||||
|
||||
TEAM_MEMBER_MD_TEMPLATE = """---
|
||||
name: [TODO: member-id]
|
||||
description: "[TODO: English description]"
|
||||
displayName:
|
||||
en: "[TODO: English display name]"
|
||||
zh: "[TODO: 中文显示名称]"
|
||||
profession:
|
||||
en: "[TODO: English profession title]"
|
||||
zh: "[TODO: 中文职业头衔]"
|
||||
maxTurns: 50
|
||||
---
|
||||
|
||||
# [TODO: 角色名称] - [TODO: 人名]
|
||||
|
||||
[TODO: 角色描述]
|
||||
|
||||
## 核心能力
|
||||
1. **[TODO: 能力1]**:[TODO: 描述]
|
||||
2. **[TODO: 能力2]**:[TODO: 描述]
|
||||
3. **[TODO: 能力3]**:[TODO: 描述]
|
||||
|
||||
## 工作流程
|
||||
1. [TODO: 步骤1]
|
||||
2. [TODO: 步骤2]
|
||||
3. [TODO: 步骤3]
|
||||
|
||||
## 输出规范
|
||||
- [TODO: 结构化输出模板]
|
||||
|
||||
## SendMessage 回传
|
||||
分析完成后,**必须通过 SendMessage 将完整分析结果回传给主理人**。
|
||||
"""
|
||||
|
||||
README_TEMPLATE = """# %(title)s
|
||||
|
||||
%(description)s
|
||||
|
||||
## 类型
|
||||
|
||||
%(expert_type)s
|
||||
|
||||
## 功能
|
||||
|
||||
[TODO: 详细功能说明]
|
||||
|
||||
## 使用示例
|
||||
|
||||
- [TODO: 示例提示词1]
|
||||
- [TODO: 示例提示词2]
|
||||
- [TODO: 示例提示词3]
|
||||
|
||||
## 头像
|
||||
|
||||
头像已自动生成在 `avatars/` 目录下。如需替换为自定义头像,要求:
|
||||
- 格式:PNG(推荐)或 JPG
|
||||
- 尺寸:512×512 px
|
||||
- 大小:单张不超过 500KB
|
||||
|
||||
## 安装
|
||||
|
||||
将专家包目录放到专家目录下:
|
||||
|
||||
```
|
||||
%(expert_plugins_dir)s/%(name)s/
|
||||
```
|
||||
|
||||
然后运行注册命令使其可见:
|
||||
|
||||
```bash
|
||||
python3 scripts/register_expert.py <expert-dir>
|
||||
```
|
||||
|
||||
## 打包分享
|
||||
|
||||
```bash
|
||||
zip -r %(name)s.zip %(name)s/
|
||||
```
|
||||
"""
|
||||
|
||||
SETTINGS_JSON = '{\n "agent": "%(team)s-team-lead"\n}\n'
|
||||
|
||||
|
||||
def init_agent(expert_dir, name):
|
||||
"""Initialize an Agent-type expert package."""
|
||||
agent_name = name
|
||||
|
||||
# .codebuddy-plugin/plugin.json
|
||||
plugin_dir = expert_dir / '.codebuddy-plugin'
|
||||
plugin_dir.mkdir()
|
||||
(plugin_dir / 'plugin.json').write_text(
|
||||
AGENT_PLUGIN_JSON % {'name': name, 'agent_name': agent_name},
|
||||
encoding='utf-8'
|
||||
)
|
||||
print(" ✅ .codebuddy-plugin/plugin.json")
|
||||
|
||||
# agents/
|
||||
agents_dir = expert_dir / 'agents'
|
||||
agents_dir.mkdir()
|
||||
(agents_dir / f'{agent_name}.md').write_text(
|
||||
AGENT_MD_TEMPLATE % {'agent_name': agent_name},
|
||||
encoding='utf-8'
|
||||
)
|
||||
print(f" ✅ agents/{agent_name}.md")
|
||||
|
||||
# avatars/
|
||||
avatars_dir = expert_dir / 'avatars'
|
||||
avatars_dir.mkdir()
|
||||
(avatars_dir / '.gitkeep').touch()
|
||||
print(" ✅ avatars/ (awaiting ImageGen)")
|
||||
|
||||
# README.md
|
||||
(expert_dir / 'README.md').write_text(
|
||||
README_TEMPLATE % {
|
||||
'title': title_case(name),
|
||||
'description': '[TODO: 一句话描述]',
|
||||
'expert_type': 'Agent 型(单个 AI 专家)',
|
||||
'name': name,
|
||||
'expert_plugins_dir': str(get_expert_plugins_dir()),
|
||||
},
|
||||
encoding='utf-8'
|
||||
)
|
||||
print(" ✅ README.md")
|
||||
|
||||
|
||||
def init_team(expert_dir, name):
|
||||
"""Initialize a Team-type expert package."""
|
||||
team = name
|
||||
|
||||
# .codebuddy-plugin/plugin.json
|
||||
plugin_dir = expert_dir / '.codebuddy-plugin'
|
||||
plugin_dir.mkdir()
|
||||
(plugin_dir / 'plugin.json').write_text(
|
||||
TEAM_PLUGIN_JSON % {'name': name, 'team': team},
|
||||
encoding='utf-8'
|
||||
)
|
||||
print(" ✅ .codebuddy-plugin/plugin.json")
|
||||
|
||||
# agents/
|
||||
agents_dir = expert_dir / 'agents'
|
||||
agents_dir.mkdir()
|
||||
(agents_dir / f'{team}-team-lead.md').write_text(
|
||||
TEAM_LEAD_MD_TEMPLATE % {'team': team},
|
||||
encoding='utf-8'
|
||||
)
|
||||
print(f" ✅ agents/{team}-team-lead.md")
|
||||
|
||||
(agents_dir / 'member-placeholder.md').write_text(TEAM_MEMBER_MD_TEMPLATE, encoding='utf-8')
|
||||
print(" ✅ agents/member-placeholder.md (rename to actual member ID)")
|
||||
|
||||
# avatars/
|
||||
avatars_dir = expert_dir / 'avatars'
|
||||
avatars_dir.mkdir()
|
||||
(avatars_dir / '.gitkeep').touch()
|
||||
print(" ✅ avatars/ (awaiting ImageGen)")
|
||||
|
||||
# settings.json
|
||||
(expert_dir / 'settings.json').write_text(
|
||||
SETTINGS_JSON % {'team': team},
|
||||
encoding='utf-8'
|
||||
)
|
||||
print(" ✅ settings.json")
|
||||
|
||||
# README.md
|
||||
(expert_dir / 'README.md').write_text(
|
||||
README_TEMPLATE % {
|
||||
'title': title_case(name),
|
||||
'description': '[TODO: 一句话描述]',
|
||||
'expert_type': 'Team 型(多角色协作团队)',
|
||||
'name': name,
|
||||
'expert_plugins_dir': str(get_expert_plugins_dir()),
|
||||
},
|
||||
encoding='utf-8'
|
||||
)
|
||||
print(" ✅ README.md")
|
||||
|
||||
|
||||
def main():
|
||||
# Parse arguments
|
||||
if len(sys.argv) < 6:
|
||||
print("Usage: init_expert.py <expert-name> --type agent|team --path <output-dir>")
|
||||
print("\nExamples:")
|
||||
print(" python3 init_expert.py my-expert --type agent --path ./output")
|
||||
print(" python3 init_expert.py my-team --type team --path ./output")
|
||||
sys.exit(1)
|
||||
|
||||
name = sys.argv[1]
|
||||
expert_type = None
|
||||
output_path = None
|
||||
|
||||
i = 2
|
||||
while i < len(sys.argv):
|
||||
if sys.argv[i] == '--type' and i + 1 < len(sys.argv):
|
||||
expert_type = sys.argv[i + 1]
|
||||
i += 2
|
||||
elif sys.argv[i] == '--path' and i + 1 < len(sys.argv):
|
||||
output_path = sys.argv[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
|
||||
if not expert_type or expert_type not in ('agent', 'team'):
|
||||
print("❌ Error: --type must be one of: agent, team")
|
||||
sys.exit(1)
|
||||
|
||||
if not output_path:
|
||||
print("❌ Error: --path is required")
|
||||
sys.exit(1)
|
||||
|
||||
# 硬检查:--path 必须是专家目录
|
||||
expected_path = get_expert_plugins_dir()
|
||||
resolved_path = Path(output_path).expanduser().resolve()
|
||||
if resolved_path != expected_path.resolve():
|
||||
print(f"❌ Error: --path 必须为专家目录: {expected_path}")
|
||||
print(f" 当前传入: {resolved_path}")
|
||||
print(f" 非专家目录下创建的专家无法被检测到。")
|
||||
sys.exit(1)
|
||||
|
||||
# Validate name
|
||||
if len(name) < 2 or not re.match(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$', name):
|
||||
print(f"❌ Error: name '{name}' must be kebab-case (lowercase letters, digits, hyphens)")
|
||||
sys.exit(1)
|
||||
|
||||
# Create expert directory
|
||||
expert_dir = Path(output_path).resolve() / name
|
||||
if expert_dir.exists():
|
||||
print(f"❌ Error: directory already exists: {expert_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
expert_dir.mkdir(parents=True)
|
||||
print(f"🚀 Initializing {expert_type} expert: {name}")
|
||||
print(f" Location: {expert_dir}\n")
|
||||
|
||||
if expert_type == 'agent':
|
||||
init_agent(expert_dir, name)
|
||||
elif expert_type == 'team':
|
||||
init_team(expert_dir, name)
|
||||
|
||||
print(f"\n✅ Expert '{name}' ({expert_type}) initialized at {expert_dir}")
|
||||
print("\nNext steps:")
|
||||
print(" 1. Fill in all [TODO] placeholders in generated files")
|
||||
print(" 2. Generate avatars using ImageGen (see references/avatar-spec.md)")
|
||||
print(" 3. Run validate_expert.py to check the package")
|
||||
print(" 4. Run register_expert.py to register in marketplace.json (makes it visible in WorkBuddy)")
|
||||
print(f"\n Note: The expert will NOT appear in WorkBuddy until registered.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Expert Packager - Validates and packages an expert directory into a zip file.
|
||||
|
||||
Usage:
|
||||
package_expert.py <path/to/expert-dir> [output-dir]
|
||||
|
||||
Example:
|
||||
python3 package_expert.py plugins/my-expert
|
||||
python3 package_expert.py plugins/my-expert ./dist
|
||||
"""
|
||||
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
# Import validate_expert from sibling script
|
||||
script_dir = Path(__file__).parent
|
||||
sys.path.insert(0, str(script_dir))
|
||||
from validate_expert import validate_expert
|
||||
|
||||
|
||||
def package_expert(expert_path, output_dir=None):
|
||||
"""
|
||||
Validate and package an expert directory into a .zip file.
|
||||
|
||||
Args:
|
||||
expert_path: Path to the expert directory
|
||||
output_dir: Optional output directory (defaults to current directory)
|
||||
|
||||
Returns:
|
||||
Path to the created zip file, or None if error
|
||||
"""
|
||||
expert_path = Path(expert_path).resolve()
|
||||
|
||||
if not expert_path.exists():
|
||||
print(f"❌ Error: Expert directory not found: {expert_path}")
|
||||
return None
|
||||
|
||||
if not expert_path.is_dir():
|
||||
print(f"❌ Error: Path is not a directory: {expert_path}")
|
||||
return None
|
||||
|
||||
# Validate first
|
||||
print("🔍 Validating expert package...\n")
|
||||
result = validate_expert(expert_path)
|
||||
print(result.summary())
|
||||
|
||||
if not result.is_valid:
|
||||
print("\n❌ Packaging aborted. Please fix validation errors first.")
|
||||
return None
|
||||
|
||||
print()
|
||||
|
||||
# Determine output location
|
||||
expert_name = expert_path.name
|
||||
if output_dir:
|
||||
out = Path(output_dir).resolve()
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
out = Path.cwd()
|
||||
|
||||
zip_filename = out / f"{expert_name}.zip"
|
||||
|
||||
# Create the zip file
|
||||
try:
|
||||
file_count = 0
|
||||
with zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for file_path in sorted(expert_path.rglob('*')):
|
||||
if file_path.is_file():
|
||||
# Skip hidden files except .codebuddy-plugin
|
||||
rel = file_path.relative_to(expert_path)
|
||||
parts = rel.parts
|
||||
if any(p.startswith('.') and p != '.codebuddy-plugin' for p in parts):
|
||||
continue
|
||||
# Skip common junk files/directories
|
||||
if any(p in ('__pycache__', 'node_modules') for p in parts):
|
||||
continue
|
||||
if file_path.name in ('.gitkeep', '.DS_Store', 'Thumbs.db'):
|
||||
continue
|
||||
|
||||
arcname = str(Path(expert_name) / rel)
|
||||
zipf.write(file_path, arcname)
|
||||
print(f" 📄 {arcname}")
|
||||
file_count += 1
|
||||
|
||||
print(f"\n✅ Packaged {file_count} files to: {zip_filename}")
|
||||
print(f" Size: {zip_filename.stat().st_size / 1024:.1f} KB")
|
||||
return zip_filename
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating zip: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 package_expert.py <path/to/expert-dir> [output-dir]")
|
||||
print("\nExample:")
|
||||
print(" python3 package_expert.py plugins/my-expert")
|
||||
print(" python3 package_expert.py plugins/my-expert ./dist")
|
||||
sys.exit(1)
|
||||
|
||||
expert_path = sys.argv[1]
|
||||
output_dir = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
|
||||
print(f"📦 Packaging expert: {expert_path}")
|
||||
if output_dir:
|
||||
print(f" Output directory: {output_dir}")
|
||||
print()
|
||||
|
||||
result = package_expert(expert_path, output_dir)
|
||||
|
||||
if result:
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Expert Register - Validates and registers an expert into marketplace.json
|
||||
|
||||
This script should be called AFTER the expert package is fully created and validated.
|
||||
Only registered experts will be visible in the WorkBuddy expert center.
|
||||
|
||||
Usage:
|
||||
register_expert.py <expert-dir> [--marketplace-dir <marketplace-dir>]
|
||||
|
||||
Examples:
|
||||
python3 register_expert.py <expert-dir>
|
||||
python3 register_expert.py ./my-expert --marketplace-dir <marketplace-dir>
|
||||
|
||||
The creating session id is read from CODEBUDDY_SESSION_ID when present.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
SESSION_ID_ENV_VAR = 'CODEBUDDY_SESSION_ID'
|
||||
UNEXPANDED_SESSION_ID_ARGS = {'$CODEBUDDY_SESSION_ID', '${CODEBUDDY_SESSION_ID}'}
|
||||
|
||||
|
||||
def get_expert_base_dir():
|
||||
"""获取专家 marketplace 根目录,优先读取 WORKBUDDY_CONFIG_DIR 环境变量。"""
|
||||
config_dir = os.environ.get('WORKBUDDY_CONFIG_DIR', '').strip()
|
||||
if not config_dir:
|
||||
config_dir = str(Path.home() / '.workbuddy')
|
||||
return Path(config_dir) / 'plugins' / 'marketplaces' / 'my-experts'
|
||||
|
||||
|
||||
def find_plugin_json(expert_dir: Path) -> Optional[Path]:
|
||||
"""Find plugin.json in the expert directory."""
|
||||
for meta_dir in ['.codebuddy-plugin', '.workbuddy-plugin']:
|
||||
candidate = expert_dir / meta_dir / 'plugin.json'
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def validate_expert_completeness(expert_dir: Path) -> Tuple[bool, List[str]]:
|
||||
"""
|
||||
Validate that the expert package is complete and ready for registration.
|
||||
Returns (is_valid, list_of_errors).
|
||||
"""
|
||||
errors = []
|
||||
|
||||
# 1. Check plugin.json exists
|
||||
plugin_json_path = find_plugin_json(expert_dir)
|
||||
if not plugin_json_path:
|
||||
errors.append("Missing plugin.json (checked .codebuddy-plugin/ and .workbuddy-plugin/)")
|
||||
return False, errors
|
||||
|
||||
# 2. Parse plugin.json
|
||||
try:
|
||||
with open(plugin_json_path, 'r', encoding='utf-8') as f:
|
||||
plugin_data = json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
errors.append(f"plugin.json is not valid JSON: {e}")
|
||||
return False, errors
|
||||
|
||||
# 3. Check required fields are not [TODO]
|
||||
required_fields = ['name', 'description', 'expertType']
|
||||
for field in required_fields:
|
||||
value = plugin_data.get(field, '')
|
||||
if not value or '[TODO' in str(value):
|
||||
errors.append(f"Field '{field}' is missing or contains [TODO] placeholder")
|
||||
|
||||
# 4. Check displayName is filled
|
||||
display_name = plugin_data.get('displayName', {})
|
||||
if isinstance(display_name, dict):
|
||||
zh_name = display_name.get('zh', '')
|
||||
en_name = display_name.get('en', '')
|
||||
if (not zh_name or '[TODO' in zh_name) and (not en_name or '[TODO' in en_name):
|
||||
errors.append("displayName (zh or en) is missing or contains [TODO]")
|
||||
elif not display_name or '[TODO' in str(display_name):
|
||||
errors.append("displayName is missing or contains [TODO]")
|
||||
|
||||
# 5. Check agents directory has at least one non-template .md file
|
||||
agents_dir = expert_dir / 'agents'
|
||||
if agents_dir.exists():
|
||||
md_files = list(agents_dir.glob('*.md'))
|
||||
if not md_files:
|
||||
errors.append("No .md files found in agents/ directory")
|
||||
else:
|
||||
# Check at least one agent file has real content (not all [TODO])
|
||||
has_real_content = False
|
||||
for md_file in md_files:
|
||||
content = md_file.read_text(encoding='utf-8')
|
||||
# Count [TODO] occurrences - if more than 3, likely still a template
|
||||
todo_count = content.count('[TODO')
|
||||
if todo_count <= 2:
|
||||
has_real_content = True
|
||||
break
|
||||
if not has_real_content:
|
||||
errors.append("All agent .md files still contain many [TODO] placeholders")
|
||||
else:
|
||||
errors.append("Missing agents/ directory")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
|
||||
def _write_session_marker(expert_dir: Path, session_id: str) -> None:
|
||||
"""Write .created-by-session marker file into the expert directory."""
|
||||
marker_path = expert_dir / '.created-by-session'
|
||||
try:
|
||||
marker_path.write_text(session_id, encoding='utf-8')
|
||||
except OSError as e:
|
||||
print(f"⚠️ Warning: could not write session marker: {e}")
|
||||
|
||||
|
||||
def normalize_session_id(value: Optional[str]) -> Optional[str]:
|
||||
"""Return a usable session id, ignoring empty or unexpanded shell placeholders."""
|
||||
session_id = (value or '').strip()
|
||||
if not session_id or session_id in UNEXPANDED_SESSION_ID_ARGS:
|
||||
return None
|
||||
return session_id
|
||||
|
||||
|
||||
def get_session_id(explicit_session_id: Optional[str]) -> Optional[str]:
|
||||
"""Resolve the creating session id from CLI arg or Bash runtime env."""
|
||||
return normalize_session_id(explicit_session_id) or normalize_session_id(os.environ.get(SESSION_ID_ENV_VAR))
|
||||
|
||||
|
||||
def get_marketplace_dir(expert_dir: Path, explicit_marketplace_dir: Optional[str]) -> Path:
|
||||
"""Determine the marketplace directory."""
|
||||
if explicit_marketplace_dir:
|
||||
return Path(explicit_marketplace_dir).expanduser().resolve()
|
||||
|
||||
# Infer from expert_dir path: .../marketplaces/my-experts/plugins/<name>
|
||||
# -> marketplace dir is .../marketplaces/my-experts/
|
||||
parts = expert_dir.parts
|
||||
try:
|
||||
plugins_idx = len(parts) - 2 # parent of expert dir should be "plugins"
|
||||
if parts[plugins_idx] == 'plugins':
|
||||
return Path(*parts[:plugins_idx])
|
||||
except (IndexError, ValueError):
|
||||
pass
|
||||
|
||||
# Fallback to default
|
||||
return get_expert_base_dir()
|
||||
|
||||
|
||||
def register_expert(expert_dir: Path, marketplace_dir: Path, session_id: Optional[str] = None) -> bool:
|
||||
"""Register the expert in marketplace.json."""
|
||||
manifest_path = marketplace_dir / '.codebuddy-plugin' / 'marketplace.json'
|
||||
|
||||
# Read existing manifest or create new one
|
||||
if manifest_path.exists():
|
||||
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||
manifest = json.load(f)
|
||||
else:
|
||||
# Create marketplace directory structure if needed
|
||||
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
manifest = {
|
||||
"name": "my-experts",
|
||||
"description": "my-experts marketplace (auto-generated)",
|
||||
"plugins": []
|
||||
}
|
||||
|
||||
# Read expert plugin.json for metadata
|
||||
plugin_json_path = find_plugin_json(expert_dir)
|
||||
if not plugin_json_path:
|
||||
print(f"❌ Error: plugin.json not found in {expert_dir}")
|
||||
return False
|
||||
with open(plugin_json_path, 'r', encoding='utf-8') as f:
|
||||
plugin_data = json.load(f)
|
||||
|
||||
expert_name = plugin_data.get('name', expert_dir.name)
|
||||
expert_description = plugin_data.get('description', '')
|
||||
source = f"./plugins/{expert_dir.name}"
|
||||
|
||||
# Check if already registered
|
||||
plugins = manifest.get('plugins', [])
|
||||
for p in plugins:
|
||||
if p.get('source') == source or p.get('name') == expert_name:
|
||||
# Update existing entry
|
||||
p['name'] = expert_name
|
||||
p['source'] = source
|
||||
p['description'] = expert_description
|
||||
if session_id:
|
||||
_write_session_marker(expert_dir, session_id)
|
||||
print(f"✅ Updated existing registration for '{expert_name}' in marketplace.json")
|
||||
with open(manifest_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(manifest, f, indent=2, ensure_ascii=False)
|
||||
return True
|
||||
|
||||
# Add new entry
|
||||
plugins.append({
|
||||
"name": expert_name,
|
||||
"source": source,
|
||||
"description": expert_description
|
||||
})
|
||||
manifest['plugins'] = plugins
|
||||
|
||||
if session_id:
|
||||
_write_session_marker(expert_dir, session_id)
|
||||
|
||||
with open(manifest_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(manifest, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"✅ Registered '{expert_name}' in marketplace.json")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: register_expert.py <expert-dir> [--marketplace-dir <marketplace-dir>]")
|
||||
print("\nExamples:")
|
||||
print(" python3 register_expert.py <expert-dir>")
|
||||
print(" python3 register_expert.py ./my-expert --marketplace-dir <marketplace-dir>")
|
||||
sys.exit(1)
|
||||
|
||||
expert_dir = Path(sys.argv[1]).expanduser().resolve()
|
||||
marketplace_dir_arg = None
|
||||
session_id_arg = None
|
||||
|
||||
i = 2
|
||||
while i < len(sys.argv):
|
||||
if sys.argv[i] == '--marketplace-dir' and i + 1 < len(sys.argv):
|
||||
marketplace_dir_arg = sys.argv[i + 1]
|
||||
i += 2
|
||||
elif sys.argv[i] == '--session-id' and i + 1 < len(sys.argv):
|
||||
session_id_arg = sys.argv[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
|
||||
# Validate expert directory exists
|
||||
if not expert_dir.exists():
|
||||
print(f"❌ Error: expert directory does not exist: {expert_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
# Validate completeness
|
||||
print(f"🔍 Validating expert package: {expert_dir.name}")
|
||||
is_valid, errors = validate_expert_completeness(expert_dir)
|
||||
|
||||
if not is_valid:
|
||||
print(f"\n❌ Expert package is incomplete. Cannot register.")
|
||||
print(" Issues found:")
|
||||
for err in errors:
|
||||
print(f" - {err}")
|
||||
print("\n Please fix the above issues and try again.")
|
||||
sys.exit(1)
|
||||
|
||||
print(" ✅ Validation passed")
|
||||
|
||||
# Determine marketplace directory
|
||||
marketplace_dir = get_marketplace_dir(expert_dir, marketplace_dir_arg)
|
||||
print(f"📋 Marketplace: {marketplace_dir}")
|
||||
|
||||
session_id = get_session_id(session_id_arg)
|
||||
|
||||
# Register
|
||||
success = register_expert(expert_dir, marketplace_dir, session_id)
|
||||
if success and session_id:
|
||||
print(f"\n🎉 Expert '{expert_dir.name}' is now registered and visible in WorkBuddy!")
|
||||
elif success:
|
||||
print(f"\n✅ Expert '{expert_dir.name}' is registered in marketplace.json.")
|
||||
print("Warning: no session id found; it may not appear in My Experts for the current user automatically.")
|
||||
else:
|
||||
print(f"\n❌ Registration failed")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Expert Validator - Validates an expert package against the WorkBuddy specification.
|
||||
|
||||
Usage:
|
||||
validate_expert.py <path/to/expert-dir>
|
||||
|
||||
Example:
|
||||
python3 validate_expert.py plugins/my-expert
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
VALID_CATEGORY_IDS = {
|
||||
'01-ProductDesign', '02-Engineering', '03-GameSpatial', '04-DataAI',
|
||||
'05-MarketingGrowth', '06-ContentCreative', '07-SalesCommerce',
|
||||
'08-FinanceInvestment', '09-OperationsHR', '10-ProjectQuality',
|
||||
'11-SecurityCompliance', '12-IndustryConsultant',
|
||||
}
|
||||
|
||||
VALID_EXPERT_TYPES = {'agent', 'team'}
|
||||
|
||||
|
||||
class ValidationResult:
|
||||
def __init__(self):
|
||||
self.errors = []
|
||||
self.warnings = []
|
||||
|
||||
def error(self, msg):
|
||||
self.errors.append(msg)
|
||||
|
||||
def warn(self, msg):
|
||||
self.warnings.append(msg)
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return len(self.errors) == 0
|
||||
|
||||
def summary(self):
|
||||
lines = []
|
||||
if self.errors:
|
||||
lines.append(f"❌ {len(self.errors)} error(s):")
|
||||
for e in self.errors:
|
||||
lines.append(f" • {e}")
|
||||
if self.warnings:
|
||||
lines.append(f"⚠️ {len(self.warnings)} warning(s):")
|
||||
for w in self.warnings:
|
||||
lines.append(f" • {w}")
|
||||
if self.is_valid:
|
||||
lines.append("✅ Expert package is valid!")
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def parse_md_frontmatter(md_path):
|
||||
"""Parse YAML frontmatter from a Markdown file. Returns dict or None."""
|
||||
try:
|
||||
content = md_path.read_text(encoding='utf-8')
|
||||
except Exception as e:
|
||||
return None, f"Cannot read {md_path}: {e}"
|
||||
|
||||
if not content.startswith('---'):
|
||||
return None, f"{md_path.name}: No YAML frontmatter found"
|
||||
|
||||
match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
|
||||
if not match:
|
||||
return None, f"{md_path.name}: Invalid frontmatter format"
|
||||
|
||||
# LIMITATION: Only parses simple top-level "key: value" pairs.
|
||||
# Does NOT handle nested objects, lists, or multi-line values.
|
||||
# Sufficient for extracting 'name', 'description', and detecting 'tools'.
|
||||
fm_text = match.group(1)
|
||||
result = {}
|
||||
|
||||
# Extract simple key-value pairs
|
||||
for line in fm_text.split('\n'):
|
||||
line = line.strip()
|
||||
if ':' in line and not line.startswith('-') and not line.startswith('#'):
|
||||
key, _, value = line.partition(':')
|
||||
key = key.strip()
|
||||
value = value.strip().strip('"').strip("'")
|
||||
if value:
|
||||
result[key] = value
|
||||
|
||||
return result, None
|
||||
|
||||
|
||||
def check_i18n_field(obj, field_name, result, context="plugin.json"):
|
||||
"""Check that a field has both 'en' and 'zh' values."""
|
||||
if field_name not in obj:
|
||||
result.error(f"{context}: missing '{field_name}'")
|
||||
return False
|
||||
|
||||
val = obj[field_name]
|
||||
if not isinstance(val, dict):
|
||||
result.error(f"{context}: '{field_name}' must be an object with 'en' and 'zh'")
|
||||
return False
|
||||
|
||||
if not val.get('en'):
|
||||
result.error(f"{context}: '{field_name}.en' is empty")
|
||||
if not val.get('zh'):
|
||||
result.error(f"{context}: '{field_name}.zh' is empty")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def validate_plugin_json(plugin_json, expert_dir, result):
|
||||
"""Validate plugin.json fields."""
|
||||
# Required fields
|
||||
for field in ('name', 'version', 'description'):
|
||||
if field not in plugin_json:
|
||||
result.error(f"plugin.json: missing required field '{field}'")
|
||||
|
||||
name = plugin_json.get('name', '')
|
||||
if name and (len(name) < 2 or not re.match(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$', name)):
|
||||
result.error(f"plugin.json: 'name' must be kebab-case (at least 2 chars), got '{name}'")
|
||||
|
||||
expert_type = plugin_json.get('expertType', '')
|
||||
if expert_type not in VALID_EXPERT_TYPES:
|
||||
result.error(f"plugin.json: 'expertType' must be one of {VALID_EXPERT_TYPES}, got '{expert_type}'")
|
||||
return expert_type
|
||||
|
||||
# plugin field consistency
|
||||
plugin_field = plugin_json.get('plugin', '')
|
||||
if plugin_field and plugin_field != name:
|
||||
result.error(f"plugin.json: 'plugin' ({plugin_field}) must equal 'name' ({name})")
|
||||
|
||||
# Agent/Team specific
|
||||
if expert_type in ('agent', 'team'):
|
||||
agent_name = plugin_json.get('agentName', '')
|
||||
if not agent_name:
|
||||
result.error("plugin.json: missing 'agentName'")
|
||||
else:
|
||||
# Check agentName matches an MD file
|
||||
md_file = expert_dir / 'agents' / f'{agent_name}.md'
|
||||
if not md_file.exists():
|
||||
result.error(f"plugin.json: agentName '{agent_name}' has no matching file agents/{agent_name}.md")
|
||||
|
||||
# agentName must not be generic "team-lead"
|
||||
if agent_name == 'team-lead':
|
||||
result.error("plugin.json: agentName must not be generic 'team-lead', add team prefix")
|
||||
|
||||
# Display fields
|
||||
check_i18n_field(plugin_json, 'displayName', result)
|
||||
check_i18n_field(plugin_json, 'profession', result)
|
||||
check_i18n_field(plugin_json, 'displayDescription', result)
|
||||
check_i18n_field(plugin_json, 'defaultInitPrompt', result)
|
||||
|
||||
# displayDescription zh length
|
||||
dd = plugin_json.get('displayDescription', {})
|
||||
zh_desc = dd.get('zh', '')
|
||||
if zh_desc and not zh_desc.startswith('[TODO'):
|
||||
zh_len = len(zh_desc)
|
||||
if zh_len < 40 or zh_len > 50:
|
||||
result.warn(f"plugin.json: displayDescription.zh is {zh_len} chars (recommended 40-50)")
|
||||
|
||||
# Team profession == displayName
|
||||
if expert_type == 'team':
|
||||
dn = plugin_json.get('displayName', {})
|
||||
prof = plugin_json.get('profession', {})
|
||||
if dn.get('zh') and prof.get('zh') and dn['zh'] != prof['zh']:
|
||||
if not dn['zh'].startswith('[TODO') and not prof['zh'].startswith('[TODO'):
|
||||
result.error(f"plugin.json: Team 'profession' must equal 'displayName' (zh: '{prof['zh']}' != '{dn['zh']}')")
|
||||
|
||||
# categoryId
|
||||
cat = plugin_json.get('categoryId', '')
|
||||
if cat and not cat.startswith('[TODO') and cat not in VALID_CATEGORY_IDS:
|
||||
result.error(f"plugin.json: invalid categoryId '{cat}'")
|
||||
|
||||
# tags (must be 3)
|
||||
tags = plugin_json.get('tags', [])
|
||||
if isinstance(tags, list) and len(tags) != 3:
|
||||
result.error(f"plugin.json: must have exactly 3 tags, got {len(tags)}")
|
||||
|
||||
# quickPrompts (must be 3)
|
||||
qp = plugin_json.get('quickPrompts', [])
|
||||
if isinstance(qp, list) and len(qp) != 3:
|
||||
result.error(f"plugin.json: must have exactly 3 quickPrompts, got {len(qp)}")
|
||||
|
||||
# defaultInitPrompt == quickPrompts[0]
|
||||
dip = plugin_json.get('defaultInitPrompt', {})
|
||||
if isinstance(qp, list) and len(qp) > 0 and isinstance(qp[0], dict):
|
||||
if dip.get('zh') and qp[0].get('zh') and dip['zh'] != qp[0]['zh']:
|
||||
if not dip['zh'].startswith('[TODO'):
|
||||
result.warn("plugin.json: defaultInitPrompt.zh should match quickPrompts[0].zh")
|
||||
|
||||
# avatar path check
|
||||
avatar = plugin_json.get('avatar', '')
|
||||
if avatar:
|
||||
avatar_path = expert_dir / avatar
|
||||
if not avatar_path.exists() and not (expert_dir / 'avatars' / '.gitkeep').exists():
|
||||
result.warn(f"plugin.json: avatar file not found: {avatar}")
|
||||
|
||||
# agents array check
|
||||
agents_arr = plugin_json.get('agents', [])
|
||||
if isinstance(agents_arr, list):
|
||||
for agent_path in agents_arr:
|
||||
rel = agent_path[2:] if agent_path.startswith('./') else agent_path
|
||||
resolved = expert_dir / rel
|
||||
if not resolved.exists() and not agent_path.startswith('[TODO'):
|
||||
result.error(f"plugin.json: agents path not found: {agent_path}")
|
||||
|
||||
# skills array check
|
||||
skills_arr = plugin_json.get('skills', [])
|
||||
if isinstance(skills_arr, list):
|
||||
for skill_path in skills_arr:
|
||||
rel = skill_path[2:] if skill_path.startswith('./') else skill_path
|
||||
resolved = expert_dir / rel
|
||||
skill_md = resolved / 'SKILL.md'
|
||||
if not skill_md.exists() and not skill_path.startswith('[TODO'):
|
||||
result.error(f"plugin.json: skill path has no SKILL.md: {skill_path}")
|
||||
|
||||
# Team specific
|
||||
if expert_type == 'team':
|
||||
team_info = plugin_json.get('teamInfo', {})
|
||||
if not team_info:
|
||||
result.error("plugin.json: Team type must have 'teamInfo'")
|
||||
else:
|
||||
lead = team_info.get('leadAgent', '')
|
||||
members_agents = team_info.get('memberAgents', [])
|
||||
if lead and lead in members_agents:
|
||||
result.error("plugin.json: teamInfo.memberAgents should NOT contain the lead agent")
|
||||
|
||||
members = plugin_json.get('members', [])
|
||||
if not members:
|
||||
result.error("plugin.json: Team type must have 'members' array")
|
||||
else:
|
||||
has_lead = any(m.get('role') == 'lead' for m in members if isinstance(m, dict))
|
||||
if not has_lead:
|
||||
result.error("plugin.json: members array must contain at least one member with role='lead'")
|
||||
|
||||
# settings.json
|
||||
settings_path = expert_dir / 'settings.json'
|
||||
if not settings_path.exists():
|
||||
result.error("Team type must have settings.json")
|
||||
else:
|
||||
try:
|
||||
settings = json.loads(settings_path.read_text(encoding='utf-8'))
|
||||
settings_agent = settings.get('agent', '')
|
||||
pj_agent = plugin_json.get('agentName', '')
|
||||
if settings_agent and pj_agent and settings_agent != pj_agent:
|
||||
result.error(f"settings.json 'agent' ({settings_agent}) != plugin.json 'agentName' ({pj_agent})")
|
||||
except json.JSONDecodeError as e:
|
||||
result.error(f"settings.json: invalid JSON: {e}")
|
||||
|
||||
return expert_type
|
||||
|
||||
|
||||
def validate_agent_mds(expert_dir, plugin_json, result):
|
||||
"""Validate all Agent MD files."""
|
||||
agents_dir = expert_dir / 'agents'
|
||||
if not agents_dir.exists():
|
||||
result.error("agents/ directory not found")
|
||||
return
|
||||
|
||||
for md_file in agents_dir.glob('*.md'):
|
||||
fm, err = parse_md_frontmatter(md_file)
|
||||
if err:
|
||||
result.error(err)
|
||||
continue
|
||||
|
||||
if not fm:
|
||||
result.error(f"{md_file.name}: empty frontmatter")
|
||||
continue
|
||||
|
||||
# name must match filename
|
||||
fm_name = fm.get('name', '')
|
||||
expected_name = md_file.stem
|
||||
if fm_name and fm_name != expected_name:
|
||||
result.error(f"{md_file.name}: frontmatter name '{fm_name}' != filename '{expected_name}'")
|
||||
|
||||
# Must have description
|
||||
if not fm.get('description'):
|
||||
result.warn(f"{md_file.name}: missing 'description' in frontmatter")
|
||||
|
||||
# Must NOT have tools field
|
||||
content = md_file.read_text(encoding='utf-8')
|
||||
fm_section = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
|
||||
if fm_section and 'tools:' in fm_section.group(1):
|
||||
result.error(f"{md_file.name}: frontmatter must NOT contain 'tools' field")
|
||||
|
||||
|
||||
def validate_file_structure(expert_dir, expert_type, result):
|
||||
"""Validate basic file structure."""
|
||||
# .codebuddy-plugin/plugin.json must exist
|
||||
pj = expert_dir / '.codebuddy-plugin' / 'plugin.json'
|
||||
if not pj.exists():
|
||||
result.error(".codebuddy-plugin/plugin.json not found")
|
||||
|
||||
# Must NOT have hooks/, commands/, .lsp.json
|
||||
for forbidden in ('hooks', 'commands'):
|
||||
if (expert_dir / forbidden).exists():
|
||||
result.error(f"Forbidden directory found: {forbidden}/")
|
||||
if (expert_dir / '.lsp.json').exists():
|
||||
result.error("Forbidden file found: .lsp.json")
|
||||
|
||||
# agents/, skills/, bin/ must be at root, not inside .codebuddy-plugin/
|
||||
cb_dir = expert_dir / '.codebuddy-plugin'
|
||||
for subdir in ('agents', 'skills', 'bin', 'avatars'):
|
||||
if (cb_dir / subdir).exists():
|
||||
result.error(f"{subdir}/ found inside .codebuddy-plugin/ — must be at plugin root")
|
||||
|
||||
# Must have agents/ dir
|
||||
if not (expert_dir / 'agents').exists():
|
||||
result.error(f"{expert_type} type must have agents/ directory")
|
||||
|
||||
# README.md recommended
|
||||
if not (expert_dir / 'README.md').exists():
|
||||
result.warn("README.md is recommended")
|
||||
|
||||
|
||||
def validate_expert(expert_path):
|
||||
"""Main validation entry point."""
|
||||
expert_dir = Path(expert_path).resolve()
|
||||
result = ValidationResult()
|
||||
|
||||
if not expert_dir.exists():
|
||||
result.error(f"Directory not found: {expert_dir}")
|
||||
return result
|
||||
|
||||
if not expert_dir.is_dir():
|
||||
result.error(f"Not a directory: {expert_dir}")
|
||||
return result
|
||||
|
||||
# Check if expert is in the default install path
|
||||
config_dir = os.environ.get('WORKBUDDY_CONFIG_DIR', '').strip() or str(Path.home() / '.workbuddy')
|
||||
default_plugins_dir = Path(config_dir) / 'plugins' / 'marketplaces' / 'my-experts' / 'plugins'
|
||||
try:
|
||||
expert_dir.relative_to(default_plugins_dir.resolve())
|
||||
except ValueError:
|
||||
result.error(
|
||||
f"专家不在专家目录下,将无法被检测到。"
|
||||
f" 专家目录: {default_plugins_dir}"
|
||||
)
|
||||
|
||||
# Load plugin.json
|
||||
pj_path = expert_dir / '.codebuddy-plugin' / 'plugin.json'
|
||||
if not pj_path.exists():
|
||||
result.error(".codebuddy-plugin/plugin.json not found")
|
||||
return result
|
||||
|
||||
try:
|
||||
plugin_json = json.loads(pj_path.read_text(encoding='utf-8'))
|
||||
except json.JSONDecodeError as e:
|
||||
result.error(f"plugin.json: invalid JSON: {e}")
|
||||
return result
|
||||
|
||||
# Validate plugin.json
|
||||
expert_type = validate_plugin_json(plugin_json, expert_dir, result)
|
||||
|
||||
# Validate file structure
|
||||
validate_file_structure(expert_dir, expert_type, result)
|
||||
|
||||
# Validate Agent MDs
|
||||
validate_agent_mds(expert_dir, plugin_json, result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python3 validate_expert.py <path/to/expert-dir>")
|
||||
print("\nExample:")
|
||||
print(" python3 validate_expert.py plugins/my-expert")
|
||||
sys.exit(1)
|
||||
|
||||
expert_path = sys.argv[1]
|
||||
print(f"🔍 Validating expert package: {expert_path}\n")
|
||||
|
||||
result = validate_expert(expert_path)
|
||||
print(result.summary())
|
||||
|
||||
sys.exit(0 if result.is_valid else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,301 @@
|
||||
import { app } from 'electron'
|
||||
import { promises as fs } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { createHash, randomUUID } from 'node:crypto'
|
||||
import { parseDocument } from 'yaml'
|
||||
import type { ExpertActivation, ExpertBinding, ExpertDocument, ExpertRecord } from '@shared/capabilities'
|
||||
import { expertText } from '@shared/capabilities'
|
||||
import managerSource from './assets/expert-manager-source.json'
|
||||
import { listMcpServers } from './mcp'
|
||||
|
||||
/** Package layout and registration semantics recovered through REA; see the adjacent source manifest. */
|
||||
const META = ['.codebuddy-plugin/plugin.json', '.workbuddy-plugin/plugin.json']
|
||||
const MAX_BYTES = 16 * 1024 * 1024
|
||||
const managerFiles: Record<string, string> = managerSource.files
|
||||
let operations: Promise<unknown> = Promise.resolve()
|
||||
function serialized<T>(run: () => Promise<T>): Promise<T> { const next = operations.then(run); operations = next.catch(() => {}); return next }
|
||||
const digest = (value: string): string => createHash('sha256').update(value).digest('hex')
|
||||
const configRoot = (): string => app.getPath('userData')
|
||||
const marketRoot = (): string => path.join(configRoot(), 'plugins', 'marketplaces', 'my-experts')
|
||||
const packageRoot = (): string => path.join(marketRoot(), 'plugins')
|
||||
const marketFile = (): string => path.join(marketRoot(), '.codebuddy-plugin', 'marketplace.json')
|
||||
function identifier(value: unknown): string {
|
||||
if (typeof value !== 'string' || !/^[a-z0-9][a-z0-9-]{0,126}[a-z0-9]$/.test(value) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(value)) throw Error('专家标识必须是至少两位的 kebab-case')
|
||||
return value
|
||||
}
|
||||
function relative(value: unknown): string {
|
||||
if (typeof value !== 'string') throw Error('专家资源路径无效')
|
||||
const clean = value.replace(/^\.\//, '')
|
||||
if (!clean || clean.length > 240 || /[\\\x00-\x1f<>:"|?*]/.test(clean) || clean.split('/').some(part => !part || part === '.' || part === '..' || /[. ]$/.test(part) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) throw Error('专家资源路径越界或无效')
|
||||
return clean
|
||||
}
|
||||
async function safePath(target: string, create = false): Promise<string> {
|
||||
const resolved = path.resolve(target), base = path.resolve(configRoot())
|
||||
const rel = path.relative(base, resolved)
|
||||
if (path.isAbsolute(rel) || rel === '..' || rel.startsWith('..' + path.sep)) throw Error('专家路径越界')
|
||||
let current = path.parse(resolved).root
|
||||
for (const part of path.relative(current, resolved).split(path.sep)) {
|
||||
current = path.join(current, part)
|
||||
try { if ((await fs.lstat(current)).isSymbolicLink()) throw Error('专家目录不能包含符号链接') }
|
||||
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; if (create) await fs.mkdir(current); else throw error }
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
async function fileMap(root: string): Promise<Record<string, string>> {
|
||||
const files: Record<string, string> = {}; let bytes = 0, count = 0
|
||||
async function visit(dir: string): Promise<void> {
|
||||
const absolute = await safePath(dir)
|
||||
for (const item of await fs.readdir(absolute, { withFileTypes: true })) {
|
||||
if (item.isSymbolicLink()) throw Error('专家包不能包含符号链接')
|
||||
if (['node_modules', '.git', '__pycache__'].includes(item.name)) continue
|
||||
const file = path.join(absolute, item.name)
|
||||
if (item.isDirectory()) { await visit(file); continue }
|
||||
if (!item.isFile()) throw Error('专家包只能包含普通文件')
|
||||
const size = (await fs.stat(file)).size; bytes += size
|
||||
if (++count > 256 || bytes > MAX_BYTES) throw Error('专家包超过文件数或大小限制')
|
||||
const key = path.relative(root, file).replace(/\\/g, '/')
|
||||
if (/\.(?:md|json|txt|yaml|yml|py|js|cjs|mjs|ts|csv|css|html|svg|cs|csproj|slnx|xml|xsd|sh|ps1|LICENSE)$/i.test(key) || ['LICENSE', '.gitignore'].includes(path.basename(key)) || key.startsWith('.created-') || key === '.pending-preview-session') {
|
||||
const data = await fs.readFile(await safePath(file))
|
||||
try { files[key] = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(data) } catch { throw Error('专家文档必须是 UTF-8 文本') }
|
||||
}
|
||||
}
|
||||
}
|
||||
await visit(root); return files
|
||||
}
|
||||
function manifest(files: Record<string, string>): Record<string, any> {
|
||||
const content = META.map(key => files[key]).find(Boolean)
|
||||
if (!content) throw Error('专家包缺少 plugin.json')
|
||||
const data = JSON.parse(content.replace(/^\uFEFF/, ''))
|
||||
if (!data || typeof data !== 'object' || Array.isArray(data)) throw Error('专家元数据无效')
|
||||
return data
|
||||
}
|
||||
function yamlBody(value: string): { meta: Record<string, any>; body: string } {
|
||||
const match = /^\uFEFF?---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(value)
|
||||
if (!match) throw Error('Agent MD 缺少 YAML frontmatter')
|
||||
const doc = parseDocument(match[1], { uniqueKeys: true, schema: 'core' })
|
||||
if (doc.errors.length) throw Error('Agent MD YAML 无效')
|
||||
const meta = doc.toJS({ maxAliasCount: 30 })
|
||||
if (!meta || typeof meta !== 'object' || Array.isArray(meta)) throw Error('Agent MD 元数据无效')
|
||||
return { meta, body: value.slice(match[0].length).trim() }
|
||||
}
|
||||
export function validateExpertFiles(files: Record<string, string>, id: string): { errors: string[]; warnings: string[] } {
|
||||
const errors: string[] = [], warnings: string[] = []
|
||||
try {
|
||||
const p = manifest(files)
|
||||
for (const key of ['name', 'version', 'description']) if (typeof p[key] !== 'string' || !p[key].trim() || p[key].includes('[TODO')) errors.push(`${key} 不能为空或包含 [TODO]`)
|
||||
if (identifier(p.name) !== id) errors.push('name 必须与专家目录一致')
|
||||
if (!['agent', 'team'].includes(p.expertType)) errors.push('expertType 必须为 agent 或 team')
|
||||
if (p.plugin && p.plugin !== p.name) errors.push('plugin 必须等于 name')
|
||||
if (typeof p.agentName !== 'string' || p.agentName === 'team-lead' || !files[`agents/${p.agentName}.md`]) errors.push('agentName 必须对应真实 Agent MD,且不能是通用 team-lead')
|
||||
for (const key of ['displayName', 'profession', 'displayDescription', 'defaultInitPrompt']) if (!p[key] || typeof p[key] !== 'object' || ['zh', 'en'].some(locale => typeof p[key][locale] !== 'string' || !p[key][locale].trim() || p[key][locale].includes('[TODO'))) errors.push(`${key} 必须包含完整的中英文内容`)
|
||||
for (const key of ['tags', 'quickPrompts']) if (!Array.isArray(p[key]) || p[key].length !== 3) errors.push(`${key} 必须恰好有 3 项`)
|
||||
if (!Array.isArray(p.agents) || !p.agents.length) errors.push('agents 必须至少声明一个 Agent MD')
|
||||
for (const key of ['agents', 'skills']) for (const entry of p[key] ?? []) {
|
||||
const resource = relative(entry) + (key === 'skills' ? '/SKILL.md' : '')
|
||||
if (!files[resource]) errors.push(`资源未找到:${resource}`)
|
||||
}
|
||||
for (const [key, value] of Object.entries(files)) {
|
||||
relative(key)
|
||||
if (/^(?:hooks|commands)(?:\/|$)|(?:^|\/)\.lsp\.json$/.test(key)) errors.push(`不支持的专家资源:${key}`)
|
||||
if (/^\.(?:codebuddy|workbuddy)-plugin\/(?:agents|skills|bin|avatars)\//.test(key)) errors.push('资源必须位于包根目录')
|
||||
if (/^agents\/[^/]+\.md$/.test(key)) {
|
||||
const { meta, body } = yamlBody(value)
|
||||
if (meta.name !== path.basename(key, '.md')) errors.push(`${key} 的 name 必须与文件名一致`)
|
||||
if ('tools' in meta) errors.push(`${key} 禁止声明 tools`)
|
||||
if (!body || body.includes('[TODO')) errors.push(`${key} 正文未完成`)
|
||||
if (!meta.description) warnings.push(`${key} 缺少 description`)
|
||||
}
|
||||
}
|
||||
const n = Array.from(p.displayDescription?.zh ?? '').length
|
||||
if (n < 40 || n > 50) warnings.push('displayDescription.zh 推荐 40–50 字')
|
||||
if (p.defaultInitPrompt?.zh !== p.quickPrompts?.[0]?.zh) warnings.push('defaultInitPrompt 应与第一条 quickPrompt 一致')
|
||||
if (p.avatar) relative(p.avatar)
|
||||
else warnings.push('尚未提供专家头像')
|
||||
if (!files['README.md']) warnings.push('尚未提供 README.md')
|
||||
if (p.expertType === 'team') {
|
||||
if (!p.teamInfo || !Array.isArray(p.members) || !p.members.some((m: any) => m?.role === 'lead')) errors.push('专家团必须声明 teamInfo 和包含主理人的 members')
|
||||
if (p.teamInfo?.memberAgents?.includes(p.teamInfo?.leadAgent)) errors.push('memberAgents 不能包含主理人')
|
||||
if (p.profession?.zh !== p.displayName?.zh) errors.push('专家团 profession 必须等于 displayName')
|
||||
if (!files['settings.json']) errors.push('专家团缺少 settings.json')
|
||||
else { const settings = JSON.parse(files['settings.json']); if (settings.agent && settings.agent !== p.agentName) errors.push('settings.json agent 与 agentName 不一致') }
|
||||
}
|
||||
} catch (error) { errors.push(error instanceof Error ? error.message : String(error)) }
|
||||
return { errors, warnings }
|
||||
}
|
||||
async function record(id: string, files: Record<string, string>, root: string): Promise<ExpertRecord> {
|
||||
const p = manifest(files), validation = validateExpertFiles(files, id)
|
||||
let avatar: string | undefined
|
||||
if (typeof p.avatar === 'string' && !/^[a-z]+:/i.test(p.avatar)) try {
|
||||
const file = await safePath(path.join(root, relative(p.avatar)))
|
||||
if ((await fs.stat(file)).size <= 500 * 1024 && /\.(png|jpe?g|webp)$/i.test(file)) avatar = `data:image/${/\.jpe?g$/i.test(file) ? 'jpeg' : path.extname(file).slice(1)};base64,${(await fs.readFile(file)).toString('base64')}`
|
||||
} catch { /* Missing avatars remain optional; never load arbitrary filesystem URLs. */ }
|
||||
return { id, name: p.name, displayName: p.displayName, profession: p.profession, description: p.description ?? '', displayDescription: p.displayDescription, version: p.version || '1.0.0', expertType: p.expertType || 'skill', agentName: p.agentName, plugin: p.plugin || p.name, pluginRegisteredName: p.name, marketplace: 'my-experts', isCustomExpert: true, expertRootDir: root, avatar, defaultInitPrompt: p.defaultInitPrompt, tags: p.tags, quickPrompts: p.quickPrompts, skills: p.skills, categoryId: p.categoryId, members: p.members, createdBySession: (files['.created-by-session'] || files['.pending-preview-session'])?.trim(), updatedAt: (await fs.stat(await safePath(path.join(root, META.find(key => files[key])!)))).mtimeMs, error: validation.errors.join('\n') || undefined }
|
||||
}
|
||||
async function listExpertsUnlocked(): Promise<ExpertRecord[]> {
|
||||
let catalog: any
|
||||
try { catalog = JSON.parse(await fs.readFile(await safePath(marketFile()), 'utf8')) } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; throw error }
|
||||
if (!Array.isArray(catalog.plugins)) throw Error('本地专家目录格式无效')
|
||||
const result: ExpertRecord[] = [], seen = new Set<string>()
|
||||
for (const item of catalog.plugins) try {
|
||||
const id = identifier(String(item.source ?? '').replace(/^\.\/plugins\//, ''))
|
||||
if (seen.has(id)) continue
|
||||
seen.add(id); const root = path.join(packageRoot(), id)
|
||||
result.push(await record(id, await fileMap(root), root))
|
||||
} catch { /* Broken entries do not prevent other experts from being discovered. */ }
|
||||
return result
|
||||
}
|
||||
async function readExpertUnlocked(id: string): Promise<ExpertDocument> {
|
||||
identifier(id); const root = path.join(packageRoot(), id), files = await fileMap(root), expert = await record(id, files, root)
|
||||
const prompt = expert.agentName && files[`agents/${expert.agentName}.md`] ? yamlBody(files[`agents/${expert.agentName}.md`]).body : ''
|
||||
return { expert, files, prompt }
|
||||
}
|
||||
export function listExperts(): Promise<ExpertRecord[]> { return serialized(listExpertsUnlocked) }
|
||||
export function readExpert(id: string): Promise<ExpertDocument> { return serialized(() => readExpertUnlocked(id)) }
|
||||
export async function activateExpert(id: string): Promise<ExpertActivation> {
|
||||
const document = await readExpert(id)
|
||||
return { ...await activateDocument(document), defaultInitPrompt: document.expert.defaultInitPrompt }
|
||||
}
|
||||
async function activateDocument(document: ExpertDocument): Promise<ExpertBinding> {
|
||||
const { expert, prompt, files } = document, id = expert.id, p = manifest(files)
|
||||
if (expert.error) throw Error(expert.error)
|
||||
if (expert.expertType === 'team') throw Error('专家团调度尚未接入,暂不能激活')
|
||||
if (!prompt) throw Error('专家缺少可运行的 Agent 指令')
|
||||
if ((p.dependencies?.connectors?.length ?? 0) > 0 || (p.connectorIds?.length ?? 0) > 0) throw Error('此专家依赖云端连接器,当前尚未接入')
|
||||
const declaration = p.dependencies?.mcpServers ?? p.mcpServers ?? (files['.mcp.json'] ? '.mcp.json' : undefined)
|
||||
if (declaration) {
|
||||
const read = (value: any): Record<string, any> => {
|
||||
if (typeof value === 'string') { const content = files[relative(value)]; if (!content) throw Error('专家依赖配置未找到'); const parsed = JSON.parse(content); return read(parsed.mcpServers ?? parsed) }
|
||||
if (Array.isArray(value)) return Object.assign({}, ...value.map(read))
|
||||
if (!value || typeof value !== 'object') throw Error('专家 MCP 依赖声明无效')
|
||||
return value
|
||||
}
|
||||
const declarations = read(declaration), connected = await listMcpServers()
|
||||
for (const name of Object.keys(declarations)) {
|
||||
const server = connected.find(item => item.name === name)
|
||||
if (!server?.enabled || server.status !== 'connected' || !server.trusted) throw Error(`请先在连接器中配置并连接 ${name}`)
|
||||
// A same-name server is not sufficient evidence of matching package dependencies.
|
||||
const required = declarations[name]
|
||||
if (required?.url && required.url !== server.url || required?.command && required.command !== server.command) throw Error(`连接器 ${name} 与专家要求的配置不一致`)
|
||||
if (required?.command) {
|
||||
if (server.transport !== 'stdio' || !Array.isArray(required.args ?? [])) throw Error(`连接器 ${name} 的传输方式或参数无效`)
|
||||
const args = (required.args ?? []).map((arg: unknown) => {
|
||||
if (typeof arg !== 'string') throw Error('MCP 参数必须是字符串')
|
||||
return arg.startsWith('./') ? path.join(expert.expertRootDir, relative(arg)) : arg
|
||||
})
|
||||
if (JSON.stringify(args) !== JSON.stringify(server.args ?? [])) throw Error(`连接器 ${name} 的启动参数与专家要求不一致`)
|
||||
}
|
||||
if (required?.url && server.transport !== 'http' || required?.type && ['stdio', 'http'].includes(required.type) && required.type !== server.transport) throw Error(`连接器 ${name} 的传输方式与专家要求不一致`)
|
||||
if (required?.cwd && path.resolve(required.cwd) !== path.resolve(server.cwd ?? '')) throw Error(`连接器 ${name} 的工作目录与专家要求不一致`)
|
||||
}
|
||||
}
|
||||
return { id, runtimeIdentity: 'expert-v2:' + JSON.stringify({ v: 2, kind: 'expert', expertId: id, pluginRegisteredName: expert.pluginRegisteredName, agentName: expert.agentName, source: 'custom' }), name: expertText(expert.displayName) || expert.name, profession: expertText(expert.profession) || undefined, avatarUrl: expert.avatar, expertType: expert.expertType }
|
||||
}
|
||||
export async function expertTurnContext(id: string): Promise<{ context: string; display: ExpertBinding; resources: Record<string, string> }> {
|
||||
const document = await readExpert(id), display = await activateDocument(document), { expert, files, prompt } = document, p = manifest(files)
|
||||
// REA agent-md-spec: Agent YAML skills are startup preloads. plugin.skills only registers directories.
|
||||
const legalSkillName = (value: unknown): value is string => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value)
|
||||
const skillEntries: unknown[] = p.skills ?? []
|
||||
const directory = skillEntries.map((entry: unknown) => {
|
||||
const key = relative(entry) + '/SKILL.md', content = files[key]
|
||||
if (typeof content !== 'string') throw Error(`专家技能资源未找到:${key}`)
|
||||
const { meta } = yamlBody(content)
|
||||
if (!legalSkillName(meta.name)) throw Error(`专家技能名称无效:${key}`)
|
||||
return { name: meta.name, path: key, description: typeof meta.description === 'string' ? meta.description : '', content }
|
||||
})
|
||||
const registered = new Map<string, typeof directory>()
|
||||
for (const skill of directory) registered.set(skill.name, [...(registered.get(skill.name) ?? []), skill])
|
||||
let preloaded = directory
|
||||
if (expert.expertType === 'agent') {
|
||||
const declared: unknown = yamlBody(files[`agents/${expert.agentName}.md`]).meta.skills
|
||||
if (declared !== undefined && (!Array.isArray(declared) || !declared.every(legalSkillName))) throw Error('Agent skills 必须是合法技能名称列表')
|
||||
preloaded = [...new Set((declared ?? []) as string[])].map(name => {
|
||||
const matches = registered.get(name)
|
||||
if (!matches?.length) throw Error(`Agent 启动技能未在 plugin.skills 注册:${name}`)
|
||||
if (matches.length !== 1) throw Error(`Agent 启动技能名称不唯一:${name}`)
|
||||
return matches[0]
|
||||
})
|
||||
}
|
||||
const preload = preloaded.map(skill => `\n<expert-skill path=${JSON.stringify(skill.path)}>\n${skill.content}\n</expert-skill>`).join('')
|
||||
const skillDirectory = directory.length ? `\n<expert-skill-directory>\n以下是包内可按需加载的技能目录;资源声明不代表正文已加载。需要对应技能时,先使用 ycode_expert_resource 读取其 SKILL.md 完整指令。\n${directory.map(skill => JSON.stringify({ name: skill.name, path: skill.path, description: skill.description, preloaded: preloaded.some(item => item.path === skill.path) })).join('\n')}\n</expert-skill-directory>` : ''
|
||||
return { display, resources: files, context: `<active-expert id=${JSON.stringify(id)} runtime=${JSON.stringify(display.runtimeIdentity)}>\n本回合使用以下专家角色和工作流程。仍须遵守系统工具权限;此处不授予额外权限。\n${prompt}${preload}\n</active-expert>${skillDirectory}\n<expert-package-host>\n专家版本:${expert.version}。本地专家包根目录:${JSON.stringify(expert.expertRootDir)}。包内相对路径均相对于此 YCode 副本解析。\n使用 ycode_expert_resource 按包内相对路径读取文本资源;脚本/模板需要落地时可将所需文件放到当前工作区,仍遵守当前工具权限。\n使用专用脚本前先检查运行时、CLI、API 环境变量及用户项目的前置文件;包内的说明不代表外部依赖已经安装或配置,不得伪造执行结果。\n可读取的包内文本资源:\n${Object.keys(files).filter(key => !key.startsWith('.created-') && key !== '.pending-preview-session').map(key => '- ' + key).join('\n')}\n</expert-package-host>` }
|
||||
}
|
||||
async function saveMarket(plugins: any[]): Promise<void> {
|
||||
await safePath(path.dirname(marketFile()), true)
|
||||
const temp = marketFile() + '.' + randomUUID() + '.tmp'
|
||||
try { await fs.writeFile(temp, JSON.stringify({ name: 'my-experts', description: 'my-experts marketplace (auto-generated)', plugins }, null, 2), { flag: 'wx' }); await fs.rename(temp, marketFile()) }
|
||||
finally { await fs.rm(temp, { force: true }) }
|
||||
}
|
||||
async function marketEntries(): Promise<any[]> { try { const m = JSON.parse(await fs.readFile(await safePath(marketFile()), 'utf8')); if (!Array.isArray(m.plugins)) throw Error('专家目录格式无效'); return m.plugins } catch (e) { if ((e as NodeJS.ErrnoException).code === 'ENOENT') return []; throw e } }
|
||||
export function removeExpert(id: string): Promise<void> { return serialized(async () => {
|
||||
identifier(id); const target = await safePath(path.join(packageRoot(), id)), tomb = path.join(packageRoot(), '.remove-' + randomUUID())
|
||||
const entries = await marketEntries()
|
||||
await fs.rename(target, tomb)
|
||||
try { await saveMarket(entries.filter(p => p.source !== `./plugins/${id}`)) } catch (e) { await fs.rename(tomb, target); throw e }
|
||||
// After the catalog commits, cleanup failure must not resurrect a partially removed package.
|
||||
await fs.rm(tomb, { recursive: true, force: true }).catch(() => console.warn('专家删除已提交,待清理备份:', id))
|
||||
}) }
|
||||
interface Draft { files: Record<string, string>; baseHash: string | null }
|
||||
export function expertPackageOperation(sessionId: string, args: Record<string, unknown>): Promise<unknown> { return serialized(async () => {
|
||||
if (!/^[a-zA-Z0-9-]{1,128}$/.test(sessionId)) throw Error('会话标识无效')
|
||||
const id = identifier(args.id), action = args.action
|
||||
const draftDir = path.join(configRoot(), 'capabilities', 'expert-drafts', sessionId), draftFile = path.join(draftDir, id + '.json')
|
||||
let draft: Draft
|
||||
try { if (action === 'read' && args.refresh === true) throw Object.assign(Error('refresh'), { code: 'ENOENT' }); draft = JSON.parse(await fs.readFile(await safePath(draftFile), 'utf8')) } catch (e) {
|
||||
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e
|
||||
let files: Record<string, string> = {}
|
||||
try { files = (await readExpertUnlocked(id)).files } catch (cause) { if ((cause as NodeJS.ErrnoException).code !== 'ENOENT') throw cause }
|
||||
draft = { files, baseHash: Object.keys(files).length ? digest(JSON.stringify(files)) : null }
|
||||
}
|
||||
if (action === 'read') {
|
||||
await safePath(draftDir, true)
|
||||
const temp = draftFile + '.' + randomUUID(); await fs.writeFile(temp, JSON.stringify(draft), { flag: 'wx' }); await fs.rename(temp, draftFile)
|
||||
return { id, ...draft }
|
||||
}
|
||||
if (action === 'write') {
|
||||
if (!args.files || typeof args.files !== 'object' || Array.isArray(args.files)) throw Error('files 必须是相对路径到 UTF-8 内容的对象')
|
||||
const next = { ...draft.files }
|
||||
for (const [key, value] of Object.entries(args.files)) {
|
||||
const name = relative(key)
|
||||
if (name === '.created-by-session' || name === '.pending-preview-session') throw Error('会话标记由注册步骤写入')
|
||||
if (value === null) delete next[name]
|
||||
else if (typeof value === 'string' && Buffer.byteLength(value) <= 1024 * 1024) next[name] = value
|
||||
else throw Error('专家文件必须是小于 1MB 的 UTF-8 文本,删除请用 null')
|
||||
}
|
||||
if (Object.keys(next).length > 256 || Buffer.byteLength(JSON.stringify(next)) > MAX_BYTES) throw Error('专家包超过大小或文件数限制')
|
||||
if (draft.baseHash && META.some(key => next[key])) { const previous = manifest(draft.files), updated = manifest(next); if (previous.name !== updated.name || previous.agentName !== updated.agentName) throw Error('修改专家时不能改变 name 或 agentName') }
|
||||
draft.files = next; await safePath(draftDir, true)
|
||||
const temp = draftFile + '.' + randomUUID(); await fs.writeFile(temp, JSON.stringify(draft), { flag: 'wx' }); await fs.rename(temp, draftFile)
|
||||
return { id, files: Object.keys(next), ...validateExpertFiles(next, id), registered: false }
|
||||
}
|
||||
const validation = validateExpertFiles(draft.files, id)
|
||||
if (action === 'validate') return { id, ...validation, valid: !validation.errors.length }
|
||||
if (action !== 'register') throw Error('action 必须是 read、write、validate 或 register')
|
||||
if (validation.errors.length) throw Error(validation.errors.join('\n'))
|
||||
const target = path.join(packageRoot(), id)
|
||||
let current: Record<string, string> | undefined
|
||||
try { current = (await readExpertUnlocked(id)).files } catch (e) { if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e }
|
||||
if ((current ? digest(JSON.stringify(current)) : null) !== draft.baseHash) throw Error('专家已被其他会话修改,请重新读取后编辑')
|
||||
await safePath(packageRoot(), true)
|
||||
const stage = path.join(packageRoot(), '.stage-' + randomUUID()), backup = path.join(packageRoot(), '.backup-' + randomUUID())
|
||||
const entries = await marketEntries(); await fs.mkdir(stage); let backedUp = false, published = false
|
||||
try {
|
||||
// Preserve existing binary resources during textual edits.
|
||||
if (current) { await safePath(target); await fs.cp(target, stage, { recursive: true, force: true, dereference: false }); await fileMap(stage) }
|
||||
for (const key of Object.keys(current ?? {})) if (!(key in draft.files)) await fs.rm(path.join(stage, relative(key)), { force: true })
|
||||
for (const [key, content] of Object.entries(draft.files)) { const file = path.join(stage, relative(key)); await safePath(path.dirname(file), true); await fs.writeFile(file, content) }
|
||||
await fs.writeFile(path.join(stage, '.created-by-session'), sessionId)
|
||||
if (current) { await fs.rename(target, backup); backedUp = true }
|
||||
await fs.rename(stage, target); published = true
|
||||
const p = manifest(draft.files), filtered = entries.filter(entry => entry.name !== p.name && entry.source !== `./plugins/${id}`)
|
||||
await saveMarket([...filtered, { name: p.name, source: `./plugins/${id}`, description: p.description }])
|
||||
} catch (error) { if (published) await fs.rm(target, { recursive: true, force: true }); if (backedUp) await fs.rename(backup, target); throw error }
|
||||
finally { await fs.rm(stage, { recursive: true, force: true }) }
|
||||
if (backedUp) await fs.rm(backup, { recursive: true, force: true }).catch(() => console.warn('专家注册已提交,待清理备份:', id))
|
||||
await fs.rm(draftFile, { force: true }).catch(() => console.warn('专家注册已提交,待清理草稿:', id))
|
||||
return { success: true, expert: (await readExpertUnlocked(id)).expert, warnings: validation.warnings }
|
||||
}) }
|
||||
export function expertManagerDocument(): { content: string; files: string[] } { return { content: managerFiles['SKILL.md'], files: Object.keys(managerFiles) } }
|
||||
export function expertManagerResource(resource: string): string { const key = relative(resource), value = Object.hasOwn(managerFiles, key) ? managerFiles[key] : undefined; if (typeof value !== 'string') throw Error('专家管理器资源不存在'); return value }
|
||||
export function expertManagerAdapter(sessionId: string): string {
|
||||
return `\n<ycode-expert-manager-host>\n以上是通过 REA 恢复的 WorkBuddy 原始 expert-manager 技能。当前运行在 YCode,以下是宿主适配:\n1. WORKBUDDY_CONFIG_DIR 对应 YCode 的受控本地配置目录;不要写入 WorkBuddy 目录,不要在工作区伪造注册结果。\n2. 使用 ycode_expert_package 代替 Python 初始化/写文件/校验/注册:先 read(id),再 write(id,files) 增量保存,validate,最后 register。files 为包内相对路径到完整 UTF-8 文本的对象,null 删除文件。格式仍遵守原 references;工具自动管理固定目录、marketplace.json 与 CODEBUDDY_SESSION_ID=${sessionId} 标记。修改前必须 read,保留未修改字段,禁止改变 name/agentName/Agent 文件名。\n3. 用 ycode_read_skill_resource(id=builtin:expert-manager,path=...) 阅读原 references/scripts;Python脚本供规范参考,不在当前宿主执行。\n4. 当前未接入 ImageGen;需要头像时如实说明并请用户提供图片,不要声称已生成。可先不填 avatar 并展示校验警告。专家团包可编辑/校验,团队调度未接入,不能声称可运行。\n5. register 返回 success:true 才算创建完成。校验失败必须修复,不得只输出一段角色描述。\n</ycode-expert-manager-host>`
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
import { app, safeStorage } from 'electron'
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { join, isAbsolute } from 'node:path'
|
||||
import { createHash, randomUUID } from 'node:crypto'
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
|
||||
import type { McpServerInput, McpServerView, McpToolView, McpStatus } from '@shared/capabilities'
|
||||
import type { ToolSpec } from '@shared/types'
|
||||
|
||||
interface SecretValues { env: Record<string, string>; headers: Record<string, string> }
|
||||
interface StoredServer extends Omit<McpServerView, 'status' | 'error' | 'trusted'> {
|
||||
secrets?: string
|
||||
trustedHash?: string
|
||||
}
|
||||
interface Registry { version: 1; servers: StoredServer[] }
|
||||
interface Runtime {
|
||||
generation: string
|
||||
configHash: string
|
||||
status: McpStatus
|
||||
error?: string
|
||||
client: Client
|
||||
transport: Transport
|
||||
controller: AbortController
|
||||
secretValues: string[]
|
||||
flight?: Promise<McpServerView>
|
||||
}
|
||||
let registry: Registry | null = null
|
||||
let loading: Promise<Registry> | null = null
|
||||
let writes: Promise<unknown> = Promise.resolve()
|
||||
const runtimes = new Map<string, Runtime>()
|
||||
const MAX_SERVERS = 100, MAX_TOOLS = 10000, CONNECT_TIMEOUT = 20000, CALL_TIMEOUT = 120000
|
||||
const filePath = (): string => join(app.getPath('userData'), 'mcp-servers.json')
|
||||
const clone = <T>(value: T): T => structuredClone(value)
|
||||
const abortError = (): Error => Object.assign(new Error('MCP 调用已取消'), { name: 'AbortError' })
|
||||
function object(value: unknown): value is Record<string, unknown> { return !!value && typeof value === 'object' && !Array.isArray(value) }
|
||||
function sorted(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map(sorted)
|
||||
return object(value) ? Object.fromEntries(Object.keys(value).sort().map(key => [key, sorted(value[key])])) : value
|
||||
}
|
||||
function readSecrets(server: StoredServer): SecretValues {
|
||||
if (!server.secrets) return { env: {}, headers: {} }
|
||||
try {
|
||||
const values = JSON.parse(safeStorage.decryptString(Buffer.from(server.secrets, 'base64'))) as SecretValues
|
||||
if (!object(values.env) || !object(values.headers)) throw Error()
|
||||
return values
|
||||
} catch { throw new Error('无法解密 MCP 凭据,请重新配置') }
|
||||
}
|
||||
function encryptSecrets(values: SecretValues): string | undefined {
|
||||
if (!Object.keys(values.env).length && !Object.keys(values.headers).length) return undefined
|
||||
try {
|
||||
if (!safeStorage.isEncryptionAvailable()) throw Error()
|
||||
return safeStorage.encryptString(JSON.stringify(values)).toString('base64')
|
||||
} catch { throw new Error('系统加密服务不可用,无法安全保存 MCP 凭据') }
|
||||
}
|
||||
function mergeSecretMap(input: unknown, previous: Record<string, string>, headers: boolean): Record<string, string> {
|
||||
if (input === undefined) return { ...previous }
|
||||
if (!object(input) || Object.keys(input).length > 128) throw new Error('MCP 环境变量或 Headers 格式无效')
|
||||
const result: Record<string, string> = Object.create(null)
|
||||
for (const [rawKey, value] of Object.entries(input)) {
|
||||
const key = headers ? rawKey.toLowerCase() : rawKey
|
||||
if (!key || key.length > 256 || !/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(key) || typeof value !== 'string' || value.length > 65536) throw new Error('MCP 环境变量或 Headers 格式无效')
|
||||
if (headers && /[\r\n]/.test(value)) throw new Error('MCP Headers 格式无效')
|
||||
// Empty retained keys keep credentials; omit a key from an explicitly supplied map to remove it.
|
||||
if (value) result[key] = value
|
||||
else if (previous[key] !== undefined) result[key] = previous[key]
|
||||
}
|
||||
return result
|
||||
}
|
||||
function normalize(input: McpServerInput, previous?: StoredServer): StoredServer {
|
||||
if (!input || typeof input.name !== 'string' || !input.name.trim() || input.name.length > 128) throw new Error('请输入有效的 MCP 名称')
|
||||
if (input.id && (!previous || previous.id !== input.id)) throw new Error('MCP 服务不存在')
|
||||
if (input.transport !== 'stdio' && input.transport !== 'http') throw new Error('不支持的 MCP 连接类型')
|
||||
const before = previous ? readSecrets(previous) : { env: {}, headers: {} }
|
||||
const secrets = { env: mergeSecretMap(input.env, before.env, false), headers: mergeSecretMap(input.headers, before.headers, true) }
|
||||
const config: Omit<McpServerInput, 'id' | 'env' | 'headers' | 'enabled'> = { name: input.name.trim(), transport: input.transport }
|
||||
if (input.transport === 'stdio') {
|
||||
if (typeof input.command !== 'string' || !input.command.trim() || input.command.length > 4096 || /[\r\n\0]/.test(input.command)) throw new Error('请输入有效的 MCP 启动命令')
|
||||
if (input.args !== undefined && (!Array.isArray(input.args) || input.args.length > 256 || input.args.some(arg => typeof arg !== 'string' || arg.length > 65536 || arg.includes('\0')))) throw new Error('MCP 参数格式无效')
|
||||
if (input.cwd && (!isAbsolute(input.cwd) || input.cwd.includes('\0'))) throw new Error('MCP 工作目录必须为绝对路径')
|
||||
config.command = input.command.trim(); config.args = input.args?.slice() || []
|
||||
if (input.cwd) config.cwd = input.cwd
|
||||
} else {
|
||||
let url: URL
|
||||
try { url = new URL(input.url || '') } catch { throw new Error('请输入有效的 MCP HTTP 地址') }
|
||||
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || [...url.searchParams.keys()].some(key => /token|key|secret|password|authorization/i.test(key))) throw new Error('MCP 地址必须为 HTTP(S),凭据请使用 Headers 配置')
|
||||
config.url = url.href
|
||||
}
|
||||
const configHash = createHash('sha256').update(JSON.stringify(sorted({ ...config, ...secrets }))).digest('hex')
|
||||
return { ...config, id: previous?.id || randomUUID(), enabled: input.enabled ?? previous?.enabled ?? false,
|
||||
envKeys: Object.keys(secrets.env).sort(), headerKeys: Object.keys(secrets.headers).sort(), configHash,
|
||||
secrets: encryptSecrets(secrets), trustedHash: previous?.configHash === configHash ? previous.trustedHash : undefined,
|
||||
tools: previous?.configHash === configHash ? clone(previous.tools) : [] }
|
||||
}
|
||||
async function load(): Promise<Registry> {
|
||||
if (registry) return registry
|
||||
if (loading) return loading
|
||||
loading = (async () => {
|
||||
let content: string
|
||||
try { content = await fs.readFile(filePath(), 'utf8') }
|
||||
catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return registry = { version: 1, servers: [] }; throw error }
|
||||
let parsed: Registry
|
||||
try { parsed = JSON.parse(content) as Registry } catch { throw new Error('MCP 配置文件损坏,请保留配置文件后检查内容') }
|
||||
if (parsed.version !== 1 || !Array.isArray(parsed.servers) || parsed.servers.length > MAX_SERVERS) throw new Error('MCP 配置版本或内容无效')
|
||||
const ids = new Set<string>()
|
||||
const normalized: StoredServer[] = []
|
||||
for (const server of parsed.servers) {
|
||||
if (!server || typeof server.id !== 'string' || ids.has(server.id) || typeof server.name !== 'string' || typeof server.configHash !== 'string'
|
||||
|| !['stdio', 'http'].includes(server.transport) || !Array.isArray(server.tools) || !Array.isArray(server.envKeys) || !Array.isArray(server.headerKeys)) throw new Error('MCP 配置内容无效')
|
||||
ids.add(server.id)
|
||||
// A hand-edited file cannot retain approval for a different command, URL or secret.
|
||||
// Reconstructing the record also excludes unknown credential-bearing fields from IPC views.
|
||||
normalized.push(normalize({ ...server, ...readSecrets(server) }, server))
|
||||
}
|
||||
return registry = { version: 1, servers: normalized }
|
||||
})().finally(() => { loading = null })
|
||||
return loading
|
||||
}
|
||||
async function persist(next: Registry): Promise<void> {
|
||||
const file = filePath(), temporary = `${file}.${randomUUID()}.tmp`
|
||||
await fs.mkdir(join(file, '..'), { recursive: true })
|
||||
try { await fs.writeFile(temporary, JSON.stringify(next, null, 2), { encoding: 'utf8', mode: 0o600 }); await fs.rename(temporary, file); registry = next }
|
||||
finally { await fs.unlink(temporary).catch(() => undefined) }
|
||||
}
|
||||
function mutate<T>(operation: (current: Registry) => Promise<T>): Promise<T> {
|
||||
const next = writes.then(async () => operation(await load()))
|
||||
writes = next.catch(() => undefined)
|
||||
return next
|
||||
}
|
||||
function requireServer(current: Registry, id: string): StoredServer {
|
||||
const server = current.servers.find(server => server.id === id)
|
||||
if (!server) throw new Error('MCP 服务不存在')
|
||||
return server
|
||||
}
|
||||
function view(server: StoredServer): McpServerView {
|
||||
const { secrets: _secret, trustedHash, ...publicFields } = server
|
||||
const runtime = runtimes.get(server.id)
|
||||
return { ...clone(publicFields), trusted: trustedHash === server.configHash,
|
||||
status: runtime?.status || 'disconnected', ...(runtime?.error ? { error: runtime.error } : {}) }
|
||||
}
|
||||
function publicError(error: unknown, operation: 'connect' | 'call'): string {
|
||||
const message = error instanceof Error ? error.message : ''
|
||||
if (/401|403|unauthoriz|forbidden/i.test(message)) return 'MCP 鉴权失败,请检查凭据与权限'
|
||||
if (/timeout|timed out/i.test(message)) return 'MCP 请求超时,请稍后重试'
|
||||
return operation === 'connect' ? '无法连接 MCP 服务,请检查命令、地址和运行环境' : 'MCP 工具调用失败'
|
||||
}
|
||||
async function closeRuntime(id: string): Promise<void> {
|
||||
const runtime = runtimes.get(id)
|
||||
if (!runtime) return
|
||||
runtimes.delete(id)
|
||||
runtime.controller.abort()
|
||||
// Close aborts all pending requests before best-effort remote session cleanup.
|
||||
if (runtime.transport instanceof StreamableHTTPClientTransport) {
|
||||
await Promise.race([runtime.transport.terminateSession().catch(() => undefined), new Promise(resolve => setTimeout(resolve, 500))])
|
||||
}
|
||||
await runtime.client.close().catch(() => undefined)
|
||||
}
|
||||
export async function listMcpServers(): Promise<McpServerView[]> { await writes; return (await load()).servers.map(view) }
|
||||
export async function saveMcpServer(input: McpServerInput): Promise<McpServerView> {
|
||||
return mutate(async current => {
|
||||
const previous = input.id ? requireServer(current, input.id) : undefined, next = normalize(input, previous)
|
||||
if (!previous && current.servers.length >= MAX_SERVERS) throw new Error('MCP 服务数量已达上限')
|
||||
if (current.servers.some(server => server.id !== next.id && server.name === next.name)) throw new Error('同名 MCP 已存在')
|
||||
await persist({ version: 1, servers: previous ? current.servers.map(server => server.id === next.id ? next : server) : [...current.servers, next] })
|
||||
if (previous && (previous.configHash !== next.configHash || !next.enabled)) await closeRuntime(next.id)
|
||||
return view(next)
|
||||
})
|
||||
}
|
||||
export async function trustMcpServer(id: string, configHash: string): Promise<void> {
|
||||
await mutate(async current => {
|
||||
const server = requireServer(current, id)
|
||||
if (server.configHash !== configHash) throw new Error('MCP 配置已变化,请重新查看并批准')
|
||||
await persist({ ...current, servers: current.servers.map(item => item.id === id ? { ...item, trustedHash: configHash } : item) })
|
||||
})
|
||||
}
|
||||
export async function connectMcpServer(id: string): Promise<McpServerView> {
|
||||
await writes
|
||||
const server = requireServer(await load(), id)
|
||||
if (!server.enabled) throw new Error('MCP 服务已禁用')
|
||||
if (server.trustedHash !== server.configHash) throw new Error('请先批准当前 MCP 配置')
|
||||
const existing = runtimes.get(id)
|
||||
if (existing?.status === 'connected' && existing.configHash === server.configHash) return view(server)
|
||||
if (existing?.flight && existing.configHash === server.configHash) return existing.flight
|
||||
if (existing) await closeRuntime(id)
|
||||
await writes
|
||||
const current = requireServer(await load(), id)
|
||||
if (current.configHash !== server.configHash || !current.enabled || current.trustedHash !== current.configHash) throw new Error('MCP 配置已变化,请重试')
|
||||
const values = readSecrets(server), client = new Client({ name: 'YCode', version: '0.1.0' })
|
||||
const transport: Transport = server.transport === 'stdio'
|
||||
? new StdioClientTransport({ command: server.command!, args: server.args, cwd: server.cwd, env: values.env, stderr: 'ignore' })
|
||||
: new StreamableHTTPClientTransport(new URL(server.url!), { requestInit: { headers: values.headers, redirect: 'error' },
|
||||
// Keep credentials on the configured origin. GET streams also use this guarded fetch.
|
||||
fetch: async (input, init) => {
|
||||
const target = new URL(input instanceof Request ? input.url : String(input)), configured = new URL(server.url!)
|
||||
if (target.origin !== configured.origin) throw new Error('MCP origin changed')
|
||||
return fetch(input, { ...init, redirect: 'error' })
|
||||
} })
|
||||
const runtime: Runtime = { generation: randomUUID(), configHash: server.configHash, status: 'connecting', client, transport,
|
||||
controller: new AbortController(), secretValues: [...Object.values(values.env), ...Object.values(values.headers),
|
||||
...Object.values(values.headers).filter(value => /^Bearer\s+/i.test(value)).map(value => value.replace(/^Bearer\s+/i, ''))].filter(Boolean) }
|
||||
runtimes.set(id, runtime)
|
||||
client.onclose = () => { if (runtimes.get(id) === runtime && runtime.status === 'connected') { runtime.status = 'error'; runtime.error = 'MCP 服务连接已断开' } }
|
||||
client.onerror = () => {} // Do not forward upstream payloads/credentials into logs or the renderer.
|
||||
runtime.flight = (async () => {
|
||||
try {
|
||||
await client.connect(transport, { signal: runtime.controller.signal, timeout: CONNECT_TIMEOUT })
|
||||
const tools: McpToolView[] = [], names = new Set<string>(), cursors = new Set<string>()
|
||||
let cursor: string | undefined
|
||||
do {
|
||||
const page = await client.listTools(cursor ? { cursor } : undefined, { signal: runtime.controller.signal, timeout: CONNECT_TIMEOUT })
|
||||
for (const tool of page.tools) {
|
||||
if (names.has(tool.name)) continue
|
||||
names.add(tool.name)
|
||||
if (tools.length >= MAX_TOOLS) throw Error('MCP tool budget exceeded')
|
||||
if (runtime.secretValues.some(secret => tool.name.includes(secret))) throw Error('MCP tool name contains a credential')
|
||||
tools.push({ name: tool.name, description: mask(tool.description || '', runtime.secretValues) as string, inputSchema: mask(tool.inputSchema, runtime.secretValues) as Record<string, unknown>,
|
||||
enabled: server.tools.find(previous => previous.name === tool.name)?.enabled !== false })
|
||||
}
|
||||
cursor = page.nextCursor
|
||||
if (cursor && cursors.has(cursor)) throw Error('MCP repeated pagination cursor')
|
||||
if (cursor) cursors.add(cursor)
|
||||
} while (cursor)
|
||||
await mutate(async current => {
|
||||
const latest = requireServer(current, id)
|
||||
if (runtimes.get(id) !== runtime || latest.configHash !== runtime.configHash || !latest.enabled || latest.trustedHash !== latest.configHash) throw abortError()
|
||||
// A concurrent tool toggle wins over an older list result.
|
||||
const merged = tools.map(tool => ({ ...tool, enabled: latest.tools.find(previous => previous.name === tool.name)?.enabled ?? tool.enabled }))
|
||||
await persist({ ...current, servers: current.servers.map(item => item.id === id ? { ...item, tools: merged } : item) })
|
||||
runtime.status = 'connected'; runtime.error = undefined
|
||||
})
|
||||
} catch (error) {
|
||||
await client.close().catch(() => undefined)
|
||||
if (runtimes.get(id) === runtime) { runtime.status = runtime.controller.signal.aborted ? 'disconnected' : 'error'; runtime.error = runtime.controller.signal.aborted ? undefined : publicError(error, 'connect') }
|
||||
} finally { runtime.flight = undefined }
|
||||
const latest = (await load()).servers.find(server => server.id === id)
|
||||
if (!latest) throw new Error('MCP 服务已移除')
|
||||
return view(latest)
|
||||
})()
|
||||
return runtime.flight
|
||||
}
|
||||
export async function disconnectMcpServer(id: string): Promise<void> { await writes; requireServer(await load(), id); await closeRuntime(id) }
|
||||
export async function setMcpEnabled(id: string, enabled: boolean): Promise<void> {
|
||||
await mutate(async current => {
|
||||
const server = requireServer(current, id)
|
||||
if (enabled && server.trustedHash !== server.configHash) throw new Error('请先批准当前 MCP 配置')
|
||||
await persist({ ...current, servers: current.servers.map(item => item.id === id ? { ...item, enabled } : item) })
|
||||
if (!enabled) await closeRuntime(id)
|
||||
})
|
||||
if (enabled) await connectMcpServer(id)
|
||||
}
|
||||
export async function setMcpToolEnabled(id: string, toolName: string, enabled: boolean): Promise<void> {
|
||||
await mutate(async current => {
|
||||
const server = requireServer(current, id)
|
||||
if (!server.tools.some(tool => tool.name === toolName)) throw new Error('MCP 工具不存在')
|
||||
await persist({ ...current, servers: current.servers.map(item => item.id === id ? { ...item, tools: item.tools.map(tool => tool.name === toolName ? { ...tool, enabled } : tool) } : item) })
|
||||
})
|
||||
}
|
||||
export async function removeMcpServer(id: string): Promise<void> {
|
||||
await mutate(async current => { requireServer(current, id); await persist({ ...current, servers: current.servers.filter(item => item.id !== id) }); await closeRuntime(id) })
|
||||
}
|
||||
export async function disposeMcpServers(): Promise<void> { await Promise.all([...runtimes.keys()].map(closeRuntime)) }
|
||||
/** Startup reconnect is background work; ordinary sends never await unavailable MCP servers. */
|
||||
export async function initializeMcpServers(): Promise<void> {
|
||||
await writes
|
||||
for (const server of (await load()).servers) if (server.enabled && server.trustedHash === server.configHash) void connectMcpServer(server.id).catch(() => undefined)
|
||||
}
|
||||
function mask(value: unknown, secrets: string[]): unknown {
|
||||
if (typeof value === 'string') { for (const secret of secrets) if (secret) value = (value as string).split(secret).join('[已隐藏]'); return value }
|
||||
if (Array.isArray(value)) return value.map(item => mask(item, secrets))
|
||||
return object(value) ? Object.fromEntries(Object.entries(value).map(([key, item]) => [key, mask(item, secrets)])) : value
|
||||
}
|
||||
/** Namespace and connection generation are captured once; changed configurations fail closed. */
|
||||
export async function snapshotMcpTools(): Promise<{ specs: ToolSpec[]; describe(name: string): { serverName: string; toolName: string } | undefined; execute(name: string, args: Record<string, unknown>, signal: AbortSignal): Promise<{ ok: boolean; content: string; data?: unknown }> }> {
|
||||
await writes
|
||||
await load()
|
||||
const specs: ToolSpec[] = [], mappings = new Map<string, { id: string; serverName: string; tool: string; runtime: Runtime }>()
|
||||
for (const server of (await load()).servers) {
|
||||
const runtime = runtimes.get(server.id)
|
||||
if (!server.enabled || server.trustedHash !== server.configHash || runtime?.status !== 'connected') continue
|
||||
for (const tool of server.tools.filter(tool => tool.enabled)) {
|
||||
const suffix = createHash('sha256').update(server.id + '\0' + tool.name).digest('hex').slice(0, 16)
|
||||
const prefix = tool.name.replace(/[^A-Za-z0-9_-]/g, '_').slice(0, 24) || 'tool', name = `mcp_${prefix}_${suffix}`
|
||||
specs.push({ type: 'function', function: { name, description: `[${server.name}] ${tool.description}`.slice(0, 8000), parameters: clone(tool.inputSchema) } })
|
||||
mappings.set(name, { id: server.id, serverName: server.name, tool: tool.name, runtime })
|
||||
}
|
||||
}
|
||||
return { specs, describe: name => { const mapping = mappings.get(name); return mapping ? { serverName: mapping.serverName, toolName: mapping.tool } : undefined }, execute: async (name, args, signal) => {
|
||||
if (signal.aborted) throw abortError()
|
||||
const mapping = mappings.get(name)
|
||||
if (!mapping) return { ok: false, content: 'MCP 工具不在本回合快照中' }
|
||||
await writes
|
||||
const server = (await load()).servers.find(server => server.id === mapping.id), runtime = mapping.runtime
|
||||
if (!server || !server.enabled || server.trustedHash !== server.configHash || server.configHash !== runtime.configHash
|
||||
|| runtimes.get(mapping.id) !== runtime || runtime.status !== 'connected' || !server.tools.some(tool => tool.name === mapping.tool && tool.enabled)) return { ok: false, content: 'MCP 配置或连接已变化,请重新发起请求' }
|
||||
try {
|
||||
const result = await runtime.client.callTool({ name: mapping.tool, arguments: args }, undefined,
|
||||
{ signal: AbortSignal.any([signal, runtime.controller.signal]), timeout: CALL_TIMEOUT })
|
||||
const data = mask(result, runtime.secretValues) as Record<string, unknown>
|
||||
const content = Array.isArray(data.content) ? data.content.map(part => object(part) && part.type === 'text' && typeof part.text === 'string' ? part.text : JSON.stringify(part)).join('\n') : JSON.stringify(data)
|
||||
return { ok: !result.isError, content, data }
|
||||
} catch (error) {
|
||||
if (signal.aborted) throw abortError()
|
||||
if (runtime.controller.signal.aborted) return { ok: false, content: 'MCP 配置或连接已变化,请重新发起请求' }
|
||||
return { ok: false, content: publicError(error, 'call') }
|
||||
}
|
||||
} }
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { ToolSpec } from '@shared/types'
|
||||
import type { ConversationToolContext, ConversationToolResult } from '../agent/conversation-tools'
|
||||
import { listSkills, buildSkillContext, loadSkillContext, readSkillResource } from './skills'
|
||||
import { snapshotMcpTools } from './mcp'
|
||||
import { expertManagerDocument, expertManagerAdapter, expertManagerResource, expertPackageOperation, expertTurnContext } from './experts'
|
||||
import { agentMode, agentModeInstructions, modeToolDenial, modeToolSpecs } from '../agent/mode-policy'
|
||||
import type { AgentMode } from '@shared/conversation'
|
||||
|
||||
export interface CapabilityToolSnapshot {
|
||||
specs: ToolSpec[]
|
||||
metadata?(name: string): Record<string, unknown> | undefined
|
||||
execute(name: string, args: Record<string, unknown>, ctx: ConversationToolContext): Promise<ConversationToolResult | undefined>
|
||||
}
|
||||
export interface CapabilitySnapshot extends CapabilityToolSnapshot {
|
||||
context: string
|
||||
references: Array<{ id: string; name: string; uri: string }>
|
||||
assistantDisplay?: import('@shared/capabilities').ExpertBinding
|
||||
}
|
||||
|
||||
/** A turn retains stable capability IDs and MCP connection generations. */
|
||||
export async function createCapabilitySnapshot(ids: string[], cwd: string, options: { sessionId?: string; expertId?: string | null; agentMode?: AgentMode } = {}): Promise<CapabilitySnapshot> {
|
||||
const mode = agentMode(options.agentMode)
|
||||
if (!Array.isArray(ids) || ids.length > 32 || ids.some(id => typeof id !== 'string')) throw new Error('技能引用无效')
|
||||
const selected = [...new Set(ids)]
|
||||
const installed = await listSkills(cwd)
|
||||
const references = selected.map(id => {
|
||||
if (id === 'builtin:expert-manager') return { id, name: 'expert-manager', uri: 'skill://expert-manager' }
|
||||
const skill = installed.find(item => item.id === id)
|
||||
if (!skill?.enabled || !skill.userInvocable) throw new Error('引用的技能已禁用或不可手动调用')
|
||||
return { id, name: skill.name, uri: `skill://${encodeURIComponent(skill.name)}` }
|
||||
})
|
||||
const managerId = 'builtin:expert-manager'
|
||||
let managerLoaded = selected.includes(managerId)
|
||||
const managerContext = (): string => `<skill id="${managerId}" name="expert-manager">\n${expertManagerDocument().content}\n</skill>${expertManagerAdapter(options.sessionId ?? '')}`
|
||||
const skillContext = [await buildSkillContext(selected.filter(id => id !== managerId), cwd), managerLoaded ? managerContext() : ''].filter(Boolean).join('\n\n')
|
||||
const available = installed.filter(item => item.enabled && !item.disableModelInvocation)
|
||||
const allowedIds = new Set([...selected, ...available.map(item => item.id), managerId])
|
||||
const mcp = await snapshotMcpTools()
|
||||
// Snapshot first: a concurrent MCP reconfiguration invalidates these generations,
|
||||
// so dependency checks can never authorize a newly substituted connection.
|
||||
const activeExpert = options.expertId ? await expertTurnContext(options.expertId) : undefined
|
||||
const specs: ToolSpec[] = [...mcp.specs]
|
||||
if (options.sessionId) specs.push({ type: 'function', function: { name: 'ycode_expert_package', description: '创建或编辑专家包:read 读取当前包;write 增量保存包内文件;validate 校验;register 原子注册到我的专家。冲突后使用 read(refresh:true) 重新读取最新包。必须先加载 expert-manager 技能。', parameters: { type: 'object', properties: { id: { type: 'string' }, action: { type: 'string', enum: ['read', 'write', 'validate', 'register'] }, refresh: { type: 'boolean' }, files: { type: 'object', additionalProperties: { anyOf: [{ type: 'string' }, { type: 'null' }] } } }, required: ['id', 'action'] } } })
|
||||
if (activeExpert) specs.push({ type: 'function', function: { name: 'ycode_expert_resource', description: '读取当前激活专家包内的 UTF-8 文本资源。', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } } })
|
||||
if (allowedIds.size) {
|
||||
specs.push({ type: 'function', function: { name: 'ycode_load_skill', description: '加载可用技能的完整指令。开始对应任务前先加载技能,遵循其流程。', parameters: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } } })
|
||||
specs.push({ type: 'function', function: { name: 'ycode_read_skill_resource', description: '读取技能包中的文本资源。path 是包内相对路径。', parameters: { type: 'object', properties: { id: { type: 'string' }, path: { type: 'string' } }, required: ['id', 'path'] } } })
|
||||
}
|
||||
const names = new Set(mcp.specs.map(spec => spec.function.name))
|
||||
return {
|
||||
specs: modeToolSpecs(specs, mode),
|
||||
references,
|
||||
assistantDisplay: activeExpert?.display,
|
||||
metadata: name => { const identity = mcp.describe(name); return identity ? { capability: 'mcp', ...identity } : undefined },
|
||||
context: [activeExpert?.context ?? (options.expertId === null ? '<active-expert disabled="true">当前会话已移除专家,后续回复使用默认助手身份,不再沿用之前专家的角色指令。</active-expert>' : ''), skillContext, `可自动调用的技能(先用 ycode_load_skill 加载):\n${[...available.map(item => ({ id: item.id, name: item.name, description: item.description })), { id: managerId, name: 'expert-manager', description: '创建、转换、修改、校验本地专家包。' }].map(item => JSON.stringify(item)).join('\n')}`, agentModeInstructions(mode)].filter(Boolean).join('\n\n'),
|
||||
async execute(name, args, ctx) {
|
||||
if (ctx.signal.aborted) throw Object.assign(new Error('回合已取消'), { name: 'AbortError' })
|
||||
const denial = modeToolDenial(mode, name)
|
||||
if (denial) return denial
|
||||
if (name === 'ycode_expert_package') {
|
||||
if (!managerLoaded || !options.sessionId) return { ok: false, content: '请先加载完整 expert-manager 技能' }
|
||||
try { return { ok: true, content: JSON.stringify(await expertPackageOperation(options.sessionId, args)), metadata: { capability: 'expert-package', action: args.action, expertId: args.id } } } catch (e) { return { ok: false, content: e instanceof Error ? e.message : String(e) } }
|
||||
}
|
||||
if (name === 'ycode_expert_resource' && options.expertId) {
|
||||
const resource = typeof args.path === 'string' ? args.path.replace(/^\.\//, '') : ''
|
||||
const content = activeExpert && Object.hasOwn(activeExpert.resources, resource) ? activeExpert.resources[resource] : undefined
|
||||
return typeof content !== 'string' ? { ok: false, content: '当前专家包中没有此文本资源' } : { ok: true, content }
|
||||
}
|
||||
if (name === 'ycode_load_skill' || name === 'ycode_read_skill_resource') {
|
||||
if (typeof args.id !== 'string' || !allowedIds.has(args.id)) return { ok: false, content: '技能不在当前回合可用范围内' }
|
||||
if (args.id === managerId) {
|
||||
if (name === 'ycode_load_skill') { managerLoaded = true; return { ok: true, content: managerContext() } }
|
||||
return { ok: true, content: expertManagerResource(String(args.path ?? '')) }
|
||||
}
|
||||
const content = name === 'ycode_load_skill' ? await loadSkillContext(args.id, cwd) : await readSkillResource(args.id, String(args.path ?? ''), cwd)
|
||||
return { ok: true, content }
|
||||
}
|
||||
if (!names.has(name)) return undefined
|
||||
if (ctx.permissionMode !== 'full-access') {
|
||||
if (!ctx.interactions) return { ok: false, content: 'MCP 调用需要用户确认' }
|
||||
const identity = mcp.describe(name)
|
||||
const decision = await ctx.interactions.approval(ctx.toolCallId, identity ? `调用 ${identity.serverName} 的 ${identity.toolName} 工具` : `调用连接器工具 ${name}`, [
|
||||
{ optionId: 'allow-once', name: '允许一次', kind: 'allow_once' },
|
||||
{ optionId: 'reject-once', name: '拒绝', kind: 'reject_once' }
|
||||
])
|
||||
if (decision.optionId !== 'allow-once') return { ok: false, content: '用户拒绝了连接器调用' }
|
||||
}
|
||||
const result = await mcp.execute(name, args, ctx.signal)
|
||||
return { ...result, metadata: { capability: 'mcp', ...mcp.describe(name) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
import { app } from 'electron'
|
||||
import { promises as fs } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { createHash, randomUUID } from 'node:crypto'
|
||||
import { parseDocument } from 'yaml'
|
||||
import { Unzip, UnzipInflate, zipSync } from 'fflate'
|
||||
import type { SkillDocument, SkillRecord } from '@shared/capabilities'
|
||||
import { expertManagerDocument, expertManagerResource } from './experts'
|
||||
|
||||
const MAX_PACKAGE = 50 * 1024 * 1024, MAX_ARCHIVE = 20 * 1024 * 1024, MAX_FILES = 2000, MAX_DOCUMENT = 1024 * 1024
|
||||
interface Entry { id: string; name: string; folder: string; enabled: boolean; digest: string }
|
||||
interface Registry { version: 1; entries: Entry[]; disabledWorkspace: string[] }
|
||||
interface PackageFile { name: string; data: Uint8Array }
|
||||
let operations = Promise.resolve()
|
||||
function serialized<T>(run: () => Promise<T>): Promise<T> {
|
||||
const next = operations.then(run); operations = next.then(() => {}, () => {}); return next
|
||||
}
|
||||
const hash = (value: string | Uint8Array): string => createHash('sha256').update(value).digest('hex')
|
||||
const rootPath = (): string => path.join(app.getPath('userData'), 'capabilities', 'skills')
|
||||
function within(root: string, target: string): boolean {
|
||||
const relative = path.relative(root, target)
|
||||
return relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith('..' + path.sep))
|
||||
}
|
||||
function relativeName(value: string): string {
|
||||
if (!value || value.includes('\\') || value.startsWith('/') || /[\x00-\x1f\x7f<>:"|?*]/u.test(value)) throw Error('技能包包含非法相对路径')
|
||||
const parts = value.replace(/\/$/, '').split('/')
|
||||
if (parts.some(part => !part || part === '.' || part === '..' || /[. ]$/.test(part) || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) throw Error('技能包包含不安全的文件路径')
|
||||
return parts.join('/')
|
||||
}
|
||||
/** Check every component before following it; no installed or discovered symlink is trusted. */
|
||||
async function checked(root: string, relative: string): Promise<string> {
|
||||
const target = path.resolve(root, relative)
|
||||
if (!within(root, target)) throw Error('技能资源路径越界')
|
||||
let current = root
|
||||
if ((await fs.lstat(root)).isSymbolicLink()) throw Error('技能目录不能是符号链接')
|
||||
for (const part of path.relative(root, target).split(path.sep).filter(Boolean)) {
|
||||
current = path.join(current, part)
|
||||
if ((await fs.lstat(current)).isSymbolicLink()) throw Error('技能包不能包含符号链接')
|
||||
}
|
||||
const actualRoot = await fs.realpath(root), actual = await fs.realpath(target)
|
||||
if (!within(actualRoot, actual)) throw Error('技能资源路径越界')
|
||||
return actual
|
||||
}
|
||||
async function ensureRoot(): Promise<string> {
|
||||
const root = rootPath()
|
||||
let current = path.parse(path.resolve(root)).root
|
||||
// Validate an ancestor before creating the next component, not after following a symlink.
|
||||
for (const segment of path.relative(current, path.join(root, 'packages')).split(path.sep)) {
|
||||
current = path.join(current, segment)
|
||||
try {
|
||||
const stat = await fs.lstat(current)
|
||||
if (stat.isSymbolicLink() || !stat.isDirectory()) throw Error('技能存储目录不能包含符号链接或文件')
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||
await fs.mkdir(current).catch(async cause => { if ((cause as NodeJS.ErrnoException).code !== 'EEXIST') throw cause })
|
||||
const stat = await fs.lstat(current)
|
||||
if (stat.isSymbolicLink() || !stat.isDirectory()) throw Error('技能存储路径被重定向')
|
||||
}
|
||||
}
|
||||
return root
|
||||
}
|
||||
async function registry(): Promise<Registry> {
|
||||
const root = await ensureRoot()
|
||||
try {
|
||||
const file = await checked(root, 'registry.json'), parsed = JSON.parse(await fs.readFile(file, 'utf8')) as Registry
|
||||
if (parsed.version !== 1 || !Array.isArray(parsed.entries) || !Array.isArray(parsed.disabledWorkspace) || parsed.entries.some(entry => !/^user:[a-f0-9]{24}$/.test(entry.id) || !/^[a-f0-9]{24}$/.test(entry.folder) || typeof entry.name !== 'string' || typeof entry.enabled !== 'boolean')) throw Error('技能目录索引格式无效')
|
||||
return parsed
|
||||
} catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { version: 1, entries: [], disabledWorkspace: [] }; throw error }
|
||||
}
|
||||
async function saveRegistry(value: Registry): Promise<void> {
|
||||
const root = await ensureRoot(), temp = path.join(root, '.registry-' + randomUUID())
|
||||
try { await fs.writeFile(temp, JSON.stringify(value, null, 2), { flag: 'wx' }); await fs.rename(temp, path.join(root, 'registry.json')) }
|
||||
finally { await fs.unlink(temp).catch(() => {}) }
|
||||
}
|
||||
function metadata(content: string): { name: string; description: string; version?: string; enabled: boolean; userInvocable: boolean; disableModelInvocation: boolean } {
|
||||
const match = /^\uFEFF?---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(content)
|
||||
if (!match) throw Error('SKILL.md 缺少 YAML 元数据')
|
||||
const document = parseDocument(match[1], { uniqueKeys: true, schema: 'core' })
|
||||
if (document.errors.length) throw Error('SKILL.md YAML 元数据无效')
|
||||
let value: unknown
|
||||
try { value = document.toJS({ maxAliasCount: 50 }) } catch { throw Error('SKILL.md YAML 引用过多') }
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) throw Error('SKILL.md 元数据必须是对象')
|
||||
const data = value as Record<string, unknown>
|
||||
if (typeof data.name !== 'string' || !data.name.trim() || data.name.trim().length > 128) throw Error('SKILL.md 需要有效 name')
|
||||
if (typeof data.description !== 'string' || !data.description.trim() || data.description.length > 8192) throw Error('SKILL.md 需要有效 description')
|
||||
for (const key of ['enabled', 'disable', 'user-invocable', 'userInvocable', 'disable-model-invocation', 'disableModelInvocation']) if (data[key] !== undefined && typeof data[key] !== 'boolean') throw Error(`SKILL.md 的 ${key} 必须是布尔值`)
|
||||
return { name: data.name.trim(), description: data.description.trim(), version: typeof data.version === 'string' || typeof data.version === 'number' ? String(data.version) : undefined,
|
||||
enabled: data.enabled !== false && data.disable !== true, userInvocable: (data['user-invocable'] ?? data.userInvocable) !== false,
|
||||
disableModelInvocation: (data['disable-model-invocation'] ?? data.disableModelInvocation) === true }
|
||||
}
|
||||
function text(bytes: Uint8Array): string {
|
||||
if (bytes.includes(0)) throw Error('技能资源不是文本文件')
|
||||
try { return new TextDecoder('utf-8', { fatal: true }).decode(bytes) } catch { throw Error('技能文档必须是 UTF-8 文本') }
|
||||
}
|
||||
async function packageFiles(root: string): Promise<PackageFile[]> {
|
||||
const files: PackageFile[] = []; let total = 0
|
||||
await checked(root, '')
|
||||
async function visit(relative: string): Promise<void> {
|
||||
const directory = await checked(root, relative)
|
||||
for (const item of await fs.readdir(directory, { withFileTypes: true })) {
|
||||
const name = relativeName(relative ? relative + '/' + item.name : item.name), file = await checked(root, name)
|
||||
if (item.isDirectory()) { await visit(name); continue }
|
||||
if (!item.isFile()) throw Error('技能包只能包含普通文件和文件夹')
|
||||
const stat = await fs.stat(file)
|
||||
total += stat.size
|
||||
if (total > MAX_PACKAGE || files.length >= MAX_FILES) throw Error('技能包超过大小或文件数量限制')
|
||||
const data = await fs.readFile(file)
|
||||
files.push({ name, data })
|
||||
}
|
||||
}
|
||||
await visit(''); return files
|
||||
}
|
||||
function packageDigest(files: PackageFile[]): string {
|
||||
const digest = createHash('sha256')
|
||||
for (const file of [...files].sort((a, b) => a.name.localeCompare(b.name))) digest.update(file.name).update('\0').update(hash(file.data)).update('\n')
|
||||
return digest.digest('hex')
|
||||
}
|
||||
/** Inspect ZIP central-directory sizes/types/paths before decompressing any entry. */
|
||||
function archiveFiles(bytes: Buffer): PackageFile[] {
|
||||
if (bytes.length > MAX_ARCHIVE) throw Error('技能 ZIP 文件超过 20MB')
|
||||
let end = -1
|
||||
for (let position = bytes.length - 22; position >= Math.max(0, bytes.length - 65557); position--) if (bytes.readUInt32LE(position) === 0x06054b50 && position + 22 + bytes.readUInt16LE(position + 20) === bytes.length) { end = position; break }
|
||||
if (end < 0 || bytes.readUInt16LE(end + 4) !== 0 || bytes.readUInt16LE(end + 6) !== 0) throw Error('技能 ZIP 格式不受支持')
|
||||
const count = bytes.readUInt16LE(end + 10), start = bytes.readUInt32LE(end + 16), size = bytes.readUInt32LE(end + 12)
|
||||
if (!count || count > MAX_FILES || start + size > end || count !== bytes.readUInt16LE(end + 8)) throw Error('技能 ZIP 目录无效')
|
||||
let cursor = start, total = 0; const names = new Set<string>(), declared = new Map<string, number>()
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (cursor + 46 > end || bytes.readUInt32LE(cursor) !== 0x02014b50) throw Error('技能 ZIP 条目无效')
|
||||
const flags = bytes.readUInt16LE(cursor + 8), originalSize = bytes.readUInt32LE(cursor + 24), nameLength = bytes.readUInt16LE(cursor + 28), extra = bytes.readUInt16LE(cursor + 30), comment = bytes.readUInt16LE(cursor + 32)
|
||||
if (cursor + 46 + nameLength + extra + comment > start + size || flags & 1 || originalSize === 0xffffffff) throw Error('不支持加密或 ZIP64 技能包')
|
||||
const raw = text(bytes.subarray(cursor + 46, cursor + 46 + nameLength)), name = relativeName(raw), mode = bytes.readUInt32LE(cursor + 38) >>> 16, type = mode & 0xf000
|
||||
if (type !== 0 && type !== 0x8000 && type !== 0x4000) throw Error('技能 ZIP 不能包含符号链接或设备')
|
||||
const key = name.normalize('NFKC').toLowerCase()
|
||||
if (names.has(key)) throw Error('技能 ZIP 包含重复路径')
|
||||
names.add(key); declared.set(raw, originalSize); total += originalSize
|
||||
if (total > MAX_PACKAGE) throw Error('技能 ZIP 解压后超过 50MB')
|
||||
cursor += 46 + nameLength + extra + comment
|
||||
}
|
||||
if (cursor !== start + size) throw Error('技能 ZIP 目录不完整')
|
||||
const files: PackageFile[] = [], seen = new Set<string>(); let actualTotal = 0, completed = 0
|
||||
const stream = new Unzip(entry => {
|
||||
const expected = declared.get(entry.name), name = relativeName(entry.name)
|
||||
if (expected === undefined || seen.has(entry.name)) throw Error('技能 ZIP 本地目录不匹配')
|
||||
seen.add(entry.name)
|
||||
const chunks: Uint8Array[] = []; let length = 0
|
||||
entry.ondata = (error, chunk, final) => {
|
||||
if (error) throw error
|
||||
length += chunk.length; actualTotal += chunk.length
|
||||
if (length > expected || actualTotal > MAX_PACKAGE) throw Error('技能 ZIP 实际解压大小超过声明或限制')
|
||||
chunks.push(chunk)
|
||||
if (final) {
|
||||
if (length !== expected) throw Error('技能 ZIP 实际解压大小与声明不符')
|
||||
completed++
|
||||
if (!entry.name.endsWith('/')) {
|
||||
const data = new Uint8Array(length); let position = 0
|
||||
for (const part of chunks) { data.set(part, position); position += part.length }
|
||||
files.push({ name, data })
|
||||
}
|
||||
}
|
||||
}
|
||||
entry.start()
|
||||
})
|
||||
stream.register(UnzipInflate)
|
||||
// Bound each inflation step as well as cumulative output; distrust claimed expanded sizes.
|
||||
try { for (let offset = 0; offset < bytes.length; offset += 4096) stream.push(bytes.subarray(offset, offset + 4096), offset + 4096 >= bytes.length) }
|
||||
catch (error) { if (error instanceof Error && error.message.startsWith('技能 ZIP')) throw error; throw Error('技能 ZIP 解压失败') }
|
||||
if (completed !== declared.size) throw Error('技能 ZIP 文件不完整')
|
||||
// Accept a direct package or one enclosing directory; never pick an arbitrary nested skill.
|
||||
if (files.some(file => file.name === 'SKILL.md')) return files
|
||||
const first = files[0]?.name.split('/')[0]
|
||||
if (first && files.every(file => file.name.startsWith(first + '/')) && files.some(file => file.name === first + '/SKILL.md')) return files.map(file => ({ ...file, name: file.name.slice(first.length + 1) }))
|
||||
throw Error('技能 ZIP 需要根目录 SKILL.md 或单个技能文件夹')
|
||||
}
|
||||
async function fromPath(input: string): Promise<PackageFile[]> {
|
||||
const source = path.resolve(input), stat = await fs.lstat(source)
|
||||
if (stat.isSymbolicLink()) throw Error('不能从符号链接导入技能')
|
||||
// Check source ancestors, including a selected SKILL.md's parent.
|
||||
await checked(path.parse(source).root, path.relative(path.parse(source).root, source))
|
||||
if (stat.isDirectory()) return packageFiles(source)
|
||||
if (!stat.isFile()) throw Error('请选择技能文件夹或 ZIP 文件')
|
||||
if (path.basename(source) === 'SKILL.md') return packageFiles(path.dirname(source))
|
||||
if (path.extname(source).toLowerCase() !== '.zip') throw Error('请选择技能文件夹或 ZIP 文件')
|
||||
if (stat.size > MAX_ARCHIVE) throw Error('技能 ZIP 文件超过 20MB')
|
||||
return archiveFiles(await fs.readFile(source))
|
||||
}
|
||||
function record(id: string, folder: string, meta: ReturnType<typeof metadata>, scope: SkillRecord['scope'], enabled: boolean, source: SkillRecord['source'], userModified: boolean): SkillRecord {
|
||||
return { id, ...meta, path: folder, scope, enabled: enabled && meta.enabled, source, userModified }
|
||||
}
|
||||
function invalidRecord(id: string, name: string, folder: string, scope: SkillRecord['scope'], error: unknown): SkillRecord {
|
||||
const code = (error as NodeJS.ErrnoException)?.code
|
||||
const message = code === 'ENOENT' ? '技能包或 SKILL.md 已被移除' : error instanceof Error ? error.message : '技能包无效'
|
||||
return { id, name, description: '', path: folder, scope, source: scope === 'user' ? 'imported' : 'discovered', enabled: false, userInvocable: false, disableModelInvocation: true, userModified: true, error: message }
|
||||
}
|
||||
async function documentAt(folder: string): Promise<string> {
|
||||
const file = await checked(folder, 'SKILL.md'), stat = await fs.stat(file)
|
||||
if (!stat.isFile() || stat.size > MAX_DOCUMENT) throw Error('SKILL.md 文件无效或超过 1MB')
|
||||
return text(await fs.readFile(file))
|
||||
}
|
||||
async function listUnlocked(cwd?: string): Promise<SkillRecord[]> {
|
||||
const state = await registry(), managed = path.join(rootPath(), 'packages'), result: SkillRecord[] = []
|
||||
for (const entry of state.entries) {
|
||||
const folder = path.join(managed, entry.folder)
|
||||
try {
|
||||
await checked(managed, entry.folder)
|
||||
const meta = metadata(await documentAt(folder)), files = await packageFiles(folder)
|
||||
result.push(record(entry.id, folder, meta, 'user', entry.enabled, 'imported', packageDigest(files) !== entry.digest))
|
||||
} catch (error) { result.push(invalidRecord(entry.id, entry.name, folder, 'user', error)) }
|
||||
}
|
||||
if (cwd) {
|
||||
const workspace = await fs.realpath(path.resolve(cwd)).catch(() => undefined)
|
||||
if (!workspace) return result
|
||||
for (const source of ['.agents/skills', '.claude/skills']) {
|
||||
let directory: string
|
||||
try { directory = await checked(workspace, source) } catch { continue }
|
||||
if (!(await fs.stat(directory)).isDirectory()) continue
|
||||
for (const item of await fs.readdir(directory, { withFileTypes: true })) {
|
||||
if (!item.isDirectory() || item.isSymbolicLink()) continue
|
||||
const folder = path.join(directory, item.name), id = 'workspace:' + hash(folder).slice(0, 24)
|
||||
try {
|
||||
await checked(directory, item.name)
|
||||
const meta = metadata(await documentAt(folder))
|
||||
await packageFiles(folder)
|
||||
result.push(record(id, folder, meta, 'workspace', !state.disabledWorkspace.includes(id), 'discovered', false))
|
||||
} catch (error) { result.push(invalidRecord(id, item.name, folder, 'workspace', error)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.sort((a, b) => a.scope === b.scope ? a.name.localeCompare(b.name) : a.scope === 'workspace' ? -1 : 1)
|
||||
}
|
||||
export function listSkills(cwd?: string): Promise<SkillRecord[]> { return serialized(() => listUnlocked(cwd)) }
|
||||
async function findSkill(id: string, cwd?: string): Promise<SkillRecord> {
|
||||
const found = (await listUnlocked(cwd)).find(skill => skill.id === id)
|
||||
if (!found) throw Error('技能不存在或不属于当前空间')
|
||||
if (found.error) throw Error(found.error)
|
||||
return found
|
||||
}
|
||||
export function importSkill(source: string, overwrite = false): Promise<SkillRecord> {
|
||||
return serialized(async () => {
|
||||
const files = await fromPath(source), main = files.find(file => file.name === 'SKILL.md')
|
||||
if (!main || main.data.length > MAX_DOCUMENT) throw Error('技能根目录需要有效 SKILL.md')
|
||||
const meta = metadata(text(main.data)), name = meta.name.normalize('NFKC').toLowerCase(), folder = hash(name).slice(0, 24), id = 'user:' + folder
|
||||
const state = await registry(), existing = state.entries.find(entry => entry.id === id)
|
||||
if (existing && !overwrite) throw Error('已存在同名技能,确认替换后重试')
|
||||
const packages = path.join(rootPath(), 'packages'), destination = path.join(packages, folder), stage = path.join(packages, '.stage-' + randomUUID()), backup = path.join(packages, '.backup-' + randomUUID())
|
||||
let moved = false, committed = false
|
||||
await fs.mkdir(stage)
|
||||
try {
|
||||
for (const file of files) { const target = path.join(stage, relativeName(file.name)); await fs.mkdir(path.dirname(target), { recursive: true }); await fs.writeFile(target, file.data, { flag: 'wx' }) }
|
||||
if (existing) {
|
||||
try { await checked(packages, folder); await fs.rename(destination, backup); moved = true }
|
||||
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error }
|
||||
}
|
||||
await fs.rename(stage, destination)
|
||||
const entry: Entry = { id, name: meta.name, folder, enabled: existing?.enabled ?? true, digest: packageDigest(files) }
|
||||
state.entries = [...state.entries.filter(item => item.id !== id), entry]
|
||||
try { await saveRegistry(state); committed = true } catch (error) { await fs.rm(destination, { recursive: true, force: true }); throw error }
|
||||
return record(id, destination, meta, 'user', entry.enabled, 'imported', false)
|
||||
} finally {
|
||||
await fs.rm(stage, { recursive: true, force: true })
|
||||
if (moved && !committed) await fs.rename(backup, destination)
|
||||
if (committed) await fs.rm(backup, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
}
|
||||
export function readSkill(id: string, cwd?: string): Promise<SkillDocument> {
|
||||
if (id === 'builtin:expert-manager') { const document = expertManagerDocument(); return Promise.resolve({ skill: { id, name: 'expert-manager', description: '创建和修改本地专家包', path: 'builtin://expert-manager', scope: 'user', enabled: true, userInvocable: true, disableModelInvocation: false, source: 'discovered', userModified: false }, ...document }) }
|
||||
return serialized(async () => { const skill = await findSkill(id, cwd); return { skill, content: await documentAt(skill.path), files: (await packageFiles(skill.path)).map(file => file.name).sort() } })
|
||||
}
|
||||
export function setSkillEnabled(id: string, enabled: boolean, cwd?: string): Promise<void> {
|
||||
return serialized(async () => {
|
||||
const skill = await findSkill(id, cwd), state = await registry()
|
||||
if (skill.scope === 'workspace') state.disabledWorkspace = [...state.disabledWorkspace.filter(item => item !== id), ...enabled ? [] : [id]]
|
||||
else state.entries = state.entries.map(entry => entry.id === id ? { ...entry, enabled } : entry)
|
||||
await saveRegistry(state)
|
||||
})
|
||||
}
|
||||
export function removeSkill(id: string): Promise<void> {
|
||||
return serialized(async () => {
|
||||
const state = await registry(), entry = state.entries.find(item => item.id === id)
|
||||
if (!entry) throw Error('只能卸载受管技能副本,空间技能请在其源目录管理')
|
||||
const packages = path.join(rootPath(), 'packages'), target = path.join(packages, entry.folder), tomb = path.join(packages, '.remove-' + randomUUID())
|
||||
await checked(packages, '')
|
||||
let moved = false, symlink = false
|
||||
try { symlink = (await fs.lstat(target)).isSymbolicLink(); await fs.rename(target, tomb); moved = true }
|
||||
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error }
|
||||
try { await saveRegistry({ ...state, entries: state.entries.filter(item => item.id !== id) }) } catch (error) { if (moved) await fs.rename(tomb, target); throw error }
|
||||
if (moved) { if (symlink) await fs.unlink(tomb); else await fs.rm(tomb, { recursive: true, force: true }) }
|
||||
})
|
||||
}
|
||||
export function exportSkill(id: string, destination: string): Promise<string> {
|
||||
return serialized(async () => {
|
||||
const skill = await findSkill(id), files = await packageFiles(skill.path), target = path.resolve(destination)
|
||||
if (!target.toLowerCase().endsWith('.zip')) throw Error('技能导出目标必须是 ZIP 文件')
|
||||
const parent = path.dirname(target)
|
||||
await checked(path.parse(parent).root, path.relative(path.parse(parent).root, parent))
|
||||
if (within(rootPath(), target) || within(skill.path, target)) throw Error('不能导出覆盖技能存储目录')
|
||||
const bytes = zipSync(Object.fromEntries(files.map(file => [file.name, file.data])))
|
||||
// Export never overwrites an existing file or follows a destination symlink.
|
||||
await fs.writeFile(target, bytes, { flag: 'wx' }); return target
|
||||
})
|
||||
}
|
||||
function context(skill: SkillRecord, markdown: string, files: string[]): string {
|
||||
if (markdown.length > 65536) throw Error('技能文档过长,请通过技能资源分段读取')
|
||||
return `\n<skill id=${JSON.stringify(skill.id)} name=${JSON.stringify(skill.name)}>\n${markdown}\n\nAvailable package resources (relative paths):\n${files.map(file => '- ' + file).join('\n')}\n</skill>\n`
|
||||
}
|
||||
export function buildSkillContext(ids: string[], cwd: string): Promise<string> {
|
||||
return serialized(async () => {
|
||||
let result = ''
|
||||
for (const id of new Set(ids)) {
|
||||
const skill = await findSkill(id, cwd)
|
||||
if (!skill.enabled || !skill.userInvocable) throw Error('该技能已禁用或不允许手动调用')
|
||||
result += context(skill, await documentAt(skill.path), (await packageFiles(skill.path)).map(file => file.name))
|
||||
if (result.length > 131072) throw Error('所选技能上下文过长')
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
export function buildAvailableSkills(cwd: string): Promise<string> {
|
||||
return serialized(async () => JSON.stringify((await listUnlocked(cwd)).filter(skill => skill.enabled && !skill.disableModelInvocation).map(skill => ({ id: skill.id, name: skill.name, description: skill.description }))))
|
||||
}
|
||||
export function loadSkillContext(id: string, cwd: string): Promise<string> {
|
||||
return serialized(async () => {
|
||||
const skill = await findSkill(id, cwd)
|
||||
if (!skill.enabled || skill.disableModelInvocation) throw Error('该技能不允许模型自动调用')
|
||||
return context(skill, await documentAt(skill.path), (await packageFiles(skill.path)).map(file => file.name))
|
||||
})
|
||||
}
|
||||
export function readSkillResource(id: string, resource: string, cwd: string): Promise<string> {
|
||||
if (id === 'builtin:expert-manager') return Promise.resolve(expertManagerResource(resource))
|
||||
return serialized(async () => {
|
||||
const skill = await findSkill(id, cwd)
|
||||
if (!skill.enabled) throw Error('该技能已禁用')
|
||||
const file = await checked(skill.path, relativeName(resource)), stat = await fs.stat(file)
|
||||
if (!stat.isFile() || stat.size > MAX_DOCUMENT) throw Error('技能资源无效或过大')
|
||||
return text(await fs.readFile(file))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 应用配置持久化。
|
||||
*
|
||||
* - 落盘位置:userData/config.json
|
||||
* - API Key 优先用 Electron safeStorage 加密(Windows 走 DPAPI),
|
||||
* 若系统不支持则降级为明文并在 UI 上如实标注。
|
||||
* - 渲染进程只能拿到 GatewayConfigView(无明文 Key)。
|
||||
*/
|
||||
|
||||
import { app, safeStorage } from 'electron'
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import type { GatewayConfig, GatewayConfigView } from '@shared/types'
|
||||
|
||||
interface StoredConfig {
|
||||
speechBaseUrl?: string
|
||||
speechModel?: string
|
||||
baseUrl: string
|
||||
model: string
|
||||
temperature: number
|
||||
maxToolRounds: number
|
||||
apiKeyEnc?: string
|
||||
apiKeyPlain?: string
|
||||
workspaceRoot?: string
|
||||
}
|
||||
|
||||
const DEFAULTS: StoredConfig = {
|
||||
baseUrl: 'https://api.deepseek.com/v1',
|
||||
model: 'deepseek-chat',
|
||||
temperature: 0.7,
|
||||
maxToolRounds: 6
|
||||
}
|
||||
|
||||
let cache: StoredConfig | null = null
|
||||
|
||||
function filePath(): string {
|
||||
return join(app.getPath('userData'), 'config.json')
|
||||
}
|
||||
|
||||
async function load(): Promise<StoredConfig> {
|
||||
if (cache) return cache
|
||||
try {
|
||||
const raw = await fs.readFile(filePath(), 'utf8')
|
||||
cache = { ...DEFAULTS, ...(JSON.parse(raw) as Partial<StoredConfig>) }
|
||||
} catch {
|
||||
cache = { ...DEFAULTS }
|
||||
}
|
||||
return cache
|
||||
}
|
||||
|
||||
async function persist(next: StoredConfig): Promise<void> {
|
||||
cache = next
|
||||
const file = filePath()
|
||||
await fs.mkdir(join(file, '..'), { recursive: true })
|
||||
await fs.writeFile(file, JSON.stringify(next, null, 2), 'utf8')
|
||||
}
|
||||
|
||||
function decryptKey(stored: StoredConfig): { key: string; storage: 'encrypted' | 'plaintext' | 'none' } {
|
||||
if (stored.apiKeyEnc) {
|
||||
try {
|
||||
return { key: safeStorage.decryptString(Buffer.from(stored.apiKeyEnc, 'base64')), storage: 'encrypted' }
|
||||
} catch {
|
||||
return { key: '', storage: 'none' }
|
||||
}
|
||||
}
|
||||
if (stored.apiKeyPlain) return { key: stored.apiKeyPlain, storage: 'plaintext' }
|
||||
return { key: '', storage: 'none' }
|
||||
}
|
||||
|
||||
/** 供主进程内部使用:拿到可直接请求上游的完整配置 */
|
||||
export async function getGatewayConfig(): Promise<GatewayConfig> {
|
||||
const stored = await load()
|
||||
const { key } = decryptKey(stored)
|
||||
return {
|
||||
speechBaseUrl: stored.speechBaseUrl,
|
||||
speechModel: stored.speechModel,
|
||||
baseUrl: stored.baseUrl,
|
||||
model: stored.model,
|
||||
temperature: stored.temperature,
|
||||
maxToolRounds: stored.maxToolRounds,
|
||||
apiKey: key
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWorkspaceRoot(): Promise<string> {
|
||||
const stored = await load()
|
||||
return stored.workspaceRoot || join(app.getPath('home'), 'YCodeWorkspace')
|
||||
}
|
||||
|
||||
export async function setWorkspaceRoot(root: string): Promise<void> {
|
||||
const stored = await load()
|
||||
await persist({ ...stored, workspaceRoot: root })
|
||||
}
|
||||
|
||||
/** 供渲染进程使用:脱敏视图 */
|
||||
export async function getConfigView(): Promise<GatewayConfigView> {
|
||||
const stored = await load()
|
||||
const { key, storage } = decryptKey(stored)
|
||||
return {
|
||||
speechBaseUrl: stored.speechBaseUrl,
|
||||
speechModel: stored.speechModel,
|
||||
baseUrl: stored.baseUrl,
|
||||
model: stored.model,
|
||||
temperature: stored.temperature,
|
||||
maxToolRounds: stored.maxToolRounds,
|
||||
hasApiKey: key.length > 0,
|
||||
keyStorage: key.length > 0 ? storage : 'none'
|
||||
}
|
||||
}
|
||||
|
||||
export interface ConfigPatch {
|
||||
speechBaseUrl?: string
|
||||
speechModel?: string
|
||||
baseUrl?: string
|
||||
model?: string
|
||||
temperature?: number
|
||||
maxToolRounds?: number
|
||||
/** 传空字符串表示清除 Key;不传表示保持原值 */
|
||||
apiKey?: string
|
||||
}
|
||||
|
||||
export async function updateConfig(patch: ConfigPatch): Promise<GatewayConfigView> {
|
||||
const stored = await load()
|
||||
const next: StoredConfig = { ...stored }
|
||||
|
||||
if (patch.speechBaseUrl !== undefined) next.speechBaseUrl = patch.speechBaseUrl.trim()
|
||||
if (patch.speechModel !== undefined) next.speechModel = patch.speechModel.trim()
|
||||
|
||||
if (patch.baseUrl !== undefined) next.baseUrl = patch.baseUrl.trim()
|
||||
if (patch.model !== undefined) next.model = patch.model.trim()
|
||||
if (patch.temperature !== undefined) next.temperature = clamp(patch.temperature, 0, 2)
|
||||
if (patch.maxToolRounds !== undefined) next.maxToolRounds = clamp(Math.round(patch.maxToolRounds), 1, 24)
|
||||
|
||||
if (patch.apiKey !== undefined) {
|
||||
const key = patch.apiKey.trim()
|
||||
if (!key) {
|
||||
delete next.apiKeyEnc
|
||||
delete next.apiKeyPlain
|
||||
} else if (safeStorage.isEncryptionAvailable()) {
|
||||
next.apiKeyEnc = safeStorage.encryptString(key).toString('base64')
|
||||
delete next.apiKeyPlain
|
||||
} else {
|
||||
next.apiKeyPlain = key
|
||||
delete next.apiKeyEnc
|
||||
}
|
||||
}
|
||||
|
||||
await persist(next)
|
||||
return getConfigView()
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
if (Number.isNaN(value)) return min
|
||||
return Math.min(max, Math.max(min, value))
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* 自建模型网关(OpenAI 兼容)。
|
||||
*
|
||||
* 职责:
|
||||
* 1. 持有上游 baseUrl / apiKey(永不进入渲染进程)
|
||||
* 2. 按配置地址选择 Chat Completions / Responses + SSE 流式
|
||||
* 3. 把上游的增量 delta 组装成「文本增量」与「完整工具调用」两类输出
|
||||
*
|
||||
* 设计约束:网关只做协议适配与组装,不含业务策略;
|
||||
* 模型路由、上下文裁剪、工具编排由上层(agent/loop.ts)负责。
|
||||
*/
|
||||
|
||||
import type { GatewayConfig, ToolSpec } from '@shared/types'
|
||||
import { resolveOpenAIEndpoint } from '@shared/openai-endpoint'
|
||||
import { parseSse } from './sse'
|
||||
import { buildResponsesRequest, parseResponsesStream } from './responses'
|
||||
|
||||
/** 上游 wire 格式的消息 */
|
||||
export interface WireMessage {
|
||||
role: 'system' | 'user' | 'assistant' | 'tool'
|
||||
content: string | null | Array<{ type: 'text'; text: string } | { type: 'image_url'; image_url: { url: string } }>
|
||||
tool_calls?: WireToolCall[]
|
||||
tool_call_id?: string
|
||||
}
|
||||
|
||||
export interface WireToolCall {
|
||||
id: string
|
||||
type: 'function'
|
||||
function: { name: string; arguments: string }
|
||||
}
|
||||
|
||||
/** 网关输出的增量事件 */
|
||||
export type GatewayDelta =
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'reasoning'; text: string }
|
||||
| { type: 'tool_calls'; calls: WireToolCall[] }
|
||||
| { type: 'finish'; reason: string | null }
|
||||
|
||||
export interface GatewayStreamRequest {
|
||||
messages: WireMessage[]
|
||||
tools?: ToolSpec[]
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
export class GatewayError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly status?: number,
|
||||
readonly body?: string
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'GatewayError'
|
||||
}
|
||||
}
|
||||
|
||||
export class Gateway {
|
||||
constructor(private readonly config: GatewayConfig) {}
|
||||
|
||||
async *stream(req: GatewayStreamRequest): AsyncGenerator<GatewayDelta, void, void> {
|
||||
if (!this.config.apiKey && this.config.authMode !== 'none') {
|
||||
throw new GatewayError('未配置 API Key,请先在设置中填写')
|
||||
}
|
||||
if (!this.config.baseUrl) {
|
||||
throw new GatewayError('未配置上游地址(baseUrl)')
|
||||
}
|
||||
|
||||
let endpoint: ReturnType<typeof resolveOpenAIEndpoint>
|
||||
try { endpoint = resolveOpenAIEndpoint(this.config.baseUrl) } catch { throw new GatewayError('上游地址格式无效') }
|
||||
const responses = endpoint.api === 'openai-responses'
|
||||
const body: Record<string, unknown> = responses ? buildResponsesRequest(this.config, req) : {
|
||||
model: this.config.model,
|
||||
messages: req.messages,
|
||||
stream: true,
|
||||
temperature: this.config.temperature
|
||||
}
|
||||
if (!responses && this.config.maxOutputTokens) body.max_tokens = this.config.maxOutputTokens
|
||||
if (!responses && req.tools && req.tools.length > 0) {
|
||||
body.tools = req.tools
|
||||
body.tool_choice = 'auto'
|
||||
}
|
||||
|
||||
let res: Response
|
||||
try {
|
||||
res = await fetch(endpoint.endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
accept: 'text/event-stream',
|
||||
...(this.config.apiKey ? { authorization: `Bearer ${this.config.apiKey}` } : {})
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: req.signal
|
||||
})
|
||||
} catch (err) {
|
||||
if ((err as Error).name === 'AbortError') throw err
|
||||
throw new GatewayError(responses ? '无法连接 Responses 上游' : `无法连接上游:${(err as Error).message}`)
|
||||
}
|
||||
|
||||
if (!res.ok || !res.body) {
|
||||
// A Responses provider can echo request credentials in arbitrary error bodies.
|
||||
const text = responses ? undefined : await safeReadText(res)
|
||||
if (responses) await res.body?.cancel().catch(() => {})
|
||||
throw new GatewayError(
|
||||
responses ? `Responses 上游返回 HTTP ${res.status}` : `上游返回 ${res.status} ${res.statusText}`.trim(),
|
||||
res.status,
|
||||
text
|
||||
)
|
||||
}
|
||||
|
||||
if (responses) {
|
||||
yield* parseResponsesStream(res.body, (message) => new GatewayError(message), req.signal)
|
||||
return
|
||||
}
|
||||
|
||||
// 累加器:按 index 归并流式工具调用分片
|
||||
const acc = new Map<number, { id: string; name: string; args: string }>()
|
||||
let finishReason: string | null = null
|
||||
|
||||
for await (const evt of parseSse(res.body)) {
|
||||
if (evt.data === '[DONE]') break
|
||||
|
||||
let payload: any
|
||||
try {
|
||||
payload = JSON.parse(evt.data)
|
||||
} catch {
|
||||
continue // 非 JSON 的心跳/注释行直接跳过
|
||||
}
|
||||
|
||||
if (payload?.error) {
|
||||
throw new GatewayError(
|
||||
`上游错误:${payload.error.message ?? JSON.stringify(payload.error)}`
|
||||
)
|
||||
}
|
||||
|
||||
const choice = payload?.choices?.[0]
|
||||
if (!choice) continue
|
||||
const delta = choice.delta ?? {}
|
||||
|
||||
if (typeof delta.content === 'string' && delta.content.length > 0) {
|
||||
yield { type: 'text', text: delta.content }
|
||||
}
|
||||
if (typeof delta.reasoning_content === 'string' && delta.reasoning_content.length > 0) {
|
||||
yield { type: 'reasoning', text: delta.reasoning_content }
|
||||
}
|
||||
|
||||
if (Array.isArray(delta.tool_calls)) {
|
||||
for (const piece of delta.tool_calls) {
|
||||
const idx = typeof piece.index === 'number' ? piece.index : 0
|
||||
const slot = acc.get(idx) ?? { id: '', name: '', args: '' }
|
||||
if (piece.id) slot.id = piece.id
|
||||
if (piece.function?.name) slot.name = piece.function.name
|
||||
if (piece.function?.arguments) slot.args += piece.function.arguments
|
||||
acc.set(idx, slot)
|
||||
}
|
||||
}
|
||||
|
||||
if (choice.finish_reason) finishReason = choice.finish_reason
|
||||
}
|
||||
|
||||
const calls: WireToolCall[] = [...acc.entries()]
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(([idx, slot]) => ({
|
||||
id: slot.id || `call_${idx}_${Date.now()}`,
|
||||
type: 'function' as const,
|
||||
function: { name: slot.name, arguments: slot.args || '{}' }
|
||||
}))
|
||||
|
||||
if (calls.length > 0) yield { type: 'tool_calls', calls }
|
||||
yield { type: 'finish', reason: finishReason }
|
||||
}
|
||||
}
|
||||
|
||||
async function safeReadText(res: Response): Promise<string> {
|
||||
try {
|
||||
return (await res.text()).slice(0, 2000)
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import type { GatewayConfig } from '@shared/types'
|
||||
import type { GatewayDelta, GatewayStreamRequest, WireMessage, WireToolCall } from './index'
|
||||
import { parseSse } from './sse'
|
||||
|
||||
type InputContent = { type: 'input_text'; text: string } | { type: 'input_image'; image_url: string; detail: 'auto' }
|
||||
type InputItem = { role: 'system' | 'user' | 'assistant'; content: string | InputContent[] }
|
||||
| { type: 'function_call'; call_id: string; name: string; arguments: string }
|
||||
| { type: 'function_call_output'; call_id: string; output: string | InputContent[] }
|
||||
|
||||
function content(value: WireMessage['content']): string | InputContent[] {
|
||||
if (!Array.isArray(value)) return value ?? ''
|
||||
return value.map(part => part.type === 'text'
|
||||
? { type: 'input_text', text: part.text }
|
||||
: { type: 'input_image', image_url: part.image_url.url, detail: 'auto' })
|
||||
}
|
||||
|
||||
/** Stateless history replay; call_id is the public function-call ID, never the output item's ID. */
|
||||
export function responsesInput(messages: WireMessage[]): InputItem[] {
|
||||
const input: InputItem[] = []
|
||||
for (const message of messages) {
|
||||
if (message.role === 'tool') {
|
||||
if (!message.tool_call_id) throw new Error('工具结果缺少调用 ID,无法转换为 Responses 输入')
|
||||
input.push({ type: 'function_call_output', call_id: message.tool_call_id, output: content(message.content) })
|
||||
continue
|
||||
}
|
||||
if (message.content !== null || !message.tool_calls?.length) input.push({ role: message.role, content: content(message.content) })
|
||||
if (message.role === 'assistant') for (const call of message.tool_calls ?? []) {
|
||||
input.push({ type: 'function_call', call_id: call.id, name: call.function.name, arguments: call.function.arguments })
|
||||
}
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
export function buildResponsesRequest(config: GatewayConfig, request: GatewayStreamRequest): Record<string, unknown> {
|
||||
const body: Record<string, unknown> = { model: config.model, input: responsesInput(request.messages), stream: true,
|
||||
temperature: config.temperature, store: false }
|
||||
if (config.maxOutputTokens) body.max_output_tokens = config.maxOutputTokens
|
||||
if (request.tools?.length) {
|
||||
body.tools = request.tools.map(tool => ({ type: 'function', name: tool.function.name, description: tool.function.description,
|
||||
parameters: tool.function.parameters, strict: false }))
|
||||
body.tool_choice = 'auto'
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
type Json = Record<string, unknown>
|
||||
const object = (value: unknown): Json => value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Json : {}
|
||||
const string = (value: unknown): string => typeof value === 'string' ? value : ''
|
||||
const index = (value: unknown): number | undefined => typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined
|
||||
interface Call { outputIndex?: number; itemId: string; callId: string; name: string; args: string; final: boolean; status?: string }
|
||||
|
||||
/** OpenAI Responses typed SSE, adapted to the existing GatewayDelta contract.
|
||||
* Only completed responses expose executable calls. Partial/failed/truncated streams throw:
|
||||
* runTurn ignores finish.reason, so returning normally would incorrectly mark them done.
|
||||
* Source: https://developers.openai.com/api/reference/resources/responses/streaming-events
|
||||
*/
|
||||
export async function* parseResponsesStream(stream: ReadableStream<Uint8Array>, error: (message: string) => Error,
|
||||
signal?: AbortSignal): AsyncGenerator<GatewayDelta, void, void> {
|
||||
const calls: Call[] = [], parts = new Map<string, string>(), outputAliases = new Map<number, string>()
|
||||
let completed = false
|
||||
const abort = (): void => { if (signal?.aborted) throw signal.reason instanceof Error ? signal.reason : new DOMException('请求已取消', 'AbortError') }
|
||||
const itemKey = (payload: Json): string => {
|
||||
const i = index(payload.output_index), id = string(payload.item_id)
|
||||
if (id && i !== undefined) {
|
||||
const before = outputAliases.get(i) ?? `index:${i}`
|
||||
if (before !== id) for (const [key, value] of parts) if (key.startsWith(before + '|')) { parts.set(id + key.slice(before.length), value); parts.delete(key) }
|
||||
outputAliases.set(i, id)
|
||||
}
|
||||
return id || (i !== undefined ? outputAliases.get(i) ?? `index:${i}` : 'unattributed')
|
||||
}
|
||||
const callSlot = (payload: Json, item: Json = {}): Call => {
|
||||
const i = index(payload.output_index), itemId = string(item.id) || string(payload.item_id), callId = string(item.call_id)
|
||||
let slot = calls.find(entry => !!itemId && entry.itemId === itemId || !!callId && entry.callId === callId || i !== undefined && entry.outputIndex === i)
|
||||
if (!slot) { slot = { outputIndex: i, itemId, callId, name: '', args: '', final: false }; calls.push(slot) }
|
||||
if (i !== undefined) slot.outputIndex = i
|
||||
if (itemId) slot.itemId = itemId
|
||||
if (callId) slot.callId = callId
|
||||
if (typeof item.name === 'string') slot.name = item.name
|
||||
if (typeof item.status === 'string') slot.status = item.status
|
||||
return slot
|
||||
}
|
||||
// Done/item/completed snapshots contain the entire value: emit only an unseen suffix.
|
||||
// Divergent snapshots cannot be represented as deltas without corrupting visible text.
|
||||
function* text(payload: Json, kind: 'text' | 'reasoning', value: string, snapshot = false): Generator<GatewayDelta> {
|
||||
const key = `${itemKey(payload)}|${kind}|${index(kind === 'reasoning' ? payload.summary_index : payload.content_index) ?? 0}`
|
||||
const prior = parts.get(key) ?? ''
|
||||
if (snapshot && !value.startsWith(prior)) throw error('Responses 上游输出快照与流式内容不一致')
|
||||
const delta = snapshot ? value.slice(prior.length) : value
|
||||
parts.set(key, snapshot ? value : prior + value)
|
||||
if (delta) yield { type: kind, text: delta }
|
||||
}
|
||||
function* output(payload: Json, item: Json, final: boolean): Generator<GatewayDelta> {
|
||||
const keyed = { ...payload, item_id: item.id ?? payload.item_id }
|
||||
itemKey(keyed)
|
||||
if (item.type === 'function_call') {
|
||||
const slot = callSlot(keyed, item)
|
||||
if (final && item.status === undefined) slot.status = 'completed'
|
||||
if (typeof item.arguments === 'string') {
|
||||
if (final) { slot.args = item.arguments; slot.final = true }
|
||||
else if (!slot.args) slot.args = item.arguments
|
||||
}
|
||||
return
|
||||
}
|
||||
if (item.type === 'message' && Array.isArray(item.content)) {
|
||||
for (let i = 0; i < item.content.length; i++) {
|
||||
const part = object(item.content[i])
|
||||
if (part.type === 'output_text') yield* text({ ...keyed, content_index: i }, 'text', string(part.text), true)
|
||||
else if (part.type === 'refusal') yield* text({ ...keyed, content_index: i }, 'text', string(part.refusal), true)
|
||||
}
|
||||
} else if (item.type === 'reasoning' && Array.isArray(item.summary)) {
|
||||
for (let i = 0; i < item.summary.length; i++) {
|
||||
const part = object(item.summary[i])
|
||||
if (part.type === 'summary_text') yield* text({ ...keyed, summary_index: i }, 'reasoning', string(part.text), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
abort()
|
||||
try {
|
||||
for await (const event of parseSse(stream)) {
|
||||
abort()
|
||||
if (event.data === '[DONE]') break
|
||||
let payload: Json
|
||||
try { payload = object(JSON.parse(event.data)) } catch { continue }
|
||||
const type = string(payload.type) || event.event
|
||||
// Never include a raw provider message/body: some gateways echo supplied credentials.
|
||||
if (type === 'error' || type === 'response.failed' || payload.error) throw error('Responses 上游生成失败')
|
||||
if (type === 'response.incomplete') {
|
||||
const reason = string(object(object(payload.response).incomplete_details).reason)
|
||||
throw error(reason === 'max_output_tokens' ? 'Responses 上游达到输出长度限制,回复未完成' : reason === 'content_filter' ? 'Responses 上游过滤了部分内容,回复未完成' : 'Responses 上游回复未完成')
|
||||
}
|
||||
if (type === 'response.output_text.delta') yield* text(payload, 'text', string(payload.delta))
|
||||
else if (type === 'response.output_text.done') yield* text(payload, 'text', string(payload.text), true)
|
||||
else if (type === 'response.refusal.delta') yield* text(payload, 'text', string(payload.delta))
|
||||
else if (type === 'response.refusal.done') yield* text(payload, 'text', string(payload.refusal), true)
|
||||
else if (type === 'response.reasoning_summary_text.delta') yield* text(payload, 'reasoning', string(payload.delta))
|
||||
else if (type === 'response.reasoning_summary_text.done') yield* text(payload, 'reasoning', string(payload.text), true)
|
||||
else if (type === 'response.function_call_arguments.delta') {
|
||||
const slot = callSlot(payload)
|
||||
if (!slot.final) slot.args += string(payload.delta)
|
||||
} else if (type === 'response.function_call_arguments.done') {
|
||||
const slot = callSlot(payload)
|
||||
if (typeof payload.arguments === 'string') { slot.args = payload.arguments; slot.final = true }
|
||||
} else if (type === 'response.output_item.added' || type === 'response.output_item.done') yield* output(payload, object(payload.item), type.endsWith('.done'))
|
||||
else if (type === 'response.content_part.done') {
|
||||
const part = object(payload.part)
|
||||
if (part.type === 'output_text') yield* text(payload, 'text', string(part.text), true)
|
||||
else if (part.type === 'refusal') yield* text(payload, 'text', string(part.refusal), true)
|
||||
} else if (type === 'response.completed') {
|
||||
const response = object(payload.response)
|
||||
if (response.error || response.status !== 'completed' || !Array.isArray(response.output)) throw error('Responses 上游未返回合法成功终态')
|
||||
for (let i = 0; i < response.output.length; i++) yield* output({ output_index: i }, object(response.output[i]), true)
|
||||
completed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
abort()
|
||||
} catch (cause) {
|
||||
abort()
|
||||
if (cause instanceof Error && cause.name === 'AbortError') throw cause
|
||||
// Preserve adapter errors; transport errors contain no provider diagnostics or credentials.
|
||||
if (cause instanceof Error && cause.name === 'GatewayError') throw cause
|
||||
throw error('Responses 上游连接中断,回复未完成')
|
||||
} finally {
|
||||
// parseSse releases its reader before returning. Stop a lingering HTTP body on early terminal events.
|
||||
await stream.cancel().catch(() => {})
|
||||
}
|
||||
if (!completed) throw error('Responses 流提前结束,未收到完成事件')
|
||||
const executable: WireToolCall[] = calls.sort((a, b) => (a.outputIndex ?? 0) - (b.outputIndex ?? 0)).map(slot => {
|
||||
if (!slot.callId || !slot.name || !slot.final || slot.status && slot.status !== 'completed') throw error('Responses 上游工具调用不完整')
|
||||
try { JSON.parse(slot.args) } catch { throw error('Responses 上游工具参数不是有效 JSON') }
|
||||
return { id: slot.callId, type: 'function', function: { name: slot.name, arguments: slot.args } }
|
||||
})
|
||||
if (executable.length) yield { type: 'tool_calls', calls: executable }
|
||||
yield { type: 'finish', reason: executable.length ? 'tool_calls' : 'stop' }
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 极简 SSE(text/event-stream)解析器。
|
||||
*
|
||||
* 只依赖 Web 标准的 ReadableStream,可直接消费 fetch 响应体。
|
||||
* 上游既可能是标准 SSE,也可能在末尾缺少空行,因此以「块边界」为准做收尾。
|
||||
*/
|
||||
|
||||
export interface SseEvent {
|
||||
event?: string
|
||||
data: string
|
||||
}
|
||||
|
||||
export async function* parseSse(
|
||||
stream: ReadableStream<Uint8Array>
|
||||
): AsyncGenerator<SseEvent, void, void> {
|
||||
const reader = stream.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
try {
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
|
||||
let index: number
|
||||
// 事件之间以空行分隔,兼容 \r\n 与 \n
|
||||
while ((index = findBoundary(buffer)) !== -1) {
|
||||
const raw = buffer.slice(0, index)
|
||||
buffer = buffer.slice(index).replace(/^(\r?\n){2}/, '')
|
||||
const parsed = parseBlock(raw)
|
||||
if (parsed) yield parsed
|
||||
}
|
||||
}
|
||||
|
||||
// 收尾:流结束时可能残留最后一个事件
|
||||
buffer += decoder.decode()
|
||||
const tail = parseBlock(buffer)
|
||||
if (tail) yield tail
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
}
|
||||
}
|
||||
|
||||
function findBoundary(buffer: string): number {
|
||||
const a = buffer.indexOf('\n\n')
|
||||
const b = buffer.indexOf('\r\n\r\n')
|
||||
if (a === -1) return b
|
||||
if (b === -1) return a
|
||||
return Math.min(a, b)
|
||||
}
|
||||
|
||||
function parseBlock(block: string): SseEvent | null {
|
||||
const lines = block.split(/\r?\n/)
|
||||
const dataLines: string[] = []
|
||||
let event: string | undefined
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line || line.startsWith(':')) continue
|
||||
if (line.startsWith('event:')) {
|
||||
event = line.slice(6).trim()
|
||||
} else if (line.startsWith('data:')) {
|
||||
dataLines.push(line.slice(5).replace(/^ /, ''))
|
||||
}
|
||||
}
|
||||
|
||||
if (dataLines.length === 0) return null
|
||||
return { event, data: dataLines.join('\n') }
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Electron 主进程入口。
|
||||
*
|
||||
* Main uses an ESM bundle; sandboxed preload remains CommonJS.
|
||||
*/
|
||||
|
||||
import { app, BrowserWindow, Menu, shell, ipcMain } from 'electron'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { disposeOmpKernels, registerIpc } from './ipc'
|
||||
import { disposeMcpServers } from './capabilities/mcp'
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
let settingsWindow: BrowserWindow | null = null
|
||||
let settingsSection = 'model'
|
||||
let settingsAction: 'new' | undefined
|
||||
let allowQuit = false
|
||||
let shutdownPromise: Promise<void> | null = null
|
||||
const mainDirectory = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
function createWindow(): void {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1335,
|
||||
height: 916,
|
||||
minWidth: 480,
|
||||
minHeight: 640,
|
||||
show: false,
|
||||
title: 'YCode',
|
||||
backgroundColor: '#ffffff',
|
||||
// 无边框窗口:标题栏与菜单栏由渲染进程自绘(对齐对标形态)
|
||||
frame: false,
|
||||
webPreferences: {
|
||||
preload: join(mainDirectory, '../preload/index.cjs'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
offscreen: process.env.YCODE_QA_OFFSCREEN === '1'
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.on('ready-to-show', () => mainWindow?.show())
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null
|
||||
})
|
||||
|
||||
// 最大化状态变化要通知渲染进程,用于切换按钮图标
|
||||
const emitState = (): void => {
|
||||
mainWindow?.webContents.send('window:state', {
|
||||
maximized: mainWindow.isMaximized(),
|
||||
fullscreen: mainWindow.isFullScreen()
|
||||
})
|
||||
}
|
||||
mainWindow.on('maximize', emitState)
|
||||
mainWindow.on('unmaximize', emitState)
|
||||
mainWindow.on('enter-full-screen', emitState)
|
||||
mainWindow.on('leave-full-screen', emitState)
|
||||
mainWindow.on('focus', () => mainWindow?.webContents.send('window:focus', { focused: true }))
|
||||
mainWindow.on('blur', () => mainWindow?.webContents.send('window:focus', { focused: false }))
|
||||
|
||||
// 外链一律交给系统浏览器,不在应用内打开
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
void shell.openExternal(url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
const devUrl = process.env['ELECTRON_RENDERER_URL']
|
||||
if (devUrl) {
|
||||
void mainWindow.loadURL(devUrl)
|
||||
} else {
|
||||
void mainWindow.loadFile(join(mainDirectory, '../renderer/index.html'))
|
||||
}
|
||||
}
|
||||
|
||||
app.setName('YCode')
|
||||
// Isolated desktop verification and portable installations can choose their own data directory.
|
||||
if (process.env.YCODE_USER_DATA_DIR) app.setPath('userData', process.env.YCODE_USER_DATA_DIR)
|
||||
|
||||
app.on('before-quit', (event) => {
|
||||
if (allowQuit) return
|
||||
event.preventDefault()
|
||||
if (shutdownPromise) return
|
||||
shutdownPromise = disposeOmpKernels().finally(() => disposeMcpServers())
|
||||
void shutdownPromise
|
||||
.catch(() => undefined)
|
||||
.then(() => {
|
||||
allowQuit = true
|
||||
app.quit()
|
||||
})
|
||||
})
|
||||
|
||||
// 自绘菜单栏:移除原生应用菜单
|
||||
Menu.setApplicationMenu(null)
|
||||
|
||||
/**
|
||||
* 受限环境下的分级降级。
|
||||
*
|
||||
* 现象:如果本进程是由受限的父进程树启动(例如从某个 Electron 宿主内部再拉起,
|
||||
* 或系统策略禁止创建受限令牌),Chromium 的 **GPU 进程沙箱**会无法创建子进程,
|
||||
* 表现为 GPU process 反复以 0xC0000005 崩溃,最终 FATAL 退出应用。
|
||||
*
|
||||
* 两个环境变量,按侵入性从小到大:
|
||||
* YCODE_DISABLE_GPU_SANDBOX=1 仅关闭 GPU 进程沙箱,渲染进程沙箱保留、GPU 仍生效(首选)
|
||||
* YCODE_DISABLE_GPU=1 完全软件渲染(最后手段,性能有损)
|
||||
*/
|
||||
if (process.env.YCODE_DISABLE_GPU === '1') {
|
||||
app.disableHardwareAcceleration()
|
||||
app.commandLine.appendSwitch('disable-gpu')
|
||||
app.commandLine.appendSwitch('disable-gpu-compositing')
|
||||
app.commandLine.appendSwitch('in-process-gpu')
|
||||
} else if (process.env.YCODE_DISABLE_GPU_SANDBOX === '1') {
|
||||
app.commandLine.appendSwitch('disable-gpu-sandbox')
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
registerIpc(() => mainWindow)
|
||||
ipcMain.handle('app:openSettings', async (_event, section = 'model', action?: 'new') => {
|
||||
settingsSection = section === 'models' ? 'model' : String(section)
|
||||
settingsAction = action === 'new' ? 'new' : undefined
|
||||
if (settingsWindow && !settingsWindow.isDestroyed()) {
|
||||
if (!settingsWindow.webContents.isLoading()) settingsWindow.webContents.send('settings:navigate', settingsSection, settingsAction)
|
||||
settingsWindow.show(); settingsWindow.focus(); return
|
||||
}
|
||||
settingsWindow = new BrowserWindow({ width: 900, height: 680, minWidth: 760, minHeight: 560, frame: false, title: 'YCode 设置',
|
||||
show: false, backgroundColor: '#ffffff', webPreferences: { preload: join(mainDirectory, '../preload/index.cjs'), contextIsolation: true, nodeIntegration: false, sandbox: true, offscreen: process.env.YCODE_QA_OFFSCREEN === '1' } })
|
||||
settingsWindow.on('ready-to-show', () => settingsWindow?.show())
|
||||
settingsWindow.on('closed', () => { settingsWindow = null })
|
||||
settingsWindow.webContents.on('did-finish-load', () => settingsWindow?.webContents.send('settings:navigate', settingsSection, settingsAction))
|
||||
settingsWindow.webContents.setWindowOpenHandler(({ url }) => { void shell.openExternal(url); return { action: 'deny' } })
|
||||
const query: Record<string, string> = { window: 'settings', section: settingsSection, ...(settingsAction ? { action: settingsAction } : {}) }
|
||||
const devUrl = process.env.ELECTRON_RENDERER_URL
|
||||
if (devUrl) { const url = new URL(devUrl); Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, value)); await settingsWindow.loadURL(url.toString()) }
|
||||
else await settingsWindow.loadFile(join(mainDirectory, '../renderer/index.html'), { query })
|
||||
})
|
||||
createWindow()
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
})
|
||||
@@ -0,0 +1,77 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { ChatInteraction, InteractionResponse, PermissionOption, QuestionItem } from '@shared/conversation'
|
||||
import type { ChatEvent } from '@shared/types'
|
||||
|
||||
interface PendingInteraction { request: ChatInteraction; resolve(value: InteractionResponse): void; reject(error: Error): void; detach(): void }
|
||||
const pending = new Map<string, PendingInteraction>()
|
||||
|
||||
function abortError(): Error { return Object.assign(new Error('交互已取消'), { name: 'AbortError' }) }
|
||||
|
||||
export function createInteractionBroker(runId: string, signal: AbortSignal, emit: (event: ChatEvent) => void) {
|
||||
async function request(input: ChatInteraction): Promise<InteractionResponse> {
|
||||
if (signal.aborted) throw abortError()
|
||||
return new Promise((resolve, reject) => {
|
||||
const onAbort = (): void => {
|
||||
const entry = pending.get(input.id)
|
||||
if (!entry) return
|
||||
pending.delete(input.id)
|
||||
entry.detach()
|
||||
emit({ runId, type: 'interaction', interaction: { ...input, status: 'cancelled' } })
|
||||
reject(abortError())
|
||||
}
|
||||
pending.set(input.id, { request: input, resolve, reject, detach: () => signal.removeEventListener('abort', onAbort) })
|
||||
signal.addEventListener('abort', onAbort, { once: true })
|
||||
emit({ runId, type: 'interaction', interaction: input })
|
||||
})
|
||||
}
|
||||
return {
|
||||
async question(toolCallId: string, title: string, questions: QuestionItem[]) {
|
||||
const response = await request({ id: randomUUID(), runId, toolCallId, kind: 'question', title, questions, status: 'pending' })
|
||||
if (response.kind !== 'question') throw new Error('问答响应类型不匹配')
|
||||
return response
|
||||
},
|
||||
async approval(toolCallId: string, title: string, options: PermissionOption[], scope?: string) {
|
||||
const id = randomUUID()
|
||||
const response = await request({ id, requestId: id, runId, toolCallId, kind: 'approval', title, options, scope, status: 'pending' })
|
||||
if (response.kind !== 'approval') throw new Error('审批响应类型不匹配')
|
||||
return response
|
||||
}
|
||||
}
|
||||
}
|
||||
export type InteractionBroker = ReturnType<typeof createInteractionBroker>
|
||||
|
||||
/** Exact run/id binding makes stale or duplicated button submissions harmless. */
|
||||
export function respondToInteraction(runId: string, id: string, response: InteractionResponse, emit: (event: ChatEvent) => void): boolean {
|
||||
const entry = pending.get(id)
|
||||
if (!entry || entry.request.runId !== runId) return false
|
||||
const request = entry.request
|
||||
if (request.kind !== response.kind) throw new Error('交互响应类型不匹配')
|
||||
let resolved: ChatInteraction
|
||||
let finalResponse = response
|
||||
if (request.kind === 'approval' && response.kind === 'approval') {
|
||||
const option = request.options.find((item) => item.optionId === response.optionId)
|
||||
if (!option) throw new Error('审批选项无效')
|
||||
resolved = { ...request, selectedOptionId: option.optionId, status: option.kind.startsWith('reject') ? 'rejected' : 'answered' }
|
||||
} else if (request.kind === 'question' && response.kind === 'question') {
|
||||
if (!response.answer || !Array.isArray(response.answer.questions)) throw new Error('问答内容无效')
|
||||
const ids = response.answer.questions.map((item) => item.id)
|
||||
if (new Set(ids).size !== ids.length || ids.some((id) => !request.questions.some((question) => question.id === id))) throw new Error('问题标识无效')
|
||||
if (!response.skipped) {
|
||||
for (const question of request.questions) {
|
||||
const answer = response.answer.questions.find((item) => item.id === question.id)
|
||||
if (!answer || !Array.isArray(answer.answers) || answer.answers.some((value) => typeof value !== 'string' || !value.trim())) throw new Error('回答内容无效')
|
||||
if (!question.multiSelect && answer.answers.length > 1) throw new Error('该问题仅支持单选')
|
||||
}
|
||||
if (!response.answer.questions.some((item) => item.answers.length)) throw new Error('请至少回答一个问题,或跳过全部')
|
||||
}
|
||||
const answer = { title: request.title, questions: request.questions.map((question) => ({ id: question.id, question: question.question,
|
||||
answers: response.skipped ? [] : response.answer.questions.find((item) => item.id === question.id)?.answers ?? [] })) }
|
||||
finalResponse = { ...response, answer }
|
||||
resolved = { ...request, answer, skipped: response.skipped, status: 'answered' }
|
||||
} else throw new Error('交互响应类型不匹配')
|
||||
pending.delete(id)
|
||||
entry.detach()
|
||||
emit({ runId, type: 'interaction', interaction: resolved })
|
||||
entry.resolve(finalResponse)
|
||||
return true
|
||||
}
|
||||
+737
@@ -0,0 +1,737 @@
|
||||
/**
|
||||
* IPC 注册层:渲染进程与主进程之间唯一的契约边界。
|
||||
*
|
||||
* 安全约定:
|
||||
* - 渲染进程无法直接访问 Node / fs / 网络
|
||||
* - API Key 只留在主进程;渲染进程只能读写脱敏视图
|
||||
* - 一次性订阅(on)在 preload 里被包装成「返回取消函数」的形式
|
||||
*/
|
||||
|
||||
import { BrowserWindow, app, dialog, ipcMain, shell, type OpenDialogOptions } from 'electron'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { unlink, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import type { ChatEvent, ChatMessage, WindowState } from '@shared/types'
|
||||
import { freezeAssistantModelMetadata } from '@shared/assistant-model'
|
||||
import type { ConfigPatchLike, EditAction, SendPayload, WorkspaceAttachResult } from '@shared/ipc'
|
||||
import { applyAssistantEvent } from '@shared/message-parts'
|
||||
import * as config from './config'
|
||||
import * as sessions from './sessions'
|
||||
import { Gateway } from './gateway'
|
||||
import { runTurn } from './agent/loop'
|
||||
import { OmpKernel, detectOmpPath, getOmpInfo } from './omp'
|
||||
import { runOmpTurn } from './agent/omp-turn'
|
||||
import { createMessageCheckpoint } from './message-checkpoint'
|
||||
import { classifyWorkspacePath, decodeDownload, imageDownloadPayload, readWorkspaceResource, readWorkspaceText, resolveWorkspaceImage } from './resources'
|
||||
import type { DownloadPayload, DownloadResult, ResourceRange } from '@shared/resources'
|
||||
import type { InteractionResponse } from '@shared/conversation'
|
||||
import { createInteractionBroker, respondToInteraction } from './interaction-broker'
|
||||
import { attachmentPrompt, commitAttachments, getAttachment, removeAttachment, stageAttachment, stageAttachmentPath } from './attachments'
|
||||
import { transcribeAudio } from './speech'
|
||||
import { getRevertChanges } from '@shared/revert'
|
||||
import { restoreWorkspaceFiles } from './workspace-revert'
|
||||
import { assistantWireHistory } from './agent/history'
|
||||
import * as models from './models'
|
||||
import * as workspaces from './workspaces'
|
||||
import { prepareOmpModel } from './omp-model'
|
||||
import type { CustomModelInput, ResolvedModel } from '@shared/models'
|
||||
import type { SessionContext } from '@shared/workspaces'
|
||||
import * as browserHost from './browser-host'
|
||||
import type { BrowserHostBounds } from '@shared/browser-host'
|
||||
import * as terminalHost from './terminal-host'
|
||||
import * as skills from './capabilities/skills'
|
||||
import * as mcp from './capabilities/mcp'
|
||||
import * as experts from './capabilities/experts'
|
||||
import { createCapabilitySnapshot } from './capabilities/runtime'
|
||||
import type { McpServerInput } from '@shared/capabilities'
|
||||
import { agentMode, agentModeContextPatch, thinkingLevel } from './agent/mode-policy'
|
||||
|
||||
/** runId → 取消控制器 */
|
||||
const activeRuns = new Map<string, AbortController>()
|
||||
const runTasks = new Map<string, { sessionId: string; promise: Promise<void> }>()
|
||||
const runEmitters = new Map<string, (event: ChatEvent) => void>()
|
||||
const chatSessionLocks = new Map<string, Promise<void>>()
|
||||
async function withChatSessionLock<T>(id: string, operation: () => Promise<T>): Promise<T> {
|
||||
const previous = chatSessionLocks.get(id) ?? Promise.resolve()
|
||||
let release!: () => void
|
||||
const pending = new Promise<void>((resolve) => { release = resolve })
|
||||
chatSessionLocks.set(id, pending)
|
||||
await previous
|
||||
try { return await operation() }
|
||||
finally { release(); if (chatSessionLocks.get(id) === pending) chatSessionLocks.delete(id) }
|
||||
}
|
||||
|
||||
interface OmpKernelEntry {
|
||||
kernel: OmpKernel
|
||||
workspaceRoot: string
|
||||
appliedBranchId?: string
|
||||
fingerprint?: string
|
||||
}
|
||||
async function applyPendingNativeBranch(sessionId: string, kernel: OmpKernel): Promise<void> {
|
||||
const pendingId = await sessions.getPendingNativeBranch(sessionId)
|
||||
const entry = ompKernels.get(sessionId)
|
||||
if (!pendingId || entry?.appliedBranchId === pendingId) return
|
||||
const response = await kernel.send({ type: 'branch', entryId: pendingId })
|
||||
if (response.success === false) throw new Error('无法恢复已保存的原生会话分支')
|
||||
if (entry) entry.appliedBranchId = pendingId
|
||||
}
|
||||
|
||||
/** Each YCode session gets a native OMP transcript; existing YCode-only history is not imported. */
|
||||
const ompKernels = new Map<string, OmpKernelEntry>()
|
||||
const ompKernelLocks = new Map<string, Promise<void>>()
|
||||
const ompRetirements = new Map<string, Promise<void>>()
|
||||
let ompShutdownStarted = false
|
||||
let disposeAllPromise: Promise<void> | null = null
|
||||
|
||||
function ompSessionPath(sessionId: string): string {
|
||||
return join(
|
||||
app.getPath('userData'),
|
||||
'omp-sessions',
|
||||
`session-${encodeURIComponent(sessionId)}.jsonl`
|
||||
)
|
||||
}
|
||||
|
||||
async function withOmpKernelLock<T>(sessionId: string, operation: () => Promise<T>): Promise<T> {
|
||||
const previous = ompKernelLocks.get(sessionId) ?? Promise.resolve()
|
||||
let release = (): void => {}
|
||||
const current = new Promise<void>((resolve) => {
|
||||
release = resolve
|
||||
})
|
||||
ompKernelLocks.set(sessionId, current)
|
||||
await previous
|
||||
try {
|
||||
return await operation()
|
||||
} finally {
|
||||
release()
|
||||
if (ompKernelLocks.get(sessionId) === current) ompKernelLocks.delete(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
function evictOmpKernel(sessionId: string, kernel: OmpKernel): void {
|
||||
if (ompKernels.get(sessionId)?.kernel !== kernel) return
|
||||
ompKernels.delete(sessionId)
|
||||
const retirement = Promise.resolve()
|
||||
.then(() => kernel.dispose())
|
||||
.catch(() => undefined)
|
||||
ompRetirements.set(sessionId, retirement)
|
||||
void retirement.then(() => {
|
||||
if (ompRetirements.get(sessionId) === retirement) ompRetirements.delete(sessionId)
|
||||
})
|
||||
}
|
||||
|
||||
async function waitForOmpRetirement(sessionId: string): Promise<void> {
|
||||
await ompRetirements.get(sessionId)
|
||||
}
|
||||
|
||||
async function getOmpKernel(sessionId: string, workspaceRoot: string, model?: ResolvedModel | null): Promise<OmpKernel | null> {
|
||||
return withOmpKernelLock(sessionId, async () => {
|
||||
if (ompShutdownStarted) return null
|
||||
|
||||
const prepared = model ? await prepareOmpModel(model) : null
|
||||
const existing = ompKernels.get(sessionId)
|
||||
if (existing?.workspaceRoot === workspaceRoot && existing?.fingerprint === prepared?.fingerprint) return existing.kernel
|
||||
|
||||
if (existing) {
|
||||
ompKernels.delete(sessionId)
|
||||
await existing.kernel.dispose()
|
||||
}
|
||||
await waitForOmpRetirement(sessionId)
|
||||
|
||||
const exePath = detectOmpPath()
|
||||
if (!exePath) return null
|
||||
|
||||
const kernel = new OmpKernel(exePath, workspaceRoot, prepared?.profile ?? 'ycode', ompSessionPath(sessionId), prepared?.model, prepared?.env)
|
||||
const entry: OmpKernelEntry = { kernel, workspaceRoot, fingerprint: prepared?.fingerprint }
|
||||
const evict = (): void => evictOmpKernel(sessionId, kernel)
|
||||
kernel.on('error', evict)
|
||||
kernel.on('exit', evict)
|
||||
ompKernels.set(sessionId, entry)
|
||||
return kernel
|
||||
})
|
||||
}
|
||||
|
||||
export async function disposeOmpKernels(): Promise<void> {
|
||||
if (disposeAllPromise) return disposeAllPromise
|
||||
ompShutdownStarted = true
|
||||
disposeAllPromise = (async () => {
|
||||
for (const controller of activeRuns.values()) controller.abort()
|
||||
while (ompKernelLocks.size > 0) {
|
||||
await Promise.all([...ompKernelLocks.values()])
|
||||
}
|
||||
const entries = [...ompKernels.values()]
|
||||
ompKernels.clear()
|
||||
await Promise.all(
|
||||
entries.map(async ({ kernel }) => {
|
||||
await kernel.dispose().catch(() => undefined)
|
||||
})
|
||||
)
|
||||
await Promise.all([...ompRetirements.values()])
|
||||
await Promise.all([...runTasks.values()].map((task) => task.promise))
|
||||
})()
|
||||
return disposeAllPromise
|
||||
}
|
||||
|
||||
async function disposeSessionKernel(sessionId: string): Promise<void> {
|
||||
await withOmpKernelLock(sessionId, async () => {
|
||||
const entry = ompKernels.get(sessionId)
|
||||
if (entry) ompKernels.delete(sessionId)
|
||||
if (entry) await entry.kernel.dispose()
|
||||
await waitForOmpRetirement(sessionId)
|
||||
})
|
||||
}
|
||||
|
||||
async function removeOmpSessionFile(sessionId: string): Promise<void> {
|
||||
try {
|
||||
await unlink(ompSessionPath(sessionId))
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err
|
||||
}
|
||||
}
|
||||
|
||||
export function registerIpc(getWindow: () => BrowserWindow | null): void {
|
||||
void mcp.initializeMcpServers().catch(() => undefined)
|
||||
ipcMain.handle('skills:list', (_event, cwd?: string) => skills.listSkills(cwd))
|
||||
ipcMain.handle('experts:list', () => experts.listExperts())
|
||||
ipcMain.handle('experts:read', (_event, id: string) => experts.readExpert(id))
|
||||
ipcMain.handle('experts:activate', (_event, id: string) => experts.activateExpert(id))
|
||||
ipcMain.handle('experts:remove', (_event, id: string) => experts.removeExpert(id))
|
||||
ipcMain.handle('experts:open-path', async (_event, id: string) => { const { expert } = await experts.readExpert(id); const error = await shell.openPath(expert.expertRootDir); if (error) throw Error(error) })
|
||||
ipcMain.handle('skills:read', (_event, id: string, cwd?: string) => skills.readSkill(id, cwd))
|
||||
ipcMain.handle('skills:enable', (_event, id: string, enabled: boolean, cwd?: string) => skills.setSkillEnabled(id, enabled, cwd))
|
||||
ipcMain.handle('skills:remove', (_event, id: string) => skills.removeSkill(id))
|
||||
ipcMain.handle('skills:import', async (event, path?: string, overwrite?: boolean, kind?: 'directory' | 'zip') => {
|
||||
if (!path) {
|
||||
const result = await dialog.showOpenDialog(senderWindow(event), kind === 'zip' ? { title: '选择技能 ZIP', properties: ['openFile'], filters: [{ name: 'ZIP', extensions: ['zip'] }] } : { title: '选择技能文件夹', properties: ['openDirectory'] })
|
||||
if (result.canceled || !result.filePaths[0]) return null
|
||||
path = result.filePaths[0]
|
||||
}
|
||||
return skills.importSkill(path, overwrite)
|
||||
})
|
||||
ipcMain.handle('skills:export', async (event, id: string, destination?: string) => {
|
||||
if (!destination) {
|
||||
const skill = (await skills.listSkills()).find(item => item.id === id)
|
||||
if (!skill) throw new Error('技能不存在')
|
||||
const result = await dialog.showSaveDialog(senderWindow(event), { title: '导出技能', defaultPath: `${skill.name}.zip`, filters: [{ name: 'ZIP', extensions: ['zip'] }] })
|
||||
if (result.canceled || !result.filePath) return null
|
||||
destination = result.filePath
|
||||
}
|
||||
return skills.exportSkill(id, destination)
|
||||
})
|
||||
ipcMain.handle('mcp:list', () => mcp.listMcpServers())
|
||||
ipcMain.handle('mcp:save', (_event, input: McpServerInput) => mcp.saveMcpServer(input))
|
||||
ipcMain.handle('mcp:trust', (_event, id: string, hash: string) => mcp.trustMcpServer(id, hash))
|
||||
ipcMain.handle('mcp:connect', (_event, id: string) => mcp.connectMcpServer(id))
|
||||
ipcMain.handle('mcp:disconnect', (_event, id: string) => mcp.disconnectMcpServer(id))
|
||||
ipcMain.handle('mcp:enable', (_event, id: string, enabled: boolean) => mcp.setMcpEnabled(id, enabled))
|
||||
ipcMain.handle('mcp:tool-enable', (_event, id: string, tool: string, enabled: boolean) => mcp.setMcpToolEnabled(id, tool, enabled))
|
||||
ipcMain.handle('mcp:remove', (_event, id: string) => mcp.removeMcpServer(id))
|
||||
function senderWindow(event: Electron.IpcMainInvokeEvent): BrowserWindow {
|
||||
const window = BrowserWindow.fromWebContents(event.sender)
|
||||
if (!window) throw new Error('宿主窗口已关闭')
|
||||
return window
|
||||
}
|
||||
ipcMain.handle('terminal:create', async (event, sessionId?: string, cols?: number, rows?: number) => terminalHost.createTerminal(senderWindow(event), await sessionRoot(sessionId), sessionId, cols, rows))
|
||||
ipcMain.handle('terminal:get', (event, id: string, sessionId?: string) => terminalHost.getTerminal(senderWindow(event), id, sessionId))
|
||||
ipcMain.handle('terminal:write', (event, id: string, data: string, sessionId?: string) => terminalHost.writeTerminal(senderWindow(event), id, data, sessionId))
|
||||
ipcMain.handle('terminal:resize', (event, id: string, cols: number, rows: number, sessionId?: string) => terminalHost.resizeTerminal(senderWindow(event), id, cols, rows, sessionId))
|
||||
ipcMain.handle('terminal:clear', (event, id: string, sessionId?: string) => terminalHost.clearTerminal(senderWindow(event), id, sessionId))
|
||||
ipcMain.handle('terminal:close', (event, id: string, sessionId?: string) => terminalHost.closeTerminal(senderWindow(event), id, sessionId))
|
||||
ipcMain.handle('terminal:rebind', async (event, id: string, previousSessionId: string | undefined, nextSessionId: string) => terminalHost.rebindTerminal(senderWindow(event), id, previousSessionId, nextSessionId, await sessionRoot(nextSessionId)))
|
||||
ipcMain.handle('browser:create', async (event, sessionId?: string, url?: string) => {
|
||||
if (sessionId) await sessions.getSessionContext(sessionId)
|
||||
return browserHost.createBrowser(senderWindow(event), sessionId, url)
|
||||
})
|
||||
ipcMain.handle('browser:rebind', async (event, id: string, previousSessionId: string | undefined, nextSessionId: string) => {
|
||||
await sessions.getSessionContext(nextSessionId)
|
||||
return browserHost.rebindBrowser(senderWindow(event), id, previousSessionId, nextSessionId)
|
||||
})
|
||||
ipcMain.handle('browser:get', (event, id: string, sessionId?: string) => browserHost.getBrowserState(senderWindow(event), id, sessionId))
|
||||
ipcMain.handle('browser:navigate', (event, id: string, url: string, sessionId?: string) => browserHost.navigateBrowser(senderWindow(event), id, url, sessionId))
|
||||
ipcMain.handle('browser:back', (event, id: string, sessionId?: string) => browserHost.backBrowser(senderWindow(event), id, sessionId))
|
||||
ipcMain.handle('browser:forward', (event, id: string, sessionId?: string) => browserHost.forwardBrowser(senderWindow(event), id, sessionId))
|
||||
ipcMain.handle('browser:reload', (event, id: string, sessionId?: string) => browserHost.reloadBrowser(senderWindow(event), id, sessionId))
|
||||
ipcMain.handle('browser:bounds', (event, id: string, bounds: BrowserHostBounds | null, sessionId?: string) => browserHost.setBrowserBounds(senderWindow(event), id, bounds, sessionId))
|
||||
ipcMain.handle('browser:close', (event, id: string, sessionId?: string) => browserHost.closeBrowser(senderWindow(event), id, sessionId))
|
||||
function changed(): void {
|
||||
const windows = typeof BrowserWindow.getAllWindows === 'function' ? BrowserWindow.getAllWindows() : [getWindow()]
|
||||
windows.forEach((win) => win?.webContents.send('settings:changed'))
|
||||
}
|
||||
async function sessionRoot(sessionId?: string): Promise<string> {
|
||||
if (sessionId) return (await sessions.getSessionContext(sessionId)).cwd ?? await config.getWorkspaceRoot()
|
||||
return workspaces.workspaceRoot()
|
||||
}
|
||||
ipcMain.handle('resources:openExternal', async (_event, input: string) => {
|
||||
const url = new URL(input)
|
||||
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('链接协议不受支持')
|
||||
await shell.openExternal(url.toString())
|
||||
})
|
||||
ipcMain.handle('attachments:pick', async () => {
|
||||
const options: OpenDialogOptions = { properties: ['openFile', 'multiSelections'], title: '添加文件' }
|
||||
const win = getWindow()
|
||||
const result = win ? await dialog.showOpenDialog(win, options) : await dialog.showOpenDialog(options)
|
||||
if (result.canceled) return []
|
||||
return Promise.all(result.filePaths.map(stageAttachmentPath))
|
||||
})
|
||||
ipcMain.handle('attachments:stage', async (_event, payload: { name: string; mimeType: string; dataBase64: string }) => {
|
||||
if (!payload || typeof payload.name !== 'string' || typeof payload.dataBase64 !== 'string' || payload.dataBase64.length > 28 * 1024 * 1024) throw new Error('附件数据无效或过大')
|
||||
return stageAttachment(payload.name, payload.mimeType, Buffer.from(payload.dataBase64, 'base64'))
|
||||
})
|
||||
ipcMain.handle('attachments:remove', (_event, id: string) => removeAttachment(id))
|
||||
ipcMain.handle('speech:transcribe', async (_event, payload: { audioBase64: string; mimeType: string }) => transcribeAudio(await config.getGatewayConfig(), payload))
|
||||
ipcMain.handle('chat:respond', (_event, runId: string, interactionId: string, response: InteractionResponse) => {
|
||||
const emit = runEmitters.get(runId)
|
||||
return { accepted: !!emit && respondToInteraction(runId, interactionId, response, emit) }
|
||||
})
|
||||
const saveDownload = async (payload: DownloadPayload): Promise<DownloadResult> => {
|
||||
const { filename, bytes } = decodeDownload(payload)
|
||||
const win = getWindow()
|
||||
const options = { title: '保存文件', defaultPath: join(app.getPath('downloads'), filename) }
|
||||
const result = win ? await dialog.showSaveDialog(win, options) : await dialog.showSaveDialog(options)
|
||||
if (result.canceled || !result.filePath) return { saved: false }
|
||||
await writeFile(result.filePath, bytes)
|
||||
return { saved: true, path: result.filePath }
|
||||
}
|
||||
ipcMain.handle('resources:list', async (_event, sessionId?: string, workspaceId?: string) => {
|
||||
const root = sessionId ? await sessionRoot(sessionId) : workspaceId ? await workspaces.workspaceRoot(workspaceId) : await sessionRoot(undefined)
|
||||
const resource = await readWorkspaceResource(root, '.')
|
||||
if (resource.kind !== 'directory') return []
|
||||
return resource.entries.filter(entry => !['.git', 'node_modules'].includes(entry.name)).slice(0, 100)
|
||||
})
|
||||
ipcMain.handle('resources:read', async (_event, path: string, range?: ResourceRange, sessionId?: string) => readWorkspaceResource(await sessionRoot(sessionId), path, range))
|
||||
ipcMain.handle('resources:read-text', async (_event, path: string, sessionId?: string) => readWorkspaceText(await sessionRoot(sessionId), path))
|
||||
ipcMain.handle('resources:classify', async (_event, path: string, sessionId?: string) => classifyWorkspacePath(await sessionRoot(sessionId), path))
|
||||
ipcMain.handle('resources:resolveImage', async (_event, source: string, sessionId?: string) => resolveWorkspaceImage(await sessionRoot(sessionId), source))
|
||||
ipcMain.handle('resources:download', (_event, payload: DownloadPayload) => saveDownload(payload))
|
||||
ipcMain.handle('resources:downloadImage', async (_event, source: string, filename?: string, sessionId?: string) => saveDownload(await imageDownloadPayload(await sessionRoot(sessionId), source, filename)))
|
||||
// ---------- 窗口控制(无边框窗口自绘标题栏用) ----------
|
||||
ipcMain.handle('window:minimize', (event) => {
|
||||
;(BrowserWindow.fromWebContents?.(event.sender) ?? getWindow())?.minimize()
|
||||
})
|
||||
ipcMain.handle('window:toggleMaximize', (event) => {
|
||||
const win = BrowserWindow.fromWebContents?.(event.sender) ?? getWindow()
|
||||
if (!win) return { maximized: false }
|
||||
if (win.isMaximized()) win.unmaximize()
|
||||
else win.maximize()
|
||||
return { maximized: win.isMaximized() }
|
||||
})
|
||||
ipcMain.handle('window:close', (event) => {
|
||||
;(BrowserWindow.fromWebContents?.(event.sender) ?? getWindow())?.close()
|
||||
})
|
||||
ipcMain.handle('window:state', (event): WindowState => {
|
||||
const win = BrowserWindow.fromWebContents?.(event.sender) ?? getWindow()
|
||||
return {
|
||||
maximized: win?.isMaximized() ?? false,
|
||||
fullscreen: win?.isFullScreen() ?? false
|
||||
}
|
||||
})
|
||||
|
||||
// ---------- 编辑动作(菜单项 → 原生 role 行为) ----------
|
||||
ipcMain.handle('edit:action', (_event, action: EditAction) => {
|
||||
const wc = getWindow()?.webContents
|
||||
if (!wc) return
|
||||
switch (action) {
|
||||
case 'undo':
|
||||
wc.undo()
|
||||
break
|
||||
case 'redo':
|
||||
wc.redo()
|
||||
break
|
||||
case 'cut':
|
||||
wc.cut()
|
||||
break
|
||||
case 'copy':
|
||||
wc.copy()
|
||||
break
|
||||
case 'paste':
|
||||
wc.paste()
|
||||
break
|
||||
case 'selectAll':
|
||||
wc.selectAll()
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// ---------- 应用信息 / 目录 ----------
|
||||
ipcMain.handle('app:info', async () => ({
|
||||
name: app.getName(),
|
||||
version: app.getVersion(),
|
||||
electron: process.versions.electron,
|
||||
chrome: process.versions.chrome,
|
||||
node: process.versions.node,
|
||||
configDir: app.getPath('userData'),
|
||||
workspace: '',
|
||||
omp: getOmpInfo()
|
||||
}))
|
||||
ipcMain.handle('app:openConfigDir', async () => {
|
||||
await shell.openPath(app.getPath('userData'))
|
||||
})
|
||||
|
||||
// ---------- 配置 ----------
|
||||
ipcMain.handle('config:get', () => config.getConfigView())
|
||||
ipcMain.handle('config:update', (_event, patch: ConfigPatchLike) => config.updateConfig(patch))
|
||||
ipcMain.handle('models:list', () => models.listModels())
|
||||
ipcMain.handle('models:save', async (_event, input: CustomModelInput, overwrite?: boolean) => {
|
||||
const result = await models.saveModel(input, overwrite)
|
||||
if (input.previousId && input.previousId !== input.id) {
|
||||
for (const session of await sessions.listSessions()) if (session.modelId === input.previousId) await sessions.updateSessionContext(session.id, { modelId: input.id })
|
||||
}
|
||||
changed()
|
||||
return result
|
||||
})
|
||||
ipcMain.handle('models:remove', async (_event, id: string) => {
|
||||
const result = await models.deleteModel(id)
|
||||
for (const session of await sessions.listSessions()) if (session.modelId === id) await sessions.updateSessionContext(session.id, { modelId: result.selectedModelId ?? undefined })
|
||||
changed()
|
||||
return result
|
||||
})
|
||||
ipcMain.handle('models:select', async (_event, id: string | null) => { const result = await models.setSelectedModel(id); changed(); return result })
|
||||
ipcMain.handle('models:test', (_event, input: CustomModelInput) => models.testModel(input))
|
||||
|
||||
// ---------- 工作目录 ----------
|
||||
ipcMain.handle('workspace:get', async () => ({ root: await config.getWorkspaceRoot() }))
|
||||
ipcMain.handle('workspace:list', () => workspaces.listWorkspaces())
|
||||
ipcMain.handle('workspace:generate', (_event, name: string) => workspaces.generateWorkspace(name))
|
||||
ipcMain.handle('workspace:select', (_event, id: string | null) => workspaces.selectWorkspace(id))
|
||||
ipcMain.handle('workspace:rename', (_event, id: string, name: string) => workspaces.renameWorkspace(id, name))
|
||||
ipcMain.handle('workspace:remove', (_event, id: string) => workspaces.removeWorkspace(id))
|
||||
ipcMain.handle('workspace:open', async (_event, id: string) => {
|
||||
const error = await shell.openPath(await workspaces.workspaceRoot(id))
|
||||
if (error) throw new Error(error)
|
||||
})
|
||||
ipcMain.handle('workspace:attach', async (_event, path?: string): Promise<WorkspaceAttachResult> => {
|
||||
if (path !== undefined) {
|
||||
if (typeof path !== 'string' || !path.trim()) throw new Error('文件夹路径无效')
|
||||
const result = await workspaces.attachWorkspace(path)
|
||||
changed()
|
||||
return { ...result, attachedWorkspaceId: result.selectedWorkspaceId ?? undefined }
|
||||
}
|
||||
const win = getWindow()
|
||||
const options: OpenDialogOptions = { title: '打开已有文件夹', properties: ['openDirectory'] }
|
||||
const result = win ? await dialog.showOpenDialog(win, options) : await dialog.showOpenDialog(options)
|
||||
if (result.canceled || !result.filePaths[0]) return workspaces.listWorkspaces()
|
||||
const registry = await workspaces.attachWorkspace(result.filePaths[0])
|
||||
changed()
|
||||
return { ...registry, attachedWorkspaceId: registry.selectedWorkspaceId ?? undefined }
|
||||
})
|
||||
ipcMain.handle('workspace:choose', async () => {
|
||||
const win = getWindow()
|
||||
const options: OpenDialogOptions = {
|
||||
title: '选择工作目录',
|
||||
properties: ['openDirectory', 'createDirectory']
|
||||
}
|
||||
const result = win
|
||||
? await dialog.showOpenDialog(win, options)
|
||||
: await dialog.showOpenDialog(options)
|
||||
if (result.canceled || result.filePaths.length === 0) {
|
||||
return { root: await config.getWorkspaceRoot(), changed: false }
|
||||
}
|
||||
await config.setWorkspaceRoot(result.filePaths[0])
|
||||
return { root: result.filePaths[0], changed: true }
|
||||
})
|
||||
|
||||
// ---------- 会话 ----------
|
||||
ipcMain.handle('session:list', () => sessions.listSessions())
|
||||
ipcMain.handle('session:create', async (_event, title?: string, context: SessionContext = {}) => {
|
||||
context = agentModeContextPatch({}, context)
|
||||
const workspaceId = context.workspaceId ?? (await workspaces.listWorkspaces()).selectedWorkspaceId ?? undefined
|
||||
const cwd = await workspaces.workspaceRoot(workspaceId)
|
||||
const modelId = context.modelId ?? (await models.listModels()).selectedModelId ?? undefined
|
||||
if (modelId) await models.resolveModel(modelId)
|
||||
if (context.expertId) await experts.activateExpert(context.expertId)
|
||||
return sessions.createSession(title, { workspaceId, cwd, modelId, agentMode: context.agentMode, ...(context.thinkingLevel !== undefined ? { thinkingLevel: thinkingLevel(context.thinkingLevel) } : {}), ...(context.expertId !== undefined ? { expertId: context.expertId } : {}) })
|
||||
})
|
||||
ipcMain.handle('session:update-context', (_event, id: string, patch: SessionContext) => withChatSessionLock(id, async () => {
|
||||
if ([...runTasks.values()].some((run) => run.sessionId === id)) throw new Error('请先停止当前运行')
|
||||
const previous = await sessions.getSessionContext(id)
|
||||
patch = agentModeContextPatch(previous, patch)
|
||||
// A conversation keeps its directory; selecting a different space starts a new task.
|
||||
if ((previous.cwd && patch.cwd && patch.cwd !== previous.cwd) || (previous.workspaceId && patch.workspaceId && patch.workspaceId !== previous.workspaceId)) throw new Error('请在目标空间新建任务')
|
||||
const context: SessionContext = { agentMode: patch.agentMode }
|
||||
if (patch.workspaceId && !previous.workspaceId) {
|
||||
const cwd = await workspaces.workspaceRoot(patch.workspaceId)
|
||||
if (previous.cwd && previous.cwd.toLowerCase() !== cwd.toLowerCase()) throw new Error('请在目标空间新建任务')
|
||||
context.workspaceId = patch.workspaceId
|
||||
context.cwd = cwd
|
||||
} else if (patch.cwd && !previous.cwd) throw new Error('请先选择空间')
|
||||
if (patch.modelId && !(await models.resolveModel(patch.modelId))) throw new Error('模型不存在')
|
||||
if (patch.modelId !== undefined) context.modelId = patch.modelId
|
||||
if (patch.thinkingLevel !== undefined) context.thinkingLevel = thinkingLevel(patch.thinkingLevel)
|
||||
if (patch.expertId !== undefined) {
|
||||
if (patch.expertId !== null) await experts.activateExpert(patch.expertId)
|
||||
context.expertId = patch.expertId
|
||||
}
|
||||
return sessions.updateSessionContext(id, context)
|
||||
}))
|
||||
ipcMain.handle('session:messages', (_event, sessionId: string) => sessions.getMessages(sessionId))
|
||||
ipcMain.handle('session:rename', (_event, sessionId: string, title: string) =>
|
||||
sessions.renameSession(sessionId, title)
|
||||
)
|
||||
ipcMain.handle('session:delete', async (_event, sessionId: string) => withChatSessionLock(sessionId, async () => {
|
||||
const tasks = [...runTasks.entries()].filter(([, task]) => task.sessionId === sessionId)
|
||||
for (const [runId] of tasks) activeRuns.get(runId)?.abort()
|
||||
await disposeSessionKernel(sessionId)
|
||||
await Promise.all(tasks.map(([, task]) => task.promise))
|
||||
await removeOmpSessionFile(sessionId)
|
||||
return sessions.deleteSession(sessionId)
|
||||
}))
|
||||
|
||||
// ---------- 对话 ----------
|
||||
ipcMain.handle('chat:feedback', async (_event, sessionId: string, messageId: string, value: 'like' | 'dislike') => {
|
||||
if (!['like', 'dislike'].includes(value)) throw new Error('反馈值无效')
|
||||
const message = (await sessions.getMessages(sessionId)).find((item) => item.id === messageId && item.role === 'assistant')
|
||||
if (!message) throw new Error('未找到助手消息')
|
||||
await sessions.patchMessage(sessionId, messageId, { feedback: value })
|
||||
})
|
||||
ipcMain.handle('chat:revert', async (_event, sessionId: string, messageId: string) => withChatSessionLock(sessionId, async () => {
|
||||
if ([...runTasks.values()].some((run) => run.sessionId === sessionId)) throw new Error('请先停止当前运行')
|
||||
const history = [...await sessions.getMessages(sessionId)]
|
||||
const index = history.findIndex((item) => item.id === messageId && item.role === 'user')
|
||||
const changes = getRevertChanges(history, messageId)
|
||||
if (!changes.length) throw new Error('此版本没有可恢复的文件修改')
|
||||
const workspace = await sessionRoot(sessionId)
|
||||
const kernel = await getOmpKernel(sessionId, workspace, await models.resolveModel((await sessions.getSessionContext(sessionId)).modelId))
|
||||
if (kernel && !history[index].nativeEntryId) throw new Error('缺少原生会话分支,无法同时恢复上下文')
|
||||
const undo = await restoreWorkspaceFiles(workspace, changes)
|
||||
try {
|
||||
if (kernel) {
|
||||
const response = await kernel.branch(history[index].nativeEntryId!)
|
||||
if (response.success === false) throw new Error('原生会话恢复失败')
|
||||
const entry = ompKernels.get(sessionId)
|
||||
if (entry) entry.appliedBranchId = history[index].nativeEntryId!
|
||||
}
|
||||
const messages = history.slice(0, index)
|
||||
await sessions.replaceMessages(sessionId, messages, 'revert', { pendingNativeBranchId: kernel ? history[index].nativeEntryId : undefined })
|
||||
return { messages }
|
||||
} catch (error) { await disposeSessionKernel(sessionId); await undo(); throw error }
|
||||
}))
|
||||
ipcMain.handle('chat:send', async (_event, payload: SendPayload) => withChatSessionLock(payload.sessionId, async () => {
|
||||
if (ompShutdownStarted) throw new Error('应用正在退出')
|
||||
const win = getWindow()
|
||||
const { sessionId, text } = payload
|
||||
if (!(await sessions.listSessions()).some((session) => session.id === sessionId)) throw new Error('会话不存在')
|
||||
if (typeof text !== 'string' || (!text.trim() && !payload.attachments?.length)) throw new Error('消息为空')
|
||||
if ([...runTasks.values()].some((run) => run.sessionId === sessionId)) throw new Error('该会话正在运行')
|
||||
const attachments = await Promise.all((payload.attachments ?? []).map((item) => getAttachment(item.id)))
|
||||
const promptAttachments = await attachmentPrompt(attachments)
|
||||
const context = await sessions.getSessionContext(sessionId)
|
||||
const mode = agentMode(context.agentMode)
|
||||
const selectedModel = await models.resolveModel(context.modelId)
|
||||
if (!selectedModel) throw new Error('请先添加并选择自定义模型')
|
||||
const snapshot = selectedModel?.config ?? await config.getGatewayConfig()
|
||||
const workspaceRoot = await sessionRoot(sessionId)
|
||||
const references = [...new Set(payload.resources ?? [])]
|
||||
const capabilitySnapshot = await createCapabilitySnapshot(payload.skillIds ?? [], workspaceRoot, { sessionId, expertId: context.expertId, agentMode: mode })
|
||||
for (const path of references) {
|
||||
if (typeof path !== 'string' || await classifyWorkspacePath(workspaceRoot, path) === 'unknown') throw new Error('引用资源不在工作区内或已不存在')
|
||||
}
|
||||
const kernel = await getOmpKernel(sessionId, workspaceRoot, selectedModel)
|
||||
if (kernel) await applyPendingNativeBranch(sessionId, kernel)
|
||||
const nativeBefore = kernel ? await kernel.send({ type: 'get_branch_messages' }) : null
|
||||
const nativeBeforeIds = new Set((nativeBefore?.data as { messages?: Array<{ entryId?: string }> } | undefined)?.messages?.map((item) => item.entryId) ?? [])
|
||||
await commitAttachments(attachments)
|
||||
let history = [...await sessions.getMessages(sessionId)]
|
||||
let selectedNativeBranch: string | undefined
|
||||
if (payload.action) {
|
||||
let targetIndex = history.findIndex((message) => message.id === payload.fromMessageId)
|
||||
if (targetIndex < 0) throw new Error('未找到需要重发的消息')
|
||||
if (payload.action === 'regenerate') {
|
||||
if (history[targetIndex].role !== 'assistant') throw new Error('只能重新生成助手消息')
|
||||
targetIndex = history.slice(0, targetIndex).findLastIndex((message) => message.role === 'user')
|
||||
}
|
||||
const target = history[targetIndex]
|
||||
if (!target || target.role !== 'user') throw new Error('未找到对应用户消息')
|
||||
if (kernel) {
|
||||
if (target.nativeSessionPath && target.nativeSessionPath !== ompSessionPath(sessionId)) throw new Error('这条历史消息没有当前 OMP 会话的分支记录')
|
||||
let entryId = target.nativeEntryId
|
||||
if (!entryId) {
|
||||
const following = history[targetIndex + 1]
|
||||
const prefixUsers = history.slice(0, targetIndex).filter((message) => message.role === 'user')
|
||||
const nativeUsers = (nativeBefore?.data as { messages?: Array<{ entryId: string; text: string }> } | undefined)?.messages ?? []
|
||||
const isLastInput = !history.slice(targetIndex + 1).some((message) => message.role === 'user')
|
||||
const canRecover = isLastInput && following?.role === 'assistant' && ['cancelled', 'error'].includes(following.status ?? '') &&
|
||||
prefixUsers.every((message, index) => !!message.nativeEntryId && message.nativeEntryId === nativeUsers[index]?.entryId)
|
||||
if (!canRecover) throw new Error('这条历史消息没有当前 OMP 会话的分支记录')
|
||||
const attached = await attachmentPrompt(target.attachments ?? [])
|
||||
const nativePrompt = [target.content, attached.text, target.resources?.length ? `引用工作区资源:\n${target.resources.join('\n')}` : '', target.capabilityContext].filter(Boolean).join('\n\n')
|
||||
if (nativeUsers.length === prefixUsers.length + 1 && nativeUsers.at(-1)?.text === nativePrompt) entryId = nativeUsers.at(-1)!.entryId
|
||||
else if (nativeUsers.length !== prefixUsers.length) throw new Error('中断会话的原生历史与消息记录不一致')
|
||||
}
|
||||
if (entryId) {
|
||||
try {
|
||||
const result = await kernel.send({ type: 'branch', entryId })
|
||||
if (result.success === false) throw new Error(String(result.error ?? '原生会话分支失败'))
|
||||
} catch (error) { await disposeSessionKernel(sessionId); throw error }
|
||||
selectedNativeBranch = entryId
|
||||
}
|
||||
const entry = ompKernels.get(sessionId)
|
||||
if (entry && entryId) entry.appliedBranchId = entryId
|
||||
}
|
||||
history = history.slice(0, targetIndex)
|
||||
}
|
||||
const runId = randomUUID()
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
id: randomUUID(),
|
||||
role: 'user',
|
||||
content: text,
|
||||
requestId: runId,
|
||||
planId: payload.planId,
|
||||
attachments,
|
||||
resources: references,
|
||||
skills: capabilitySnapshot.references,
|
||||
capabilityContext: capabilitySnapshot.context || undefined,
|
||||
expertId: context.expertId ?? undefined,
|
||||
createdAt: Date.now(),
|
||||
status: 'done'
|
||||
}
|
||||
let assistant: ChatMessage = {
|
||||
id: randomUUID(),
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
...freezeAssistantModelMetadata(selectedModel.profile),
|
||||
assistantDisplay: capabilitySnapshot.assistantDisplay,
|
||||
requestId: runId,
|
||||
tools: [],
|
||||
parts: [],
|
||||
createdAt: Date.now(),
|
||||
turnStartedAt: userMessage.createdAt,
|
||||
status: 'streaming'
|
||||
}
|
||||
try { await sessions.startTurn(sessionId, userMessage, assistant, history, payload.action, selectedNativeBranch) }
|
||||
catch (error) { if (selectedNativeBranch) await disposeSessionKernel(sessionId); throw error }
|
||||
|
||||
// 回合开始时冻结配置:运行中改设置不影响本回合
|
||||
const controller = new AbortController()
|
||||
activeRuns.set(runId, controller)
|
||||
|
||||
const publish = (event: ChatEvent): void => {
|
||||
if (!win?.isDestroyed?.() && !win?.webContents.isDestroyed?.()) {
|
||||
// Window teardown must not interrupt persistence or sidecar cleanup.
|
||||
try { win?.webContents.send('chat:event', { ...event, sessionId }) } catch { /* window closed */ }
|
||||
}
|
||||
}
|
||||
const checkpoint = createMessageCheckpoint(
|
||||
(message) => sessions.patchMessage(sessionId, message.id, {
|
||||
content: message.content,
|
||||
tools: message.tools,
|
||||
parts: message.parts,
|
||||
interactions: message.interactions,
|
||||
status: message.status,
|
||||
turnStartedAt: message.turnStartedAt,
|
||||
turnDurationMs: message.turnDurationMs,
|
||||
error: message.error
|
||||
}),
|
||||
(error) => console.error('会话增量保存失败,将在回合结束时重试', error)
|
||||
)
|
||||
let terminalEvent: ChatEvent | null = null
|
||||
const emit = (event: ChatEvent): void => {
|
||||
const isTerminal = event.type === 'done' || event.type === 'cancelled' || event.type === 'error'
|
||||
const observedEvent: ChatEvent = isTerminal
|
||||
? { ...event, turnDurationMs: Math.max(0, Date.now() - userMessage.createdAt) } : event
|
||||
assistant = applyAssistantEvent(assistant, observedEvent)
|
||||
if (isTerminal) {
|
||||
terminalEvent = observedEvent
|
||||
} else {
|
||||
checkpoint.schedule(assistant)
|
||||
publish(event)
|
||||
}
|
||||
}
|
||||
runEmitters.set(runId, emit)
|
||||
const interactionBroker = createInteractionBroker(runId, controller.signal, emit)
|
||||
const promptText = [text, promptAttachments.text, references.length ? `引用工作区资源:\n${references.join('\n')}` : '', capabilitySnapshot.context].filter(Boolean).join('\n\n')
|
||||
|
||||
// 回合在后台推进,立刻把 runId 还给渲染进程
|
||||
const initialMessages = payload.action ? structuredClone([...history, userMessage, assistant]) : undefined
|
||||
const task = (async () => {
|
||||
try {
|
||||
// 内核选择:优先 OMP(外部 Agent 内核),不可用时回退自建循环
|
||||
if (kernel) {
|
||||
const result = await runOmpTurn({
|
||||
capabilityTools: capabilitySnapshot,
|
||||
agentMode: mode,
|
||||
thinkingLevel: context.thinkingLevel,
|
||||
runId,
|
||||
kernel,
|
||||
supportsToolCall: selectedModel?.profile.supportsToolCall,
|
||||
userText: promptText,
|
||||
images: promptAttachments.images,
|
||||
workspaceRoot,
|
||||
permissionMode: payload.permissionMode ?? 'default-sandbox',
|
||||
interactions: interactionBroker,
|
||||
signal: controller.signal,
|
||||
emit
|
||||
})
|
||||
assistant.content = result.content
|
||||
assistant.tools = result.tools
|
||||
// 终态由内核回合给出(abort 后到达的 agent_end 记为 cancelled)
|
||||
assistant.status = result.status
|
||||
const branch = await kernel.send({ type: 'get_branch_messages' })
|
||||
const data = branch.data as { messages?: Array<{ entryId?: string; text?: string }> } | undefined
|
||||
const native = data?.messages?.at(-1)
|
||||
if (native?.entryId && !nativeBeforeIds.has(native.entryId) && native.text === promptText) {
|
||||
await sessions.patchMessage(sessionId, userMessage.id, { nativeEntryId: native.entryId, nativeSessionPath: ompSessionPath(sessionId) })
|
||||
await sessions.setPendingNativeBranch(sessionId, undefined)
|
||||
}
|
||||
} else {
|
||||
const wireHistory = (await Promise.all(history.map(async (message) => {
|
||||
if (message.role === 'assistant') return assistantWireHistory(message)
|
||||
const attached = await attachmentPrompt(message.attachments ?? [])
|
||||
const content = [message.content, attached.text, message.resources?.length ? `引用工作区资源:\n${message.resources.join('\n')}` : '', message.capabilityContext].filter(Boolean).join('\n\n')
|
||||
return [{ role: message.role, content: attached.images.length ? [{ type: 'text' as const, text: content }, ...attached.images.map((image) => ({ type: 'image_url' as const, image_url: { url: `data:${image.mimeType};base64,${image.data}` } }))] : content }]
|
||||
}))).flat()
|
||||
const result = await runTurn({
|
||||
capabilityTools: capabilitySnapshot,
|
||||
agentMode: mode,
|
||||
runId,
|
||||
history,
|
||||
wireHistory,
|
||||
userText: promptText,
|
||||
images: promptAttachments.images,
|
||||
interactions: interactionBroker,
|
||||
permissionMode: payload.permissionMode ?? 'default-sandbox',
|
||||
signal: controller.signal,
|
||||
emit,
|
||||
gateway: new Gateway(snapshot),
|
||||
supportsToolCall: selectedModel?.profile.supportsToolCall,
|
||||
model: snapshot.model,
|
||||
maxToolRounds: snapshot.maxToolRounds,
|
||||
workspaceRoot
|
||||
})
|
||||
assistant.content = result.content
|
||||
assistant.tools = result.tools
|
||||
// 旧循环只在正常完成时走到这里;abort/error 走下面的 catch
|
||||
assistant.status = 'done'
|
||||
}
|
||||
} catch (err) {
|
||||
if ((err as Error).name === 'AbortError' || controller.signal.aborted) {
|
||||
emit({ runId, type: 'cancelled' })
|
||||
} else {
|
||||
emit({ runId, type: 'error', message: err instanceof Error ? err.message : String(err) })
|
||||
}
|
||||
} finally {
|
||||
controller.abort()
|
||||
try {
|
||||
await checkpoint.finish(assistant)
|
||||
if (terminalEvent) publish(terminalEvent)
|
||||
} catch (error) {
|
||||
publish({ runId, type: 'error', message: `会话保存失败:${error instanceof Error ? error.message : String(error)}` })
|
||||
} finally {
|
||||
activeRuns.delete(runId)
|
||||
runEmitters.delete(runId)
|
||||
}
|
||||
}
|
||||
})()
|
||||
runTasks.set(runId, { sessionId, promise: task })
|
||||
void task.finally(() => runTasks.delete(runId))
|
||||
|
||||
return { runId, assistantMessageId: assistant.id, modelId: assistant.modelId, modelName: assistant.modelName,
|
||||
assistantDisplay: assistant.assistantDisplay, userMessage, ...(initialMessages ? { messages: initialMessages } : {}) }
|
||||
}))
|
||||
|
||||
ipcMain.handle('chat:cancel', (_event, runId: string) => {
|
||||
const controller = activeRuns.get(runId)
|
||||
if (!controller) return { cancelled: false }
|
||||
controller.abort()
|
||||
return { cancelled: true }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { ChatMessage } from '@shared/types'
|
||||
|
||||
/** Coalesce streaming snapshots; the terminal write always follows any in-flight write. */
|
||||
export function createMessageCheckpoint(
|
||||
save: (message: ChatMessage) => Promise<void>,
|
||||
onError: (error: unknown) => void,
|
||||
interval = 100
|
||||
): { schedule(message: ChatMessage): void; finish(message: ChatMessage): Promise<void> } {
|
||||
let pending: ChatMessage | null = null
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
let writing: Promise<void> = Promise.resolve()
|
||||
let busy = false
|
||||
let closed = false
|
||||
|
||||
const arm = (): void => {
|
||||
if (closed || busy || timer || !pending) return
|
||||
timer = setTimeout(() => {
|
||||
timer = null
|
||||
const snapshot = pending
|
||||
pending = null
|
||||
if (!snapshot) return
|
||||
busy = true
|
||||
writing = Promise.resolve()
|
||||
.then(() => save(snapshot))
|
||||
.catch(onError)
|
||||
.finally(() => {
|
||||
busy = false
|
||||
arm()
|
||||
})
|
||||
}, interval)
|
||||
}
|
||||
|
||||
return {
|
||||
schedule(message) {
|
||||
if (closed) return
|
||||
pending = { ...message }
|
||||
arm()
|
||||
},
|
||||
async finish(message) {
|
||||
closed = true
|
||||
pending = null
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = null
|
||||
await writing
|
||||
await save(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
/** Local custom models. Credentials stay in the main process; legacy config is read only. */
|
||||
import { app, safeStorage } from 'electron'
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { CustomModelInput, CustomModelProfile, ModelRegistryView, ModelTestResult, ResolvedModel } from '@shared/models'
|
||||
import { resolveOpenAIEndpoint } from '@shared/openai-endpoint'
|
||||
import { getGatewayConfig } from './config'
|
||||
|
||||
interface StoredModel extends Omit<CustomModelProfile, 'hasApiKey' | 'keyStorage'> {
|
||||
apiKeyEnc?: string
|
||||
apiKeyPlain?: string
|
||||
}
|
||||
interface StoredRegistry {
|
||||
version: 1
|
||||
models: StoredModel[]
|
||||
selectedModelId: string | null
|
||||
}
|
||||
|
||||
let cache: StoredRegistry | null = null
|
||||
let loading: Promise<StoredRegistry> | null = null
|
||||
let writes: Promise<unknown> = Promise.resolve()
|
||||
|
||||
function filePath(): string { return join(app.getPath('userData'), 'models.json') }
|
||||
|
||||
function readKey(model: StoredModel): { key: string; storage: CustomModelProfile['keyStorage'] } {
|
||||
if (model.apiKeyEnc) {
|
||||
try { return { key: safeStorage.decryptString(Buffer.from(model.apiKeyEnc, 'base64')), storage: 'encrypted' } }
|
||||
catch { return { key: '', storage: 'none' } }
|
||||
}
|
||||
return model.apiKeyPlain ? { key: model.apiKeyPlain, storage: 'plaintext' } : { key: '', storage: 'none' }
|
||||
}
|
||||
|
||||
function writeKey(model: StoredModel, key: string): void {
|
||||
delete model.apiKeyEnc
|
||||
delete model.apiKeyPlain
|
||||
if (!key) return
|
||||
try {
|
||||
if (safeStorage.isEncryptionAvailable()) model.apiKeyEnc = safeStorage.encryptString(key).toString('base64')
|
||||
else model.apiKeyPlain = key
|
||||
} catch { throw new Error('无法安全保存 API Key,请检查系统加密服务') }
|
||||
}
|
||||
|
||||
function viewOf(model: StoredModel): CustomModelProfile {
|
||||
const { apiKeyEnc: _encrypted, apiKeyPlain: _plain, ...profile } = model
|
||||
const { key, storage } = readKey(model)
|
||||
return { ...profile, supportedEfforts: profile.supportedEfforts?.slice(), hasApiKey: !!key, keyStorage: key ? storage : 'none' }
|
||||
}
|
||||
|
||||
function registryView(registry: StoredRegistry): ModelRegistryView {
|
||||
return { models: registry.models.map(viewOf), selectedModelId: registry.selectedModelId }
|
||||
}
|
||||
|
||||
async function persist(registry: StoredRegistry): Promise<void> {
|
||||
const file = filePath()
|
||||
await fs.mkdir(join(file, '..'), { recursive: true })
|
||||
const temporary = `${file}.${randomUUID()}.tmp`
|
||||
try {
|
||||
await fs.writeFile(temporary, JSON.stringify(registry, null, 2), { encoding: 'utf8', mode: 0o600 })
|
||||
await fs.rename(temporary, file)
|
||||
cache = registry
|
||||
} catch (error) {
|
||||
await fs.unlink(temporary).catch(() => undefined)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function readRegistry(persistMigration: boolean): Promise<StoredRegistry> {
|
||||
let raw: string
|
||||
try { raw = await fs.readFile(filePath(), 'utf8') }
|
||||
catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||
const legacy = await getGatewayConfig()
|
||||
const migrated: StoredModel = {
|
||||
id: 'legacy-default', name: legacy.model, providerId: 'custom',
|
||||
model: legacy.model, baseUrl: legacy.baseUrl, authMode: 'api-key',
|
||||
supportsToolCall: true, supportsImages: false, supportsReasoning: false,
|
||||
temperature: legacy.temperature, maxToolRounds: legacy.maxToolRounds
|
||||
}
|
||||
writeKey(migrated, legacy.apiKey)
|
||||
const initial: StoredRegistry = { version: 1, models: [migrated], selectedModelId: migrated.id }
|
||||
if (persistMigration) await persist(initial)
|
||||
return initial
|
||||
}
|
||||
let parsed: Partial<StoredRegistry> & { schemaVersion?: number }
|
||||
try { parsed = JSON.parse(raw) as typeof parsed }
|
||||
catch { throw new Error('模型配置文件损坏,请保留 models.json 后检查内容') }
|
||||
// Accept either version marker without rewriting existing secrets during a read.
|
||||
if (!parsed || typeof parsed !== 'object' || (parsed.version ?? parsed.schemaVersion) !== 1 || !Array.isArray(parsed.models)) {
|
||||
throw new Error('无法读取此版本的模型配置文件')
|
||||
}
|
||||
const ids = new Set<string>()
|
||||
for (const model of parsed.models) {
|
||||
if (!model || typeof model.id !== 'string' || !model.id || ids.has(model.id)
|
||||
|| typeof model.model !== 'string' || typeof model.baseUrl !== 'string'
|
||||
|| typeof model.name !== 'string' || typeof model.providerId !== 'string') {
|
||||
throw new Error('模型配置内容无效,请保留 models.json 后检查内容')
|
||||
}
|
||||
ids.add(model.id)
|
||||
}
|
||||
const selectedModelId = parsed.selectedModelId === null ? null
|
||||
: typeof parsed.selectedModelId === 'string' && ids.has(parsed.selectedModelId)
|
||||
? parsed.selectedModelId : parsed.models[0]?.id ?? null
|
||||
return { version: 1, models: parsed.models, selectedModelId }
|
||||
}
|
||||
|
||||
async function load(): Promise<StoredRegistry> {
|
||||
if (cache) return cache
|
||||
if (loading) return loading
|
||||
loading = readRegistry(true).then(registry => { cache = registry; return registry })
|
||||
try { return await loading }
|
||||
finally { loading = null }
|
||||
}
|
||||
|
||||
function mutate(operation: (registry: StoredRegistry) => Promise<ModelRegistryView>): Promise<ModelRegistryView> {
|
||||
const next = writes.then(async () => operation(await load()))
|
||||
writes = next.catch(() => undefined)
|
||||
return next
|
||||
}
|
||||
|
||||
function normalize(input: CustomModelInput, existing?: StoredModel, testing = false): StoredModel {
|
||||
if (!input || typeof input !== 'object') throw new Error('模型配置无效')
|
||||
const providerId = typeof input.providerId === 'string' ? input.providerId.trim() : ''
|
||||
const model = typeof input.model === 'string' ? input.model.trim() : ''
|
||||
const baseUrl = typeof input.baseUrl === 'string' ? input.baseUrl.trim().replace(/\/+$/, '') : ''
|
||||
if (!providerId) throw new Error('请选择供应商')
|
||||
if (!model) throw new Error('请输入模型名称')
|
||||
if (!baseUrl) throw new Error('请输入 API 地址')
|
||||
let endpoint: URL
|
||||
try { endpoint = new URL(baseUrl) } catch { throw new Error('请输入有效的 API 地址') }
|
||||
if (!['http:', 'https:'].includes(endpoint.protocol) || endpoint.username || endpoint.password) throw new Error('API 地址必须使用 http 或 https,且不能包含账户密码')
|
||||
if (endpoint.search || endpoint.hash) throw new Error('API 地址不能包含查询参数或片段')
|
||||
const id = typeof input.id === 'string' ? input.id.trim() : ''
|
||||
const name = typeof input.name === 'string' ? input.name.trim() : ''
|
||||
if (!id) throw new Error('模型标识不能为空')
|
||||
if (!name) throw new Error('请输入模型显示名称')
|
||||
if (input.authMode !== undefined && !['api-key', 'none'].includes(input.authMode)) throw new Error('鉴权方式无效')
|
||||
const suppliedKey = typeof input.apiKey === 'string' ? input.apiKey.replace(/\s/gu, '') : ''
|
||||
if (suppliedKey && !/^[\x21-\x7e]+$/.test(suppliedKey)) throw new Error('API Key 只能包含 ASCII 可打印字符')
|
||||
const authMode = input.authMode ?? 'api-key'
|
||||
const key = authMode === 'none' || input.clearApiKey ? '' : suppliedKey || (existing ? readKey(existing).key : '')
|
||||
if (authMode !== 'none' && !key && (testing || !input.clearApiKey)) throw new Error('请输入 API Key')
|
||||
const next: StoredModel = { id, name, providerId, model, baseUrl, authMode }
|
||||
for (const field of ['supportsToolCall', 'supportsImages', 'supportsReasoning', 'onlyReasoning', 'canDisableThinking', 'useCustomProtocol'] as const) {
|
||||
if (typeof input[field] === 'boolean') next[field] = input[field]
|
||||
}
|
||||
if (next.supportsReasoning) {
|
||||
if (typeof input.defaultEffort === 'string') next.defaultEffort = input.defaultEffort
|
||||
if (Array.isArray(input.supportedEfforts) && input.supportedEfforts.every(value => typeof value === 'string')) next.supportedEfforts = [...new Set(input.supportedEfforts)]
|
||||
} else {
|
||||
delete next.onlyReasoning
|
||||
delete next.canDisableThinking
|
||||
}
|
||||
for (const field of ['maxInputTokens', 'maxOutputTokens'] as const) {
|
||||
const value = input[field]
|
||||
if (value !== undefined) {
|
||||
if (!Number.isFinite(value)) throw new Error('Token 数量必须是有效数字')
|
||||
next[field] = Math.trunc(value)
|
||||
}
|
||||
}
|
||||
for (const field of ['temperature', 'maxToolRounds'] as const) {
|
||||
const value = input[field] ?? existing?.[field]
|
||||
if (value !== undefined) {
|
||||
if (!Number.isFinite(value)) throw new Error('模型参数必须是有效数字')
|
||||
next[field] = field === 'temperature' ? Math.max(0, Math.min(2, value)) : Math.max(1, Math.min(24, Math.round(value)))
|
||||
}
|
||||
}
|
||||
// Preserve ciphertext when editing without a new key (including on platforms temporarily unable to encrypt).
|
||||
if (existing && authMode !== 'none' && !input.clearApiKey && !suppliedKey) {
|
||||
next.apiKeyEnc = existing.apiKeyEnc
|
||||
next.apiKeyPlain = existing.apiKeyPlain
|
||||
} else writeKey(next, key)
|
||||
return next
|
||||
}
|
||||
|
||||
export async function listModels(): Promise<ModelRegistryView> {
|
||||
await writes
|
||||
return registryView(await load())
|
||||
}
|
||||
|
||||
export function saveModel(input: CustomModelInput, overwrite = false): Promise<ModelRegistryView> {
|
||||
return mutate(async registry => {
|
||||
const previousId = input?.previousId || input?.id
|
||||
const existing = registry.models.find(model => model.id === previousId)
|
||||
const target = registry.models.find(model => model.id === input?.id?.trim())
|
||||
if (target && target.id !== input.previousId && !overwrite) throw new Error('同名模型已存在,请确认是否覆盖')
|
||||
const next = normalize(input, existing ?? target)
|
||||
const remaining = registry.models.filter(model => model.id !== previousId && model.id !== next.id)
|
||||
const oldIndex = registry.models.findIndex(model => model.id === previousId || model.id === next.id)
|
||||
remaining.splice(oldIndex < 0 ? remaining.length : Math.min(oldIndex, remaining.length), 0, next)
|
||||
const selectedModelId = !registry.selectedModelId || registry.selectedModelId === previousId ? next.id : registry.selectedModelId
|
||||
const updated: StoredRegistry = { version: 1, models: remaining, selectedModelId }
|
||||
await persist(updated)
|
||||
return registryView(updated)
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteModel(id: string): Promise<ModelRegistryView> {
|
||||
return mutate(async registry => {
|
||||
const models = registry.models.filter(model => model.id !== id)
|
||||
const selectedModelId = registry.selectedModelId === id ? models[0]?.id ?? null : registry.selectedModelId
|
||||
const updated: StoredRegistry = { version: 1, models, selectedModelId }
|
||||
await persist(updated)
|
||||
return registryView(updated)
|
||||
})
|
||||
}
|
||||
|
||||
export function setSelectedModel(id: string | null): Promise<ModelRegistryView> {
|
||||
return mutate(async registry => {
|
||||
if (id !== null && !registry.models.some(model => model.id === id)) throw new Error('所选模型不存在')
|
||||
const updated: StoredRegistry = { ...registry, selectedModelId: id }
|
||||
await persist(updated)
|
||||
return registryView(updated)
|
||||
})
|
||||
}
|
||||
|
||||
export async function resolveModel(id?: string | null): Promise<ResolvedModel | null> {
|
||||
await writes
|
||||
const registry = await load()
|
||||
const targetId = id === undefined ? registry.selectedModelId : id
|
||||
if (!targetId) return null
|
||||
const model = registry.models.find(model => model.id === targetId)
|
||||
if (!model) throw new Error('所选模型不存在,请重新选择模型')
|
||||
const legacy = await getGatewayConfig()
|
||||
return {
|
||||
profile: viewOf(model),
|
||||
config: { ...legacy, baseUrl: model.baseUrl, model: model.model, apiKey: readKey(model).key,
|
||||
authMode: model.authMode, maxOutputTokens: model.maxOutputTokens,
|
||||
temperature: model.temperature ?? legacy.temperature, maxToolRounds: model.maxToolRounds ?? legacy.maxToolRounds }
|
||||
}
|
||||
}
|
||||
|
||||
/** Test unsaved settings using only the specified user's OpenAI-compatible endpoint. */
|
||||
export async function testModel(input: CustomModelInput): Promise<ModelTestResult> {
|
||||
const startedAt = Date.now()
|
||||
const result = (success: boolean, message: string): ModelTestResult => ({ success, message, durationMs: Date.now() - startedAt })
|
||||
let model: StoredModel
|
||||
try {
|
||||
await writes
|
||||
const registry = cache ?? await readRegistry(false)
|
||||
model = normalize(input, registry.models.find(item => item.id === (input.previousId || input.id)), true)
|
||||
} catch (error) { return result(false, (error as Error).message) }
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(), 15_000)
|
||||
try {
|
||||
const key = readKey(model).key
|
||||
const { api, endpoint } = resolveOpenAIEndpoint(model.baseUrl)
|
||||
const payload = api === 'openai-responses'
|
||||
? { model: model.model, input: 'Reply with OK.', stream: false, max_output_tokens: 16 }
|
||||
: { model: model.model, messages: [{ role: 'user', content: 'Reply with OK.' }], stream: false, max_tokens: 16 }
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST', signal: controller.signal,
|
||||
headers: { 'content-type': 'application/json', ...(key ? { authorization: `Bearer ${key}` } : {}) },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.body?.cancel()
|
||||
const messages: Record<number, string> = { 401: '鉴权失败,请检查 API Key', 403: '上游拒绝访问,请检查 API Key 和权限',
|
||||
404: '未找到模型或接口,请检查模型名称和 API 地址', 429: '上游请求频率或额度受限,请稍后重试' }
|
||||
return result(false, messages[response.status] ?? `上游返回 HTTP ${response.status}`)
|
||||
}
|
||||
// Limit response consumption; never return upstream text that could echo credentials.
|
||||
const reader = response.body?.getReader()
|
||||
if (!reader) return result(false, '上游返回为空')
|
||||
const chunks: Uint8Array[] = []
|
||||
let total = 0
|
||||
while (true) {
|
||||
const { value, done } = await reader.read()
|
||||
if (done) break
|
||||
total += value.length
|
||||
if (total > 1024 * 1024) { await reader.cancel(); return result(false, '上游响应过大') }
|
||||
chunks.push(value)
|
||||
}
|
||||
const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown
|
||||
if (api === 'openai-responses') {
|
||||
if (!isResponsesTestResult(parsed)) return result(false, '上游未返回兼容的 Responses 结果')
|
||||
} else if (!parsed || typeof parsed !== 'object' || !('choices' in parsed)
|
||||
|| !Array.isArray(parsed.choices) || !parsed.choices.length
|
||||
|| !parsed.choices.some(choice => choice && (choice.message || typeof choice.text === 'string'))) {
|
||||
return result(false, '上游未返回兼容的对话结果')
|
||||
}
|
||||
return result(true, '连接测试成功')
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) return result(false, '连接超时,请检查 API 地址或稍后重试')
|
||||
return result(false, error instanceof SyntaxError ? '上游未返回有效的 JSON 对话结果' : '无法连接上游,请检查 API 地址和网络')
|
||||
} finally { clearTimeout(timer) }
|
||||
}
|
||||
|
||||
/** A short probe may spend its whole output budget on reasoning without producing visible text. */
|
||||
function isResponsesTestResult(value: unknown): boolean {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return false
|
||||
const response = value as Record<string, unknown>
|
||||
if (response.error != null || (response.object !== undefined && response.object !== 'response')
|
||||
|| (response.status !== undefined && response.status !== 'completed' && response.status !== 'incomplete')
|
||||
|| !Array.isArray(response.output)) return false
|
||||
const validOutput = response.output.every(item => {
|
||||
if (!item || typeof item !== 'object' || Array.isArray(item)) return false
|
||||
const output = item as Record<string, unknown>
|
||||
if (output.type === 'reasoning') {
|
||||
return Array.isArray(output.summary) && output.summary.every(part => part && typeof part === 'object'
|
||||
&& part.type === 'summary_text' && typeof part.text === 'string')
|
||||
}
|
||||
return output.type === 'message' && output.role === 'assistant' && Array.isArray(output.content)
|
||||
&& output.content.length > 0 && output.content.every(part => part && typeof part === 'object'
|
||||
&& ((part.type === 'output_text' && typeof part.text === 'string')
|
||||
|| (part.type === 'refusal' && typeof part.refusal === 'string')))
|
||||
})
|
||||
if (!validOutput) return false
|
||||
if (response.status === 'incomplete') {
|
||||
const details = response.incomplete_details
|
||||
return !!details && typeof details === 'object' && 'reason' in details
|
||||
&& (details.reason === 'max_output_tokens' || details.reason === 'content_filter')
|
||||
}
|
||||
return response.output.length > 0
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import type { ResolvedModel } from '@shared/models'
|
||||
import { resolveOpenAIEndpoint } from '@shared/openai-endpoint'
|
||||
|
||||
/** Dedicated immutable profile per effective configuration; active turns keep their own snapshot. */
|
||||
export async function prepareOmpModel(model: ResolvedModel): Promise<{ profile: string; model: string; fingerprint: string; env: NodeJS.ProcessEnv }> {
|
||||
const endpoint = resolveOpenAIEndpoint(model.config.baseUrl)
|
||||
const fingerprint = createHash('sha256').update(JSON.stringify({ model, api: endpoint.api, baseUrl: endpoint.baseUrl })).digest('hex')
|
||||
const profile = `ycode-managed-${fingerprint.slice(0, 20)}`
|
||||
const ompHome = process.env.YCODE_OMP_HOME || homedir()
|
||||
const directory = join(ompHome, '.omp', 'profiles', profile, 'agent')
|
||||
const p = model.profile
|
||||
const yaml = [
|
||||
'providers:', ' ycode:',
|
||||
` baseUrl: ${JSON.stringify(endpoint.baseUrl)}`,
|
||||
` api: ${endpoint.api}`,
|
||||
...(p.authMode === 'none' ? [' auth: none'] : [' apiKey: YCODE_MODEL_API_KEY']),
|
||||
' models:', ` - id: ${JSON.stringify(model.config.model)}`,
|
||||
` name: ${JSON.stringify(p.name || p.model)}`,
|
||||
` reasoning: ${!!p.supportsReasoning}`,
|
||||
` input: ${p.supportsImages ? '[text, image]' : '[text]'}`,
|
||||
` contextWindow: ${p.maxInputTokens || 128000}`,
|
||||
` maxTokens: ${p.maxOutputTokens || 8192}`, ''
|
||||
].join('\n')
|
||||
await mkdir(directory, { recursive: true })
|
||||
await writeFile(join(directory, 'models.yml'), yaml, 'utf8')
|
||||
// The key is passed only in the child environment, never written into the YAML or process arguments.
|
||||
return { profile, model: `ycode/${model.config.model}`, fingerprint, env: { YCODE_MODEL_API_KEY: model.config.apiKey,
|
||||
...(process.env.YCODE_OMP_HOME ? { USERPROFILE: ompHome, HOME: ompHome } : {}) } }
|
||||
}
|
||||
+447
@@ -0,0 +1,447 @@
|
||||
/**
|
||||
* OMP 内核 sidecar(`omp --mode rpc`)。
|
||||
*
|
||||
* OMP RPC uses newline-delimited JSON frames over stdio:
|
||||
* - stdio 上的 **newline-delimited JSONL**(不是 JSON-RPC 2.0)
|
||||
* - 启动后先写一个 `ready` 帧
|
||||
* - stdin:命令(可带 `id` 用于关联);stdout:响应 + 会话事件
|
||||
* - 单帧上限 1 MiB
|
||||
*
|
||||
* 持久会话使用 OMP 自己的 JSONL transcript;未提供 sessionPath 时保持临时模式。
|
||||
*/
|
||||
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
||||
import { createInterface } from 'node:readline'
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { mkdir } from 'node:fs/promises'
|
||||
import { homedir } from 'node:os'
|
||||
import { dirname, isAbsolute, join } from 'node:path'
|
||||
|
||||
/** OMP 单帧上限(含换行) */
|
||||
const MAX_FRAME_BYTES = 1024 * 1024
|
||||
/** 命令默认超时 */
|
||||
const COMMAND_TIMEOUT_MS = 60_000
|
||||
|
||||
export interface OmpFrame<TData = unknown> {
|
||||
type: string
|
||||
id?: string
|
||||
command?: string
|
||||
success?: boolean
|
||||
data?: TData
|
||||
error?: string | { message?: string }
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface OmpAgentMessage {
|
||||
role: string
|
||||
content?: unknown[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface OmpSessionTreeEntry {
|
||||
id: string
|
||||
parentId: string | null
|
||||
type: string
|
||||
message?: OmpAgentMessage
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface OmpSessionTreeNode {
|
||||
entry: OmpSessionTreeEntry
|
||||
children: OmpSessionTreeNode[]
|
||||
label?: string
|
||||
}
|
||||
|
||||
export interface OmpBranchMessage {
|
||||
entryId: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface OmpHostToolDefinition {
|
||||
name: string
|
||||
label?: string
|
||||
description: string
|
||||
parameters: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface OmpToolInfo {
|
||||
id?: string
|
||||
name?: string
|
||||
/** Optional human-readable action text emitted alongside the native tool call. */
|
||||
intent?: string
|
||||
argsRaw?: string
|
||||
/** 部分输出 / 最终输出(尽力解析) */
|
||||
output?: string
|
||||
ok?: boolean
|
||||
isError?: boolean
|
||||
}
|
||||
|
||||
export interface OmpPromptImage {
|
||||
type: 'image'
|
||||
data: string
|
||||
mimeType: string
|
||||
}
|
||||
|
||||
interface Pending {
|
||||
resolve: (value: OmpFrame) => void
|
||||
reject: (err: Error) => void
|
||||
timer: NodeJS.Timeout
|
||||
}
|
||||
|
||||
/** 在常见位置探测 OMP 可执行文件 */
|
||||
export function detectOmpPath(): string | null {
|
||||
const candidates = [
|
||||
process.env['YCODE_OMP_PATH'],
|
||||
process.env['LOCALAPPDATA'] ? `${process.env['LOCALAPPDATA']}\\Ygent\\omp.exe` : null,
|
||||
'C:\\Program Files\\Ygent\\omp.exe'
|
||||
].filter((p): p is string => !!p)
|
||||
for (const p of candidates) {
|
||||
if (existsSync(p)) return p
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** OMP 内核的对外信息(设置页 / 输入框展示用) */
|
||||
export interface OmpInfo {
|
||||
available: boolean
|
||||
exePath: string | null
|
||||
profile: string
|
||||
/** 从 profile 的 models.yml 读出的首个自定义模型(`<provider>/<id>` 形式),读不到为 null */
|
||||
model: string | null
|
||||
}
|
||||
|
||||
/** Returns the first custom model with its OMP provider namespace. */
|
||||
export function parseFirstProfileModel(modelsYaml: string): string | null {
|
||||
let inProviders = false
|
||||
let providerId: string | null = null
|
||||
let inModels = false
|
||||
|
||||
for (const line of modelsYaml.split(/\r?\n/)) {
|
||||
if (!line.trim() || line.trimStart().startsWith('#')) continue
|
||||
const indent = line.length - line.trimStart().length
|
||||
const content = line.trimStart()
|
||||
|
||||
if (indent === 0) {
|
||||
inProviders = /^providers:\s*(?:#.*)?$/.test(content)
|
||||
providerId = null
|
||||
inModels = false
|
||||
continue
|
||||
}
|
||||
if (!inProviders) continue
|
||||
|
||||
if (indent === 2) {
|
||||
const provider = content.match(/^(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9._-]+)):\s*(?:#.*)?$/)
|
||||
providerId = provider?.[1] ?? provider?.[2] ?? provider?.[3] ?? null
|
||||
inModels = false
|
||||
continue
|
||||
}
|
||||
|
||||
if (!providerId) continue
|
||||
if (indent === 4) {
|
||||
inModels = /^models:\s*(?:#.*)?$/.test(content)
|
||||
continue
|
||||
}
|
||||
if (!inModels || indent < 6) continue
|
||||
|
||||
const model = content.match(/^[-]\s*id:\s*(?:"([^"]+)"|'([^']+)'|([^\s#]+))\s*(?:#.*)?$/)
|
||||
const modelId = model?.[1] ?? model?.[2] ?? model?.[3]
|
||||
if (modelId) return `${providerId}/${modelId}`
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/** Reads the executable and first provider/model pair declared in the selected profile. */
|
||||
export function getOmpInfo(profile = 'ycode'): OmpInfo {
|
||||
const exePath = detectOmpPath()
|
||||
if (!exePath) {
|
||||
return { available: false, exePath: null, profile, model: null }
|
||||
}
|
||||
const modelsPath = join(homedir(), '.omp', 'profiles', profile, 'agent', 'models.yml')
|
||||
let model: string | null = null
|
||||
try {
|
||||
const text = readFileSync(modelsPath, 'utf8')
|
||||
model = parseFirstProfileModel(text)
|
||||
} catch {
|
||||
// profile 未配置时只暴露可用性
|
||||
}
|
||||
return { available: true, exePath, profile, model }
|
||||
}
|
||||
|
||||
export class OmpKernel extends EventEmitter {
|
||||
private child: ChildProcessWithoutNullStreams | null = null
|
||||
private pending = new Map<string, Pending>()
|
||||
private seq = 0
|
||||
private starting: Promise<void> | null = null
|
||||
private readyDone = false
|
||||
private disposed = false
|
||||
private disposal: Promise<void> | null = null
|
||||
private startupErrorReported = false
|
||||
|
||||
constructor(
|
||||
private readonly exePath: string,
|
||||
private readonly cwd: string,
|
||||
private readonly profile = 'ycode',
|
||||
private readonly sessionPath?: string,
|
||||
private readonly modelOverride?: string | null,
|
||||
private readonly environment?: NodeJS.ProcessEnv
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
/** 启动进程并等待 ready 帧;重复调用只会启动一次 */
|
||||
async start(): Promise<void> {
|
||||
if (this.disposed) throw new Error('OMP 内核已释放')
|
||||
if (this.readyDone) return
|
||||
if (this.starting) return this.starting
|
||||
|
||||
this.startupErrorReported = false
|
||||
const starting = this.startProcess().catch((err: Error) => {
|
||||
if (this.starting === starting) this.starting = null
|
||||
if (!this.startupErrorReported && this.listenerCount('error') > 0) {
|
||||
this.startupErrorReported = true
|
||||
this.emit('error', err)
|
||||
}
|
||||
throw err
|
||||
})
|
||||
this.starting = starting
|
||||
return starting
|
||||
}
|
||||
|
||||
private async startProcess(): Promise<void> {
|
||||
if (!existsSync(this.exePath)) {
|
||||
throw new Error(`OMP 可执行文件不存在:${this.exePath}`)
|
||||
}
|
||||
if (this.sessionPath) {
|
||||
if (!isAbsolute(this.sessionPath)) throw new Error('OMP 会话路径必须是绝对路径')
|
||||
await mkdir(dirname(this.sessionPath), { recursive: true })
|
||||
}
|
||||
if (this.disposed) throw new Error('OMP 内核已释放')
|
||||
|
||||
// spawn 的 cwd 必须存在,否则报 ENOENT(与可执行文件无关)
|
||||
const cwd = existsSync(this.cwd) ? this.cwd : homedir()
|
||||
const model = this.modelOverride === undefined ? getOmpInfo(this.profile).model : this.modelOverride
|
||||
if (!model || model.indexOf('/') <= 0 || model.endsWith('/')) {
|
||||
throw new Error(`OMP profile "${this.profile}" 缺少有效的 provider/model;请在 agent/models.yml 中配置模型`)
|
||||
}
|
||||
|
||||
const args = ['--mode', 'rpc', '--profile', this.profile, `--model=${model}`, '--cwd', cwd, '--no-tools']
|
||||
if (this.sessionPath) {
|
||||
args.push(`--session-dir=${dirname(this.sessionPath)}`, `--resume=${this.sessionPath}`)
|
||||
} else {
|
||||
args.push('--no-session')
|
||||
}
|
||||
|
||||
const child = spawn(this.exePath, args, { cwd, windowsHide: true, env: { ...process.env, ...this.environment } })
|
||||
this.child = child
|
||||
const rl = createInterface({ input: child.stdout })
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let settled = false
|
||||
let startupTimer: NodeJS.Timeout | undefined
|
||||
const settle = (err?: Error): void => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
if (startupTimer) clearTimeout(startupTimer)
|
||||
this.off('frame', onReady)
|
||||
if (err) reject(err)
|
||||
else resolve()
|
||||
}
|
||||
|
||||
const onReady = (frame: OmpFrame): void => {
|
||||
if (frame.type !== 'ready') return
|
||||
this.readyDone = true
|
||||
settle()
|
||||
}
|
||||
|
||||
child.on('error', (err) => {
|
||||
this.startupErrorReported = true
|
||||
if (this.listenerCount('error') > 0) this.emit('error', err)
|
||||
settle(err)
|
||||
})
|
||||
child.on('exit', (code) => {
|
||||
const wasReady = this.readyDone
|
||||
this.emit('exit', code)
|
||||
rl.close()
|
||||
for (const [id, pending] of this.pending) {
|
||||
clearTimeout(pending.timer)
|
||||
pending.reject(new Error(`OMP 进程已退出(code=${code}),命令 ${id} 未完成`))
|
||||
}
|
||||
this.pending.clear()
|
||||
if (this.child === child) this.child = null
|
||||
this.readyDone = false
|
||||
this.starting = null
|
||||
if (!wasReady) settle(new Error(`OMP 进程在 ready 前退出(code=${code})`))
|
||||
})
|
||||
child.stderr.on('data', (chunk: Buffer) => {
|
||||
this.emit('stderr', chunk.toString())
|
||||
})
|
||||
|
||||
rl.on('line', (line) => this.handleLine(line))
|
||||
this.on('frame', onReady)
|
||||
startupTimer = setTimeout(() => settle(new Error('OMP 启动超时(未收到 ready 帧)')), 30_000)
|
||||
startupTimer.unref()
|
||||
})
|
||||
}
|
||||
|
||||
private handleLine(line: string): void {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed) return
|
||||
if (trimmed.length > MAX_FRAME_BYTES) {
|
||||
this.emit('error', new Error(`OMP 帧超过上限(${trimmed.length} 字节)`))
|
||||
return
|
||||
}
|
||||
let frame: OmpFrame
|
||||
try {
|
||||
frame = JSON.parse(trimmed) as OmpFrame
|
||||
} catch {
|
||||
this.emit('error', new Error(`OMP 输出无法解析:${trimmed.slice(0, 200)}`))
|
||||
return
|
||||
}
|
||||
|
||||
// 带响应 id 的帧 → 关联到挂起命令
|
||||
if (frame.id && this.pending.has(frame.id)) {
|
||||
const p = this.pending.get(frame.id)
|
||||
if (p) {
|
||||
this.pending.delete(frame.id)
|
||||
clearTimeout(p.timer)
|
||||
p.resolve(frame)
|
||||
}
|
||||
// 响应帧同时也广播(prompt 的 ack 可能带 agentInvoked 信息)
|
||||
this.emit('frame', frame)
|
||||
return
|
||||
}
|
||||
|
||||
this.emit('frame', frame)
|
||||
this.emit('event', frame)
|
||||
}
|
||||
|
||||
/** 发送一条命令并等待其响应 */
|
||||
async send(command: Record<string, unknown>, timeoutMs = COMMAND_TIMEOUT_MS): Promise<OmpFrame> {
|
||||
await this.start()
|
||||
const id = `req_${++this.seq}`
|
||||
const payload = JSON.stringify({ id, ...command })
|
||||
|
||||
return new Promise<OmpFrame>((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
this.pending.delete(id)
|
||||
reject(new Error(`OMP 命令超时:${String(command.type)}`))
|
||||
}, timeoutMs)
|
||||
timer.unref?.()
|
||||
|
||||
this.pending.set(id, { resolve, reject, timer })
|
||||
this.child?.stdin.write(payload + '\n')
|
||||
})
|
||||
}
|
||||
|
||||
/** 发起一次对话回合;事件经 `event` 回调流出 */
|
||||
async prompt(message: string, images?: OmpPromptImage[]): Promise<OmpFrame> {
|
||||
const command: Record<string, unknown> = { type: 'prompt', message }
|
||||
if (images?.length) command.images = images
|
||||
return this.send(command, COMMAND_TIMEOUT_MS * 10)
|
||||
}
|
||||
|
||||
async abort(): Promise<OmpFrame> {
|
||||
return this.send({ type: 'abort' }, 15_000)
|
||||
}
|
||||
|
||||
async getMessages(): Promise<OmpFrame<{ messages: OmpAgentMessage[] }>> {
|
||||
return this.send({ type: 'get_messages' }) as Promise<OmpFrame<{ messages: OmpAgentMessage[] }>>
|
||||
}
|
||||
|
||||
async getTree(): Promise<OmpFrame<{ tree: OmpSessionTreeNode[]; leafId: string | null }>> {
|
||||
return this.send({ type: 'get_tree' }) as Promise<OmpFrame<{ tree: OmpSessionTreeNode[]; leafId: string | null }>>
|
||||
}
|
||||
|
||||
async getBranchMessages(): Promise<OmpFrame<{ messages: OmpBranchMessage[] }>> {
|
||||
return this.send({ type: 'get_branch_messages' }) as Promise<OmpFrame<{ messages: OmpBranchMessage[] }>>
|
||||
}
|
||||
|
||||
async branch(entryId: string): Promise<OmpFrame<{ text?: string; cancelled?: boolean }>> {
|
||||
if (!entryId.trim()) throw new Error('OMP branch requires a native user message entryId')
|
||||
return this.send({ type: 'branch', entryId }) as Promise<OmpFrame<{ text?: string; cancelled?: boolean }>>
|
||||
}
|
||||
|
||||
async setHostTools(tools: OmpHostToolDefinition[]): Promise<OmpFrame<{ toolNames: string[] }>> {
|
||||
return this.send({ type: 'set_host_tools', tools }) as Promise<OmpFrame<{ toolNames: string[] }>>
|
||||
}
|
||||
|
||||
/** Levels come from `getAvailableThinkingLevels()`; `off` disables thinking for this session. */
|
||||
async setThinkingLevel(level: string): Promise<void> {
|
||||
await this.send({ type: 'set_thinking_level', level })
|
||||
}
|
||||
|
||||
async getAvailableThinkingLevels(): Promise<string[]> {
|
||||
const frame = await this.send({ type: 'get_available_thinking_levels' }) as OmpFrame<{ levels?: string[] }>
|
||||
return Array.isArray(frame.levels) ? frame.levels : []
|
||||
}
|
||||
|
||||
/** Resolve an OMP host_tool_call. The host request id, not toolCallId, correlates the reply. */
|
||||
async respondToHostTool(requestId: string, content: string, isError = false): Promise<void> {
|
||||
await this.start()
|
||||
if (!requestId.trim()) throw new Error('OMP host tool response requires a request id')
|
||||
const child = this.child
|
||||
if (!child) throw new Error('OMP sidecar is not running')
|
||||
const payload = {
|
||||
type: 'host_tool_result',
|
||||
id: requestId,
|
||||
result: { content: [{ type: 'text', text: content }] },
|
||||
...(isError ? { isError: true } : {})
|
||||
}
|
||||
child.stdin.write(`${JSON.stringify(payload)}\n`)
|
||||
}
|
||||
|
||||
dispose(): Promise<void> {
|
||||
if (this.disposal) return this.disposal
|
||||
this.disposed = true
|
||||
const child = this.child
|
||||
if (!child) {
|
||||
this.removeAllListeners()
|
||||
this.disposal = Promise.resolve()
|
||||
return this.disposal
|
||||
}
|
||||
|
||||
this.disposal = new Promise<void>((resolve) => {
|
||||
let completed = false
|
||||
let killTimer: NodeJS.Timeout | undefined
|
||||
const finish = (): void => {
|
||||
if (completed) return
|
||||
completed = true
|
||||
if (killTimer) clearTimeout(killTimer)
|
||||
child.off('exit', finish)
|
||||
child.off('close', finish)
|
||||
child.off('error', finishOnSpawnError)
|
||||
if (this.child === child) this.child = null
|
||||
this.removeAllListeners()
|
||||
resolve()
|
||||
}
|
||||
const finishOnSpawnError = (): void => {
|
||||
if (child.pid === undefined) finish()
|
||||
}
|
||||
|
||||
if (child.exitCode !== null || child.signalCode !== null) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
child.once('exit', finish)
|
||||
child.once('close', finish)
|
||||
child.on('error', finishOnSpawnError)
|
||||
try {
|
||||
child.stdin.end()
|
||||
} catch {
|
||||
// 进程可能已退出
|
||||
}
|
||||
killTimer = setTimeout(() => {
|
||||
if (child.exitCode !== null || child.signalCode !== null) return
|
||||
try {
|
||||
child.kill()
|
||||
} catch {
|
||||
// exit / close 事件仍负责完成释放
|
||||
}
|
||||
}, 1500)
|
||||
killTimer.unref()
|
||||
})
|
||||
return this.disposal
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { basename, extname, isAbsolute, relative, resolve, sep } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { DownloadPayload, ResourceRange, WorkspaceResource } from '@shared/resources'
|
||||
|
||||
// YCode host adapter for the restored renderer's path/image/download contracts.
|
||||
const MAX_TEXT_BYTES = 2 * 1024 * 1024
|
||||
const MAX_IMAGE_BYTES = 20 * 1024 * 1024
|
||||
const MAX_DOWNLOAD_BYTES = 32 * 1024 * 1024
|
||||
const IMAGE_MIME: Record<string, string> = {
|
||||
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
|
||||
'.webp': 'image/webp', '.svg': 'image/svg+xml', '.bmp': 'image/bmp', '.avif': 'image/avif', '.ico': 'image/x-icon'
|
||||
}
|
||||
|
||||
function nativePath(input: string): string {
|
||||
if (/^local-file:/i.test(input)) return fileURLToPath(`file:${input.slice('local-file:'.length)}`)
|
||||
if (/^file:/i.test(input)) return fileURLToPath(input)
|
||||
if (!isAbsolute(input) && /^[a-z][a-z0-9+.-]*:/i.test(input)) throw new Error('不支持的本地路径格式')
|
||||
return input
|
||||
}
|
||||
|
||||
/** Resolve both lexical and real paths so symlinks cannot cross the workspace boundary. */
|
||||
export async function resolveWorkspacePath(workspace: string, input: string): Promise<string> {
|
||||
if (typeof input !== 'string' || !input.trim() || input.includes('\0')) throw new Error('路径为空或无效')
|
||||
const root = await fs.realpath(workspace)
|
||||
const target = resolve(root, nativePath(input.trim()))
|
||||
const contains = (path: string): boolean => {
|
||||
const rel = relative(root, path)
|
||||
return rel === '' || (!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`))
|
||||
}
|
||||
if (!contains(target)) throw new Error('路径超出工作目录')
|
||||
const real = await fs.realpath(target)
|
||||
if (!contains(real)) throw new Error('路径超出工作目录')
|
||||
return real
|
||||
}
|
||||
|
||||
export async function classifyWorkspacePath(workspace: string, input: string): Promise<'file' | 'directory' | 'unknown'> {
|
||||
try {
|
||||
const path = await resolveWorkspacePath(workspace, input)
|
||||
const stat = await fs.stat(path)
|
||||
return stat.isDirectory() ? 'directory' : stat.isFile() ? 'file' : 'unknown'
|
||||
} catch { return 'unknown' }
|
||||
}
|
||||
|
||||
async function readBounded(path: string, limit: number): Promise<Buffer> {
|
||||
const file = await fs.open(path, 'r')
|
||||
try {
|
||||
const stat = await file.stat()
|
||||
if (!stat.isFile()) throw new Error('不是普通文件')
|
||||
if (stat.size > limit) throw new Error(`文件超过预览大小限制(${Math.round(limit / 1024 / 1024)} MB)`)
|
||||
const buffer = Buffer.alloc(Math.min(stat.size + 1, limit + 1))
|
||||
let count = 0
|
||||
while (count < buffer.length) {
|
||||
const { bytesRead } = await file.read(buffer, count, buffer.length - count, null)
|
||||
if (!bytesRead) break
|
||||
count += bytesRead
|
||||
}
|
||||
if (count > limit) throw new Error('文件超过预览大小限制')
|
||||
return buffer.subarray(0, count)
|
||||
} finally { await file.close() }
|
||||
}
|
||||
|
||||
export async function resolveWorkspaceImage(workspace: string, source: string): Promise<string | undefined> {
|
||||
try {
|
||||
if (/^data:image\//i.test(source) || /^https?:/i.test(source)) return source
|
||||
const path = await resolveWorkspacePath(workspace, source)
|
||||
const mime = IMAGE_MIME[extname(path).toLowerCase()]
|
||||
if (!mime) return undefined
|
||||
return `data:${mime};base64,${(await readBounded(path, MAX_IMAGE_BYTES)).toString('base64')}`
|
||||
} catch { return undefined }
|
||||
}
|
||||
|
||||
export async function readWorkspaceResource(workspace: string, input: string, range?: ResourceRange): Promise<WorkspaceResource> {
|
||||
const path = await resolveWorkspacePath(workspace, input)
|
||||
const stat = await fs.stat(path)
|
||||
const name = basename(path)
|
||||
if (stat.isDirectory()) {
|
||||
const all = await fs.readdir(path, { withFileTypes: true })
|
||||
const entries = all.filter((entry) => entry.isFile() || entry.isDirectory())
|
||||
.sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name))
|
||||
.slice(0, 500).map((entry) => ({ name: entry.name, path: resolve(path, entry.name), type: entry.isDirectory() ? 'directory' as const : 'file' as const }))
|
||||
return { kind: 'directory', path, name, entries, truncated: all.length > 500 }
|
||||
}
|
||||
const mime = IMAGE_MIME[extname(path).toLowerCase()]
|
||||
if (mime) {
|
||||
const buffer = await readBounded(path, MAX_IMAGE_BYTES)
|
||||
return { kind: 'image', path, name, url: `data:${mime};base64,${buffer.toString('base64')}` }
|
||||
}
|
||||
const buffer = await readBounded(path, MAX_TEXT_BYTES)
|
||||
if (buffer.includes(0)) throw new Error('该文件暂不支持文本预览')
|
||||
const lines = buffer.toString('utf8').split(/\r?\n/)
|
||||
const start = range && Number.isFinite(range.start) ? Math.min(lines.length, Math.max(1, Math.trunc(range.start))) : 1
|
||||
const end = range?.end && Number.isFinite(range.end) ? Math.max(start, Math.min(lines.length, Math.trunc(range.end))) : start
|
||||
const firstLine = Math.max(1, start - 20)
|
||||
const count = Math.min(2000, Math.max(400, end - firstLine + 21))
|
||||
return { kind: 'text', path, name, content: lines.slice(firstLine - 1, firstLine - 1 + count).join('\n'), firstLine,
|
||||
totalLines: lines.length, range: range ? { start, end } : undefined, truncated: firstLine > 1 || firstLine - 1 + count < lines.length }
|
||||
}
|
||||
|
||||
/** Artifact readers need the whole document; the line-window preview is unsuitable for diffs. */
|
||||
export async function readWorkspaceText(workspace: string, input: string): Promise<string> {
|
||||
const path = await resolveWorkspacePath(workspace, input)
|
||||
const buffer = await readBounded(path, MAX_TEXT_BYTES)
|
||||
if (buffer.includes(0)) throw new Error('该文件暂不支持文本预览')
|
||||
return buffer.toString('utf8')
|
||||
}
|
||||
|
||||
export function decodeDownload(payload: DownloadPayload): { filename: string; bytes: Buffer; mimeType: string } {
|
||||
if (!payload || typeof payload.filename !== 'string' || typeof payload.data !== 'string' || !['utf8', 'base64'].includes(payload.encoding)) throw new Error('下载内容无效')
|
||||
if (payload.data.length > MAX_DOWNLOAD_BYTES * 1.4) throw new Error('下载内容过大')
|
||||
const allowed = new Set([...Object.values(IMAGE_MIME), 'text/plain', 'text/markdown'])
|
||||
const mimeType = typeof payload.mimeType === 'string' ? payload.mimeType.split(';')[0].trim().toLowerCase() : ''
|
||||
if (!allowed.has(mimeType)) throw new Error('不支持的下载格式')
|
||||
const filename = basename(payload.filename.replace(/\\/g, '/')).replace(/[<>:"|?*\x00-\x1f]/g, '_')
|
||||
if (!filename || filename === '.' || filename === '..') throw new Error('文件名无效')
|
||||
const bytes = Buffer.from(payload.data, payload.encoding)
|
||||
if (bytes.length > MAX_DOWNLOAD_BYTES) throw new Error('下载内容过大')
|
||||
return { filename, bytes, mimeType }
|
||||
}
|
||||
|
||||
export async function imageDownloadPayload(workspace: string, source: string, filename?: string): Promise<DownloadPayload> {
|
||||
let mime: string
|
||||
let bytes: Buffer
|
||||
let suggestedName = 'image'
|
||||
if (/^data:image\//i.test(source)) {
|
||||
const match = /^data:(image\/[^;,]+)(;base64)?,([\s\S]*)$/i.exec(source)
|
||||
if (!match || source.length > MAX_IMAGE_BYTES * 1.4) throw new Error('图片内容无效或过大')
|
||||
mime = match[1].toLowerCase()
|
||||
bytes = match[2] ? Buffer.from(match[3], 'base64') : Buffer.from(decodeURIComponent(match[3]))
|
||||
} else if (/^https?:/i.test(source)) {
|
||||
const url = new URL(source)
|
||||
suggestedName = basename(url.pathname) || 'image'
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(15000) })
|
||||
if (!response.ok) throw new Error(`图片下载失败(${response.status})`)
|
||||
mime = response.headers.get('content-type')?.split(';')[0].toLowerCase() ?? ''
|
||||
if (!mime.startsWith('image/')) throw new Error('下载内容不是图片')
|
||||
const reader = response.body?.getReader()
|
||||
if (!reader) throw new Error('图片内容为空')
|
||||
const chunks: Uint8Array[] = []
|
||||
let size = 0
|
||||
try {
|
||||
while (true) {
|
||||
const { value, done } = await reader.read()
|
||||
if (done) break
|
||||
size += value.length
|
||||
if (size > MAX_IMAGE_BYTES) throw new Error('图片超过下载大小限制')
|
||||
chunks.push(value)
|
||||
}
|
||||
} finally { await reader.cancel().catch(() => undefined); reader.releaseLock() }
|
||||
bytes = Buffer.concat(chunks)
|
||||
} else {
|
||||
const path = await resolveWorkspacePath(workspace, source)
|
||||
mime = IMAGE_MIME[extname(path).toLowerCase()]
|
||||
if (!mime) throw new Error('不支持的图片格式')
|
||||
suggestedName = basename(path)
|
||||
bytes = await readBounded(path, MAX_IMAGE_BYTES)
|
||||
}
|
||||
if (bytes.length > MAX_IMAGE_BYTES) throw new Error('图片超过下载大小限制')
|
||||
const extension = Object.keys(IMAGE_MIME).find((key) => IMAGE_MIME[key] === mime) || '.png'
|
||||
const name = filename || (extname(suggestedName) ? suggestedName : `${suggestedName}${extension}`)
|
||||
return { filename: name, mimeType: mime, data: bytes.toString('base64'), encoding: 'base64' }
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* 会话与消息持久化。
|
||||
*
|
||||
* 存储:userData/sessions.json(单文件,MVP 规模足够)
|
||||
* 真实业务后续应换成 SQLite;此处刻意把接口收敛成 Store 形态,便于替换实现。
|
||||
*/
|
||||
|
||||
import { app } from 'electron'
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { ChatMessage, SessionMeta } from '@shared/types'
|
||||
import { applyAssistantEvent } from '@shared/message-parts'
|
||||
import type { SessionContext } from '@shared/workspaces'
|
||||
import { migrateImportedWorkBuddyMessages, type WorkBuddyImportProvenance } from '@shared/workbuddy-import'
|
||||
import { agentMode, agentModeContextPatch } from './agent/mode-policy'
|
||||
|
||||
interface SessionRecord extends SessionMeta {
|
||||
messages: ChatMessage[]
|
||||
pendingNativeBranchId?: string
|
||||
importProvenance?: WorkBuddyImportProvenance
|
||||
}
|
||||
|
||||
interface StoreShape {
|
||||
sessions: SessionRecord[]
|
||||
}
|
||||
|
||||
let cache: StoreShape | null = null
|
||||
let loadPromise: Promise<StoreShape> | null = null
|
||||
let persistQueue: Promise<void> = Promise.resolve()
|
||||
|
||||
function filePath(): string {
|
||||
return join(app.getPath('userData'), 'sessions.json')
|
||||
}
|
||||
|
||||
async function load(): Promise<StoreShape> {
|
||||
if (cache) return cache
|
||||
if (!loadPromise) {
|
||||
loadPromise = (async () => {
|
||||
try {
|
||||
const raw = await fs.readFile(filePath(), 'utf8')
|
||||
const parsed = JSON.parse(raw) as StoreShape
|
||||
cache = { sessions: Array.isArray(parsed.sessions) ? parsed.sessions : [] }
|
||||
let recovered = false
|
||||
for (const session of cache.sessions) {
|
||||
const migrated = migrateImportedWorkBuddyMessages(session.messages ?? [], session.importProvenance)
|
||||
if (migrated.changed) { session.messages = migrated.messages; recovered = true }
|
||||
session.messages = (session.messages ?? []).map((message) => {
|
||||
if (message.status !== 'streaming') return message
|
||||
recovered = true
|
||||
return { ...applyAssistantEvent(message, { runId: message.requestId ?? '', type: 'cancelled' }), interrupted: true }
|
||||
})
|
||||
}
|
||||
if (recovered) await persist()
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { cache = null; throw error }
|
||||
cache = { sessions: [] }
|
||||
}
|
||||
return cache as StoreShape
|
||||
})()
|
||||
}
|
||||
|
||||
const pending = loadPromise
|
||||
try {
|
||||
return await pending
|
||||
} finally {
|
||||
if (loadPromise === pending) loadPromise = null
|
||||
}
|
||||
}
|
||||
|
||||
async function persist(): Promise<void> {
|
||||
if (!cache) return
|
||||
const snapshot = JSON.stringify(cache, null, 2)
|
||||
const userData = app.getPath('userData')
|
||||
const file = filePath()
|
||||
const temporaryFile = join(userData, `.sessions-${randomUUID()}.tmp`)
|
||||
const write = persistQueue.then(async () => {
|
||||
await fs.mkdir(userData, { recursive: true })
|
||||
try {
|
||||
await fs.writeFile(temporaryFile, snapshot, 'utf8')
|
||||
await fs.rename(temporaryFile, file)
|
||||
} catch (error) {
|
||||
await fs.rm(temporaryFile, { force: true }).catch(() => undefined)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
// A failed write reaches its caller but does not reject subsequent writes.
|
||||
persistQueue = write.catch(() => undefined)
|
||||
await write
|
||||
}
|
||||
|
||||
export async function listSessions(): Promise<SessionMeta[]> {
|
||||
const store = await load()
|
||||
return [...store.sessions]
|
||||
.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
.map(({ id, title, createdAt, updatedAt, workspaceId, cwd, modelId, expertId, agentMode: mode, thinkingLevel: level }) => ({ id, title, createdAt, updatedAt, workspaceId, cwd, modelId, expertId, agentMode: agentMode(mode), thinkingLevel: level }))
|
||||
}
|
||||
|
||||
export async function createSession(title = '新对话', context: SessionContext = {}): Promise<SessionMeta> {
|
||||
context = agentModeContextPatch({}, context)
|
||||
const store = await load()
|
||||
const now = Date.now()
|
||||
const record: SessionRecord = { ...context, id: randomUUID(), title, createdAt: now, updatedAt: now, messages: [] }
|
||||
store.sessions.push(record)
|
||||
await persist()
|
||||
return { ...context, id: record.id, title: record.title, createdAt: now, updatedAt: now }
|
||||
}
|
||||
|
||||
export async function getSessionContext(id: string): Promise<SessionContext> {
|
||||
const session = (await load()).sessions.find((item) => item.id === id)
|
||||
if (!session) throw new Error('会话不存在')
|
||||
return { workspaceId: session.workspaceId, cwd: session.cwd, modelId: session.modelId, expertId: session.expertId, agentMode: agentMode(session.agentMode), thinkingLevel: session.thinkingLevel }
|
||||
}
|
||||
export async function updateSessionContext(id: string, patch: SessionContext): Promise<SessionMeta> {
|
||||
const session = (await load()).sessions.find((item) => item.id === id)
|
||||
if (!session) throw new Error('会话不存在')
|
||||
const previous = { ...session }
|
||||
Object.assign(session, agentModeContextPatch(session, patch))
|
||||
try { await persist() } catch (error) {
|
||||
for (const key of Object.keys(session)) if (!Object.hasOwn(previous, key)) delete (session as unknown as Record<string, unknown>)[key]
|
||||
Object.assign(session, previous); throw error
|
||||
}
|
||||
const { id: sessionId, title, createdAt, updatedAt, workspaceId, cwd, modelId, expertId, agentMode: mode, thinkingLevel: level } = session
|
||||
return { id: sessionId, title, createdAt, updatedAt, workspaceId, cwd, modelId, expertId, agentMode: agentMode(mode), thinkingLevel: level }
|
||||
}
|
||||
|
||||
export async function getMessages(sessionId: string): Promise<ChatMessage[]> {
|
||||
const store = await load()
|
||||
return store.sessions.find((s) => s.id === sessionId)?.messages ?? []
|
||||
}
|
||||
|
||||
export async function getPendingNativeBranch(sessionId: string): Promise<string | undefined> {
|
||||
return (await load()).sessions.find((session) => session.id === sessionId)?.pendingNativeBranchId
|
||||
}
|
||||
export async function setPendingNativeBranch(sessionId: string, entryId: string | undefined): Promise<void> {
|
||||
const session = (await load()).sessions.find((item) => item.id === sessionId)
|
||||
if (!session) throw new Error('会话不存在')
|
||||
const previous = session.pendingNativeBranchId
|
||||
session.pendingNativeBranchId = entryId
|
||||
try { await persist() } catch (error) { session.pendingNativeBranchId = previous; throw error }
|
||||
}
|
||||
|
||||
export async function appendMessage(sessionId: string, message: ChatMessage): Promise<void> {
|
||||
const store = await load()
|
||||
const session = store.sessions.find((s) => s.id === sessionId)
|
||||
if (!session) return
|
||||
session.messages.push(message)
|
||||
session.updatedAt = Date.now()
|
||||
await persist()
|
||||
}
|
||||
|
||||
export async function patchMessage(
|
||||
sessionId: string,
|
||||
messageId: string,
|
||||
patch: Partial<ChatMessage>
|
||||
): Promise<void> {
|
||||
const store = await load()
|
||||
const session = store.sessions.find((s) => s.id === sessionId)
|
||||
if (!session) return
|
||||
const message = session.messages.find((m) => m.id === messageId)
|
||||
if (!message) return
|
||||
Object.assign(message, patch)
|
||||
session.updatedAt = Date.now()
|
||||
await persist()
|
||||
}
|
||||
|
||||
export async function renameSession(sessionId: string, title: string): Promise<void> {
|
||||
const store = await load()
|
||||
const session = store.sessions.find((s) => s.id === sessionId)
|
||||
if (!session) return
|
||||
session.title = title.trim() || session.title
|
||||
session.updatedAt = Date.now()
|
||||
await persist()
|
||||
}
|
||||
|
||||
export async function deleteSession(sessionId: string): Promise<void> {
|
||||
const store = await load()
|
||||
store.sessions = store.sessions.filter((s) => s.id !== sessionId)
|
||||
await persist()
|
||||
}
|
||||
|
||||
/** Keep the abandoned branch on disk before replacing the active conversation. */
|
||||
export async function replaceMessages(sessionId: string, messages: ChatMessage[], reason: string, options?: { pendingNativeBranchId?: string }): Promise<void> {
|
||||
const store = await load()
|
||||
const session = store.sessions.find((item) => item.id === sessionId)
|
||||
if (!session) throw new Error('会话不存在')
|
||||
const directory = join(app.getPath('userData'), 'session-branches', encodeURIComponent(sessionId))
|
||||
await fs.mkdir(directory, { recursive: true })
|
||||
await fs.writeFile(join(directory, `${Date.now()}-${randomUUID()}.json`), JSON.stringify({ reason, session }, null, 2), 'utf8')
|
||||
const previous = session.messages
|
||||
const updatedAt = session.updatedAt
|
||||
const previousBranch = session.pendingNativeBranchId
|
||||
session.messages = messages
|
||||
if (options) session.pendingNativeBranchId = options.pendingNativeBranchId
|
||||
session.updatedAt = Date.now()
|
||||
try { await persist() } catch (error) { session.messages = previous; session.updatedAt = updatedAt; session.pendingNativeBranchId = previousBranch; throw error }
|
||||
}
|
||||
|
||||
/** A turn and its selected native branch are one atomic session-store mutation. */
|
||||
export async function startTurn(sessionId: string, user: ChatMessage, assistant: ChatMessage, prefix: ChatMessage[], reason?: string, nativeBranchId?: string): Promise<void> {
|
||||
const store = await load()
|
||||
const session = store.sessions.find((item) => item.id === sessionId)
|
||||
if (!session) throw new Error('会话不存在')
|
||||
if (reason) {
|
||||
const directory = join(app.getPath('userData'), 'session-branches', encodeURIComponent(sessionId))
|
||||
await fs.mkdir(directory, { recursive: true })
|
||||
await fs.writeFile(join(directory, `${Date.now()}-${randomUUID()}.json`), JSON.stringify({ reason, session }, null, 2), 'utf8')
|
||||
}
|
||||
const previous = { messages: session.messages, title: session.title, updatedAt: session.updatedAt, pendingNativeBranchId: session.pendingNativeBranchId }
|
||||
session.messages = [...prefix, user, assistant]
|
||||
if (!prefix.length) session.title = deriveTitle(user.content)
|
||||
if (nativeBranchId) session.pendingNativeBranchId = nativeBranchId
|
||||
session.updatedAt = Date.now()
|
||||
try { await persist() } catch (error) { Object.assign(session, previous); throw error }
|
||||
}
|
||||
|
||||
/** 用首条用户消息生成标题 */
|
||||
export function deriveTitle(text: string): string {
|
||||
const flat = text.replace(/\s+/g, ' ').trim()
|
||||
return flat.length > 24 ? `${flat.slice(0, 24)}…` : flat || '新对话'
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { GatewayConfig } from '@shared/types'
|
||||
|
||||
/** Native transcription adapter. A provider failure is returned to the actual voice UI. */
|
||||
export async function transcribeAudio(config: GatewayConfig, payload: { audioBase64: string; mimeType: string }): Promise<{ text: string }> {
|
||||
if (!config.apiKey) throw new Error('语音转写需要配置支持转写的模型服务')
|
||||
if (typeof payload?.audioBase64 !== 'string' || payload.audioBase64.length > 32 * 1024 * 1024) throw new Error('录音内容无效或过大')
|
||||
if (!['audio/wav', 'audio/x-wav', 'audio/webm', 'audio/ogg', 'audio/mp4'].includes(payload.mimeType)) throw new Error('录音格式不受支持')
|
||||
const bytes = Buffer.from(payload.audioBase64, 'base64')
|
||||
const extension = payload.mimeType.includes('wav') ? 'wav' : payload.mimeType.split('/')[1]
|
||||
const form = new FormData()
|
||||
form.append('file', new Blob([bytes], { type: payload.mimeType }), `recording.${extension}`)
|
||||
form.append('model', config.speechModel || 'whisper-1')
|
||||
const base = (config.speechBaseUrl || config.baseUrl).replace(/\/+$/, '')
|
||||
const response = await fetch(`${base}/audio/transcriptions`, { method: 'POST', headers: { authorization: `Bearer ${config.apiKey}` }, body: form, signal: AbortSignal.timeout(90000) })
|
||||
if (!response.ok) throw new Error(`语音转写失败(${response.status});请检查模型服务是否支持语音转写`)
|
||||
const result = await response.json() as { text?: unknown }
|
||||
if (typeof result.text !== 'string') throw new Error('语音服务未返回有效文本')
|
||||
return { text: result.text }
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import type { BrowserWindow } from 'electron'
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { isAbsolute, join, resolve } from 'node:path'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { spawn, type IPty, type IDisposable } from 'node-pty'
|
||||
import type { TerminalHostEvent, TerminalHostView } from '@shared/terminal-host'
|
||||
|
||||
export const TERMINAL_HOST_LIMITS = Object.freeze({ perWindow: 8, perSession: 4, total: 32, retainedPerWindow: 32,
|
||||
bufferCharacters: 262_144, inputCharacters: 65_536, pendingCharacters: 262_144 })
|
||||
|
||||
interface TerminalEntry {
|
||||
owner: BrowserWindow
|
||||
view: TerminalHostView
|
||||
pty: IPty
|
||||
listeners: IDisposable[]
|
||||
pending: string
|
||||
timer?: ReturnType<typeof setTimeout>
|
||||
}
|
||||
const terminals = new Map<string, TerminalEntry>()
|
||||
const owners = new WeakSet<BrowserWindow>()
|
||||
|
||||
function validOwner(owner: BrowserWindow): void {
|
||||
if (!owner || owner.isDestroyed() || owner.webContents.isDestroyed()) throw new Error('终端所属窗口已关闭')
|
||||
}
|
||||
function session(value?: string): string | undefined {
|
||||
if (value === undefined) return undefined
|
||||
if (typeof value !== 'string' || !value.trim() || value.length > 128) throw new Error('终端会话标识无效')
|
||||
return value
|
||||
}
|
||||
function dimension(value: number | undefined, fallback: number): number {
|
||||
if (value === undefined) return fallback
|
||||
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 2 || value > 1000) throw new Error('终端尺寸无效')
|
||||
return value
|
||||
}
|
||||
function owned(owner: BrowserWindow, id: string, sessionId?: string): TerminalEntry {
|
||||
validOwner(owner)
|
||||
const entry = terminals.get(id)
|
||||
if (!entry || entry.owner !== owner || entry.view.sessionId !== session(sessionId)) throw new Error('终端不存在或不属于当前会话')
|
||||
return entry
|
||||
}
|
||||
|
||||
/** Clone the environment and remove credentials before creating any shell child process. */
|
||||
export function terminalEnvironment(environment: NodeJS.ProcessEnv = process.env): Record<string, string> {
|
||||
const clean: Record<string, string> = {}
|
||||
for (const [name, value] of Object.entries(environment)) {
|
||||
if (value === undefined || /^=/u.test(name)) continue
|
||||
if (/(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|CREDENTIAL|AUTH|COOKIE)/iu.test(name)
|
||||
|| /^(?:ELECTRON_RUN_AS_NODE|NODE_OPTIONS|NODE_PATH)$/iu.test(name)) continue
|
||||
clean[name] = value
|
||||
}
|
||||
clean.TERM = 'xterm-256color'
|
||||
clean.COLORTERM = 'truecolor'
|
||||
return clean
|
||||
}
|
||||
function send(entry: TerminalEntry, event: TerminalHostEvent): void {
|
||||
if (entry.owner.isDestroyed() || entry.owner.webContents.isDestroyed()) return
|
||||
try { entry.owner.webContents.send('terminal:event', event) } catch { /* A closing owner is cleaned by its closed listener. */ }
|
||||
}
|
||||
function flush(entry: TerminalEntry): void {
|
||||
if (entry.timer) clearTimeout(entry.timer)
|
||||
entry.timer = undefined
|
||||
if (!entry.pending) return
|
||||
const data = entry.pending
|
||||
entry.pending = ''
|
||||
send(entry, { id: entry.view.id, sessionId: entry.view.sessionId, sequence: ++entry.view.sequence, type: 'data', data })
|
||||
}
|
||||
function append(entry: TerminalEntry, data: string): void {
|
||||
entry.view.buffer = (entry.view.buffer + data).slice(-TERMINAL_HOST_LIMITS.bufferCharacters)
|
||||
const last = entry.view.replay.at(-1)
|
||||
if (last && last.cols === entry.view.cols && last.rows === entry.view.rows) last.data += data
|
||||
else entry.view.replay.push({ data, cols: entry.view.cols, rows: entry.view.rows })
|
||||
let excess = entry.view.replay.reduce((total, chunk) => total + chunk.data.length, 0) - TERMINAL_HOST_LIMITS.bufferCharacters
|
||||
while (excess > 0) {
|
||||
const first = entry.view.replay[0]
|
||||
if (first.data.length <= excess) { excess -= first.data.length; entry.view.replay.shift() }
|
||||
else { first.data = first.data.slice(excess); excess = 0 }
|
||||
}
|
||||
entry.pending = (entry.pending + data).slice(-TERMINAL_HOST_LIMITS.pendingCharacters)
|
||||
if (!entry.timer) entry.timer = setTimeout(() => flush(entry), 16)
|
||||
}
|
||||
function remove(entry: TerminalEntry): void {
|
||||
terminals.delete(entry.view.id)
|
||||
if (entry.timer) clearTimeout(entry.timer)
|
||||
entry.timer = undefined
|
||||
entry.pending = ''
|
||||
entry.listeners.forEach(listener => listener.dispose())
|
||||
if (!entry.view.exited) { try { entry.pty.kill() } catch { /* Already exited between the lifecycle checks. */ } }
|
||||
}
|
||||
|
||||
/** cwd must be resolved from the bound session/selected workspace by the IPC host. */
|
||||
export async function createTerminal(owner: BrowserWindow, cwd: string, sessionId?: string, cols = 80, rows = 24): Promise<TerminalHostView> {
|
||||
validOwner(owner)
|
||||
const ownerSession = session(sessionId), columns = dimension(cols, 80), lines = dimension(rows, 24)
|
||||
if (typeof cwd !== 'string' || !isAbsolute(cwd)) throw new Error('终端需要有效的绝对工作目录')
|
||||
const root = resolve(cwd)
|
||||
if (!(await fs.stat(root).catch(() => null))?.isDirectory()) throw new Error('工作空间文件夹不存在')
|
||||
validOwner(owner)
|
||||
const existing = [...terminals.values()]
|
||||
const active = existing.filter(entry => !entry.view.exited)
|
||||
if (active.length >= TERMINAL_HOST_LIMITS.total || active.filter(entry => entry.owner === owner).length >= TERMINAL_HOST_LIMITS.perWindow
|
||||
|| active.filter(entry => entry.owner === owner && entry.view.sessionId === ownerSession).length >= TERMINAL_HOST_LIMITS.perSession) {
|
||||
throw new Error('打开的终端数量已达上限,请先关闭其他终端')
|
||||
}
|
||||
const retained = existing.filter(entry => entry.owner === owner)
|
||||
if (retained.length >= TERMINAL_HOST_LIMITS.retainedPerWindow) {
|
||||
const retired = retained.find(entry => entry.view.exited)
|
||||
if (retired) remove(retired)
|
||||
else throw new Error('打开的终端数量已达上限,请先关闭其他终端')
|
||||
}
|
||||
// No profile scripts, command interpolation or gateway credentials enter this process.
|
||||
const shell = process.platform === 'win32'
|
||||
? join(process.env.SystemRoot || process.env.WINDIR || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe') : '/bin/bash'
|
||||
let pty: IPty
|
||||
try { pty = spawn(shell, process.platform === 'win32' ? ['-NoLogo', '-NoProfile'] : ['--noprofile', '--norc'], {
|
||||
name: 'xterm-256color', cols: columns, rows: lines, cwd: root, env: terminalEnvironment(),
|
||||
...(process.platform === 'win32' ? { useConpty: true, useConptyDll: true } : { encoding: 'utf8' })
|
||||
}) } catch { throw new Error('无法启动本地终端,请检查 PowerShell 和系统终端支持') }
|
||||
const view: TerminalHostView = { id: randomUUID(), sessionId: ownerSession, cwd: root, title: 'PowerShell', shell,
|
||||
cols: columns, rows: lines, buffer: '', replay: [], sequence: 0, exited: false }
|
||||
const entry: TerminalEntry = { owner, pty, view, listeners: [], pending: '' }
|
||||
terminals.set(view.id, entry)
|
||||
entry.listeners.push(pty.onData(data => { if (terminals.has(view.id)) append(entry, data) }), pty.onExit(({ exitCode }) => {
|
||||
if (!terminals.has(view.id)) return
|
||||
flush(entry)
|
||||
view.exited = true
|
||||
view.exitCode = exitCode
|
||||
send(entry, { id: view.id, sessionId: view.sessionId, sequence: ++view.sequence, type: 'exit', exitCode })
|
||||
}))
|
||||
if (!owners.has(owner)) { owners.add(owner); owner.once('closed', () => disposeTerminalHosts(owner)) }
|
||||
return snapshot(entry)
|
||||
}
|
||||
function snapshot(entry: TerminalEntry): TerminalHostView { return { ...entry.view, replay: entry.view.replay.map(chunk => ({ ...chunk })) } }
|
||||
export function getTerminal(owner: BrowserWindow, id: string, sessionId?: string): TerminalHostView {
|
||||
const entry = owned(owner, id, sessionId)
|
||||
flush(entry)
|
||||
return snapshot(entry)
|
||||
}
|
||||
export function writeTerminal(owner: BrowserWindow, id: string, data: string, sessionId?: string): void {
|
||||
const entry = owned(owner, id, sessionId)
|
||||
if (typeof data !== 'string' || data.length > TERMINAL_HOST_LIMITS.inputCharacters) throw new Error('终端输入过长或无效')
|
||||
if (entry.view.exited) throw new Error('终端进程已退出')
|
||||
try { entry.pty.write(data) } catch { throw new Error('终端输入失败,进程可能已退出') }
|
||||
}
|
||||
export function resizeTerminal(owner: BrowserWindow, id: string, cols: number, rows: number, sessionId?: string): void {
|
||||
const entry = owned(owner, id, sessionId), columns = dimension(cols, 80), lines = dimension(rows, 24)
|
||||
if (entry.view.exited) return
|
||||
if (columns === entry.view.cols && lines === entry.view.rows) return
|
||||
try { entry.pty.resize(columns, lines); entry.view.cols = columns; entry.view.rows = lines }
|
||||
catch { throw new Error('无法调整终端尺寸') }
|
||||
}
|
||||
export function clearTerminal(owner: BrowserWindow, id: string, sessionId?: string): void {
|
||||
const entry = owned(owner, id, sessionId)
|
||||
flush(entry)
|
||||
entry.view.buffer = '\x1b[2J\x1b[3J\x1b[H'
|
||||
entry.view.replay = [{ data: entry.view.buffer, cols: entry.view.cols, rows: entry.view.rows }]
|
||||
send(entry, { id, sessionId: entry.view.sessionId, sequence: ++entry.view.sequence, type: 'data', data: entry.view.buffer })
|
||||
}
|
||||
export function closeTerminal(owner: BrowserWindow, id: string, sessionId?: string): void {
|
||||
if (!terminals.has(id)) return
|
||||
remove(owned(owner, id, sessionId))
|
||||
}
|
||||
/** Bind a draft terminal to its first persisted conversation without changing its process or cwd. */
|
||||
export function rebindTerminal(owner: BrowserWindow, id: string, previousSessionId: string | undefined, nextSessionId: string, expectedCwd: string): TerminalHostView {
|
||||
const entry = owned(owner, id, previousSessionId)
|
||||
const next = session(nextSessionId)
|
||||
if (!next || typeof expectedCwd !== 'string' || !isAbsolute(expectedCwd)) throw new Error('终端目标会话或工作目录无效')
|
||||
const normalized = (value: string): string => process.platform === 'win32' ? resolve(value).toLowerCase() : resolve(value)
|
||||
if (normalized(expectedCwd) !== normalized(entry.view.cwd)) throw new Error('终端工作目录与目标会话不一致')
|
||||
if ([...terminals.values()].filter(item => item !== entry && item.owner === owner && item.view.sessionId === next && !item.view.exited).length >= TERMINAL_HOST_LIMITS.perSession) {
|
||||
throw new Error('目标会话的终端数量已达上限')
|
||||
}
|
||||
flush(entry)
|
||||
entry.view.sessionId = next
|
||||
return snapshot(entry)
|
||||
}
|
||||
export function disposeTerminalHosts(owner?: BrowserWindow): void {
|
||||
for (const entry of terminals.values()) if (!owner || entry.owner === owner) remove(entry)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { dirname, isAbsolute, relative, resolve, sep, join } from 'node:path'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { RevertChange } from '@shared/revert'
|
||||
|
||||
async function replaceFile(path: string, content: string | null): Promise<void> {
|
||||
if (content === null) { await fs.unlink(path); return }
|
||||
const temporary = join(dirname(path), `.ycode-restore-${randomUUID()}.tmp`)
|
||||
try { await fs.writeFile(temporary, content, 'utf8'); await fs.rename(temporary, path) }
|
||||
finally { await fs.unlink(temporary).catch(() => undefined) }
|
||||
}
|
||||
/** Validate every current file before applying any change; roll back if a write fails. */
|
||||
export async function restoreWorkspaceFiles(workspace: string, changes: RevertChange[]): Promise<() => Promise<void>> {
|
||||
const root = await fs.realpath(workspace)
|
||||
const validated: RevertChange[] = []
|
||||
for (const change of changes) {
|
||||
const path = await fs.realpath(resolve(change.path))
|
||||
const rel = relative(root, path)
|
||||
if (!rel || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error('恢复文件不在当前工作区内')
|
||||
if (await fs.readFile(path, 'utf8') !== change.after) throw new Error(`文件已被外部修改,未执行恢复:${rel}`)
|
||||
validated.push({ ...change, path })
|
||||
}
|
||||
const applied: RevertChange[] = []
|
||||
const undo = async (): Promise<void> => {
|
||||
for (const change of [...applied].reverse()) await replaceFile(change.path, change.after)
|
||||
}
|
||||
try {
|
||||
for (const change of validated) {
|
||||
if (await fs.readFile(change.path, 'utf8') !== change.after) throw new Error('文件在恢复前已变化')
|
||||
await replaceFile(change.path, change.before)
|
||||
applied.push(change)
|
||||
}
|
||||
} catch (error) { await undo(); throw error }
|
||||
return undo
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { app } from 'electron'
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { basename, join, resolve } from 'node:path'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { getWorkspaceRoot } from './config'
|
||||
import type { WorkspaceRecord, WorkspaceRegistryView } from '@shared/workspaces'
|
||||
|
||||
let state: WorkspaceRegistryView | null = null
|
||||
let loading: Promise<WorkspaceRegistryView> | null = null
|
||||
let queue: Promise<unknown> = Promise.resolve()
|
||||
const filename = (): string => join(app.getPath('userData'), 'workspaces.json')
|
||||
async function load(): Promise<WorkspaceRegistryView> {
|
||||
if (state) return state
|
||||
if (!loading) loading = (async () => {
|
||||
try {
|
||||
const parsed = JSON.parse(await fs.readFile(filename(), 'utf8')) as WorkspaceRegistryView
|
||||
if (!Array.isArray(parsed.workspaces)) throw new Error('空间配置损坏')
|
||||
state = parsed
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||
state = { workspaces: [], selectedWorkspaceId: null }
|
||||
// Preserve the previous global directory as an existing workspace.
|
||||
const root = await getWorkspaceRoot()
|
||||
if (await fs.stat(root).then((s) => s.isDirectory()).catch(() => false)) {
|
||||
const workspace = { id: randomUUID(), name: basename(root), root, createdAt: Date.now() }
|
||||
state.workspaces.push(workspace)
|
||||
state.selectedWorkspaceId = workspace.id
|
||||
}
|
||||
await persist(state)
|
||||
}
|
||||
return state!
|
||||
})().finally(() => { loading = null })
|
||||
return loading
|
||||
}
|
||||
async function persist(next: WorkspaceRegistryView): Promise<void> {
|
||||
const path = filename()
|
||||
await fs.mkdir(app.getPath('userData'), { recursive: true })
|
||||
const temporary = `${path}.${randomUUID()}.tmp`
|
||||
try { await fs.writeFile(temporary, JSON.stringify(next, null, 2), 'utf8'); await fs.rename(temporary, path) }
|
||||
finally { await fs.rm(temporary, { force: true }).catch(() => undefined) }
|
||||
state = next
|
||||
}
|
||||
function mutate<T>(operation: () => Promise<T>): Promise<T> {
|
||||
const result = queue.then(operation)
|
||||
queue = result.catch(() => undefined)
|
||||
return result
|
||||
}
|
||||
export async function listWorkspaces(): Promise<WorkspaceRegistryView> { return structuredClone(await load()) }
|
||||
export function validateWorkspaceName(value: string): string {
|
||||
const name = value.trim()
|
||||
if (!name) throw new Error('请输入空间名称')
|
||||
if (name.length > 100 || /[<>:"/\\|?*\x00-\x1f]/.test(name) || /[. ]$/.test(name) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(name) || name === '.' || name === '..') throw new Error('空间名称包含不可用字符')
|
||||
return name
|
||||
}
|
||||
export async function generateWorkspace(nameInput: string): Promise<WorkspaceRegistryView> {
|
||||
return mutate(async () => {
|
||||
const name = validateWorkspaceName(nameInput)
|
||||
const current = await load()
|
||||
const parent = join(app.getPath('home'), 'YCodeWorkspaces')
|
||||
await fs.mkdir(parent, { recursive: true })
|
||||
const root = join(parent, name)
|
||||
try { await fs.mkdir(root) } catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'EEXIST') throw new Error('同名文件夹已存在,请更换名称或打开已有文件夹')
|
||||
throw error
|
||||
}
|
||||
const workspace = { id: randomUUID(), name, root, createdAt: Date.now() }
|
||||
const next = { workspaces: [...current.workspaces, workspace], selectedWorkspaceId: workspace.id }
|
||||
try { await persist(next) } catch (error) { await fs.rmdir(root).catch(() => undefined); throw error }
|
||||
return structuredClone(next)
|
||||
})
|
||||
}
|
||||
export async function attachWorkspace(path: string): Promise<WorkspaceRegistryView> {
|
||||
return mutate(async () => {
|
||||
const root = resolve(path)
|
||||
if (!(await fs.stat(root)).isDirectory()) throw new Error('请选择文件夹')
|
||||
const current = await load()
|
||||
const existing = current.workspaces.find((item) => item.root.toLowerCase() === root.toLowerCase())
|
||||
const record: WorkspaceRecord = existing ?? { id: randomUUID(), name: basename(root), root, createdAt: Date.now() }
|
||||
const next = { workspaces: existing ? current.workspaces : [...current.workspaces, record], selectedWorkspaceId: record.id }
|
||||
await persist(next)
|
||||
return structuredClone(next)
|
||||
})
|
||||
}
|
||||
export async function selectWorkspace(id: string | null): Promise<WorkspaceRegistryView> {
|
||||
return mutate(async () => {
|
||||
const current = await load()
|
||||
if (id && !current.workspaces.some((item) => item.id === id)) throw new Error('空间不存在')
|
||||
const workspace = current.workspaces.find((item) => item.id === id)
|
||||
if (workspace && !(await fs.stat(workspace.root).then((info) => info.isDirectory()).catch(() => false))) throw new Error('空间文件夹不存在,请重新打开文件夹')
|
||||
const next = { ...current, selectedWorkspaceId: id }
|
||||
await persist(next)
|
||||
return structuredClone(next)
|
||||
})
|
||||
}
|
||||
export async function renameWorkspace(id: string, nameInput: string): Promise<WorkspaceRegistryView> {
|
||||
return mutate(async () => {
|
||||
const name = nameInput.trim()
|
||||
if (!name) throw new Error('请输入空间名称')
|
||||
const current = await load()
|
||||
if (!current.workspaces.some((item) => item.id === id)) throw new Error('空间不存在')
|
||||
const next = { ...current, workspaces: current.workspaces.map((item) => item.id === id ? { ...item, name } : item) }
|
||||
await persist(next)
|
||||
return structuredClone(next)
|
||||
})
|
||||
}
|
||||
export async function removeWorkspace(id: string): Promise<WorkspaceRegistryView> {
|
||||
return mutate(async () => {
|
||||
const current = await load()
|
||||
const next = { workspaces: current.workspaces.filter((item) => item.id !== id), selectedWorkspaceId: current.selectedWorkspaceId === id ? null : current.selectedWorkspaceId }
|
||||
// Removing the sidebar registration never deletes the user's files.
|
||||
await persist(next)
|
||||
return structuredClone(next)
|
||||
})
|
||||
}
|
||||
export async function workspaceRoot(id?: string): Promise<string> {
|
||||
const current = await load()
|
||||
const selected = id ?? current.selectedWorkspaceId
|
||||
const record = current.workspaces.find((item) => item.id === selected)
|
||||
if (id && !record) throw new Error('空间不存在')
|
||||
const root = record?.root ?? await getWorkspaceRoot()
|
||||
if (record) {
|
||||
if (!(await fs.stat(root).then((info) => info.isDirectory()).catch(() => false))) throw new Error('空间文件夹不存在,请重新打开文件夹')
|
||||
} else await fs.mkdir(root, { recursive: true })
|
||||
return root
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Preload:contextBridge 暴露受控 API。
|
||||
* 渲染进程只能看到这里列出的方法,拿不到 ipcRenderer 本体。
|
||||
*/
|
||||
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
import type { ChatEvent, WindowState } from '@shared/types'
|
||||
import type { ConfigPatchLike, EditAction, SendPayload, WorkspaceAttachResult, YCodeApi } from '@shared/ipc'
|
||||
import type { DownloadPayload, ResourceRange } from '@shared/resources'
|
||||
import type { InteractionResponse } from '@shared/conversation'
|
||||
|
||||
const api: YCodeApi = {
|
||||
experts: {
|
||||
list: () => ipcRenderer.invoke('experts:list'),
|
||||
read: id => ipcRenderer.invoke('experts:read', id),
|
||||
activate: id => ipcRenderer.invoke('experts:activate', id),
|
||||
remove: id => ipcRenderer.invoke('experts:remove', id),
|
||||
openPath: id => ipcRenderer.invoke('experts:open-path', id)
|
||||
},
|
||||
skills: {
|
||||
list: (cwd) => ipcRenderer.invoke('skills:list', cwd),
|
||||
importPath: (path, overwrite, kind) => ipcRenderer.invoke('skills:import', path, overwrite, kind),
|
||||
read: (id, cwd) => ipcRenderer.invoke('skills:read', id, cwd),
|
||||
setEnabled: (id, enabled, cwd) => ipcRenderer.invoke('skills:enable', id, enabled, cwd),
|
||||
remove: (id) => ipcRenderer.invoke('skills:remove', id),
|
||||
export: (id, destination) => ipcRenderer.invoke('skills:export', id, destination)
|
||||
},
|
||||
mcp: {
|
||||
list: () => ipcRenderer.invoke('mcp:list'),
|
||||
save: (input) => ipcRenderer.invoke('mcp:save', input),
|
||||
trust: (id, hash) => ipcRenderer.invoke('mcp:trust', id, hash),
|
||||
connect: (id) => ipcRenderer.invoke('mcp:connect', id),
|
||||
disconnect: (id) => ipcRenderer.invoke('mcp:disconnect', id),
|
||||
setEnabled: (id, enabled) => ipcRenderer.invoke('mcp:enable', id, enabled),
|
||||
setToolEnabled: (id, tool, enabled) => ipcRenderer.invoke('mcp:tool-enable', id, tool, enabled),
|
||||
remove: (id) => ipcRenderer.invoke('mcp:remove', id)
|
||||
},
|
||||
terminal: {
|
||||
rebind: (id, previousSessionId, nextSessionId) => ipcRenderer.invoke('terminal:rebind', id, previousSessionId, nextSessionId),
|
||||
create: (sessionId, cols, rows) => ipcRenderer.invoke('terminal:create', sessionId, cols, rows),
|
||||
get: (id, sessionId) => ipcRenderer.invoke('terminal:get', id, sessionId),
|
||||
write: (id, data, sessionId) => ipcRenderer.invoke('terminal:write', id, data, sessionId),
|
||||
resize: (id, cols, rows, sessionId) => ipcRenderer.invoke('terminal:resize', id, cols, rows, sessionId),
|
||||
clear: (id, sessionId) => ipcRenderer.invoke('terminal:clear', id, sessionId),
|
||||
close: (id, sessionId) => ipcRenderer.invoke('terminal:close', id, sessionId),
|
||||
onEvent: (listener) => {
|
||||
const handler = (_event: unknown, payload: Parameters<typeof listener>[0]): void => listener(payload)
|
||||
ipcRenderer.on('terminal:event', handler)
|
||||
return () => ipcRenderer.removeListener('terminal:event', handler)
|
||||
}
|
||||
},
|
||||
browser: {
|
||||
rebind: (id, previousSessionId, nextSessionId) => ipcRenderer.invoke('browser:rebind', id, previousSessionId, nextSessionId),
|
||||
create: (sessionId, url) => ipcRenderer.invoke('browser:create', sessionId, url),
|
||||
get: (id, sessionId) => ipcRenderer.invoke('browser:get', id, sessionId),
|
||||
navigate: (id, url, sessionId) => ipcRenderer.invoke('browser:navigate', id, url, sessionId),
|
||||
back: (id, sessionId) => ipcRenderer.invoke('browser:back', id, sessionId),
|
||||
forward: (id, sessionId) => ipcRenderer.invoke('browser:forward', id, sessionId),
|
||||
reload: (id, sessionId) => ipcRenderer.invoke('browser:reload', id, sessionId),
|
||||
bounds: (id, bounds, sessionId) => ipcRenderer.invoke('browser:bounds', id, bounds, sessionId),
|
||||
close: (id, sessionId) => ipcRenderer.invoke('browser:close', id, sessionId)
|
||||
},
|
||||
window: {
|
||||
minimize: () => ipcRenderer.invoke('window:minimize'),
|
||||
toggleMaximize: () => ipcRenderer.invoke('window:toggleMaximize'),
|
||||
close: () => ipcRenderer.invoke('window:close'),
|
||||
getState: () => ipcRenderer.invoke('window:state'),
|
||||
onState: (listener: (state: WindowState) => void) => {
|
||||
const handler = (_event: unknown, payload: WindowState): void => listener(payload)
|
||||
ipcRenderer.on('window:state', handler)
|
||||
return () => {
|
||||
ipcRenderer.removeListener('window:state', handler)
|
||||
}
|
||||
},
|
||||
onFocus: (listener: (focused: boolean) => void) => {
|
||||
const handler = (_event: unknown, payload: { focused: boolean }): void =>
|
||||
listener(payload.focused)
|
||||
ipcRenderer.on('window:focus', handler)
|
||||
return () => {
|
||||
ipcRenderer.removeListener('window:focus', handler)
|
||||
}
|
||||
}
|
||||
},
|
||||
edit: {
|
||||
action: (action: EditAction) => ipcRenderer.invoke('edit:action', action)
|
||||
},
|
||||
app: {
|
||||
info: () => ipcRenderer.invoke('app:info'),
|
||||
openConfigDir: () => ipcRenderer.invoke('app:openConfigDir'),
|
||||
openSettings: (section, action) => ipcRenderer.invoke('app:openSettings', section, action),
|
||||
onSettingsNavigate: (listener) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, section: string, action?: 'new'): void => listener(section, action)
|
||||
ipcRenderer.on('settings:navigate', handler)
|
||||
return () => { ipcRenderer.removeListener('settings:navigate', handler) }
|
||||
},
|
||||
onSettingsChanged: (listener) => {
|
||||
const handler = (): void => listener()
|
||||
ipcRenderer.on('settings:changed', handler)
|
||||
return () => ipcRenderer.removeListener('settings:changed', handler)
|
||||
}
|
||||
},
|
||||
config: {
|
||||
get: () => ipcRenderer.invoke('config:get'),
|
||||
update: (patch: ConfigPatchLike) => ipcRenderer.invoke('config:update', patch)
|
||||
},
|
||||
workspace: {
|
||||
get: () => ipcRenderer.invoke('workspace:get'),
|
||||
choose: () => ipcRenderer.invoke('workspace:choose'),
|
||||
list: () => ipcRenderer.invoke('workspace:list'),
|
||||
generate: (name) => ipcRenderer.invoke('workspace:generate', name),
|
||||
attach: (path): Promise<WorkspaceAttachResult> => ipcRenderer.invoke('workspace:attach', path),
|
||||
select: (id) => ipcRenderer.invoke('workspace:select', id),
|
||||
rename: (id, name) => ipcRenderer.invoke('workspace:rename', id, name),
|
||||
remove: (id) => ipcRenderer.invoke('workspace:remove', id),
|
||||
open: (id) => ipcRenderer.invoke('workspace:open', id)
|
||||
},
|
||||
models: {
|
||||
list: () => ipcRenderer.invoke('models:list'),
|
||||
save: (input, overwrite) => ipcRenderer.invoke('models:save', input, overwrite),
|
||||
remove: (id) => ipcRenderer.invoke('models:remove', id),
|
||||
select: (id) => ipcRenderer.invoke('models:select', id),
|
||||
test: (input) => ipcRenderer.invoke('models:test', input)
|
||||
},
|
||||
resources: {
|
||||
list: (sessionId?: string, workspaceId?: string) => ipcRenderer.invoke('resources:list', sessionId, workspaceId),
|
||||
openExternal: (url) => ipcRenderer.invoke('resources:openExternal', url),
|
||||
read: (path: string, range?: ResourceRange, sessionId?: string) => ipcRenderer.invoke('resources:read', path, range, sessionId),
|
||||
readText: (path: string, sessionId?: string) => ipcRenderer.invoke('resources:read-text', path, sessionId),
|
||||
classify: (path: string, sessionId?: string) => ipcRenderer.invoke('resources:classify', path, sessionId),
|
||||
resolveImage: (source: string, sessionId?: string) => ipcRenderer.invoke('resources:resolveImage', source, sessionId),
|
||||
download: (payload: DownloadPayload) => ipcRenderer.invoke('resources:download', payload),
|
||||
downloadImage: (source: string, filename?: string, sessionId?: string) => ipcRenderer.invoke('resources:downloadImage', source, filename, sessionId)
|
||||
},
|
||||
attachments: {
|
||||
pick: () => ipcRenderer.invoke('attachments:pick'),
|
||||
stage: (payload) => ipcRenderer.invoke('attachments:stage', payload),
|
||||
remove: (id) => ipcRenderer.invoke('attachments:remove', id)
|
||||
},
|
||||
speech: { transcribe: (payload) => ipcRenderer.invoke('speech:transcribe', payload) },
|
||||
session: {
|
||||
list: () => ipcRenderer.invoke('session:list'),
|
||||
create: (title, context) => ipcRenderer.invoke('session:create', title, context),
|
||||
updateContext: (id, context) => ipcRenderer.invoke('session:update-context', id, context),
|
||||
messages: (sessionId: string) => ipcRenderer.invoke('session:messages', sessionId),
|
||||
rename: (sessionId: string, title: string) => ipcRenderer.invoke('session:rename', sessionId, title),
|
||||
remove: (sessionId: string) => ipcRenderer.invoke('session:delete', sessionId)
|
||||
},
|
||||
chat: {
|
||||
revert: (sessionId, userMessageId) => ipcRenderer.invoke('chat:revert', sessionId, userMessageId),
|
||||
feedback: (sessionId, messageId, value) => ipcRenderer.invoke('chat:feedback', sessionId, messageId, value),
|
||||
send: (payload: SendPayload) => ipcRenderer.invoke('chat:send', payload),
|
||||
cancel: (runId: string) => ipcRenderer.invoke('chat:cancel', runId),
|
||||
respond: (runId: string, interactionId: string, response: InteractionResponse) => ipcRenderer.invoke('chat:respond', runId, interactionId, response),
|
||||
onEvent: (listener: (event: ChatEvent) => void) => {
|
||||
const handler = (_event: unknown, payload: ChatEvent): void => listener(payload)
|
||||
ipcRenderer.on('chat:event', handler)
|
||||
return () => {
|
||||
ipcRenderer.removeListener('chat:event', handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld('ycode', api)
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self' 'unsafe-inline'; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https: http:; font-src 'self' data:; connect-src 'self' ws: http://localhost:*; object-src 'none'; base-uri 'none'"
|
||||
/>
|
||||
<title>YCode</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,356 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Sidebar from './components/Sidebar'
|
||||
import ChatPane from './components/ChatPane'
|
||||
import CapabilityCenter from './capabilities/CapabilityCenter'
|
||||
import { type SkillRecord } from '@shared/capabilities'
|
||||
import RightSidebar from './components/RightSidebar'
|
||||
import { useWorkbenchStore } from './stores/workbench'
|
||||
import { emptyWorkbenchSession, workbenchOwner } from './workbuddy/workbench/workbench-state'
|
||||
import { Rw, Vw, clampSidebarWidth, getNarrowState, resolveNarrowState, resolveSidebarDragAction,
|
||||
resolveDetailDrawerWidth, resolveDetailPanelLayoutWidths, resolveRememberedDetailWidth } from './workbuddy/workbench/source-panel-layout'
|
||||
import { useSourcePanelMotion } from './workbuddy/workbench/useSourcePanelMotion'
|
||||
import { useSourceDrawerMotion } from './workbuddy/workbench/useSourceDrawerMotion'
|
||||
import './workbuddy/workbench/panel-motion-source.css'
|
||||
import './workbuddy/workbench/drawer-motion-source.css'
|
||||
import './workbuddy/workbench/panel-layout-adapters.css'
|
||||
import { NativeBrowserPane } from './workbuddy/workbench/NativeBrowserPane'
|
||||
import { NativeTerminalPane } from './workbuddy/workbench/NativeTerminalPane'
|
||||
import type { WorkbenchHost } from './workbuddy/workbench/SourceWorkbench'
|
||||
import type { ResourceRange, WorkspaceResource } from '@shared/resources'
|
||||
import SettingsDialog from './components/SettingsDialog'
|
||||
import AboutDialog from './components/AboutDialog'
|
||||
import TitleBar from './components/TitleBar'
|
||||
import { useChatStore } from './stores/chat'
|
||||
import { useSettingsStore } from './stores/settings'
|
||||
import { initFontSize } from './fontSize'
|
||||
import { useSharedTheme } from './useSharedTheme'
|
||||
|
||||
const emptyPanel = emptyWorkbenchSession()
|
||||
function readSidebarWidth(): number {
|
||||
const saved = Number(localStorage.getItem('ycode:sidebar-width'))
|
||||
return Number.isFinite(saved) && saved > 0 ? clampSidebarWidth(saved) : Rw.EXPANDED_WIDTH
|
||||
}
|
||||
|
||||
export default function App(): React.JSX.Element {
|
||||
const [theme, setTheme] = useSharedTheme()
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
const [aboutOpen, setAboutOpen] = useState(false)
|
||||
const [route, setRoute] = useState<'chat' | 'capabilities'>('chat')
|
||||
const [capabilityTab, setCapabilityTab] = useState<'experts' | 'skills' | 'connectors'>('experts')
|
||||
const [capabilityExpertPage, setCapabilityExpertPage] = useState<'market' | 'mine'>('market')
|
||||
const expertReturnOwner = useRef<string | undefined>(undefined)
|
||||
const [resizing, setResizing] = useState(false)
|
||||
const [viewportWidth, setViewportWidth] = useState(window.innerWidth)
|
||||
const [narrow, setNarrow] = useState(() => getNarrowState(window.innerWidth))
|
||||
const [sidebarWidth, setSidebarWidth] = useState(readSidebarWidth)
|
||||
const [leftDrawerOpen, setLeftDrawerOpen] = useState(false)
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState<boolean>(
|
||||
() => localStorage.getItem('ycode:sidebar-collapsed') === '1'
|
||||
)
|
||||
const currentSessionId = useChatStore(state => state.currentSessionId)
|
||||
const draftId = useChatStore(state => state.draftId)
|
||||
const panelOwner = workbenchOwner(currentSessionId, draftId)
|
||||
const panel = useWorkbenchStore(state => state.sessions[panelOwner] ?? emptyPanel)
|
||||
const leftWidth = narrow.isNarrowForSidebar ? 0 : sidebarCollapsed ? Rw.COLLAPSED_WIDTH : sidebarWidth
|
||||
const rightVisible = route === 'chat' && panel.open === true
|
||||
const sessions = useChatStore(state => state.sessions)
|
||||
const draftContext = useChatStore(state => state.draftContext)
|
||||
const defaultWorkspace = useSettingsStore(state => state.workspace)
|
||||
const capabilityCwd = sessions.find(session => session.id === currentSessionId)?.cwd ?? draftContext.cwd ?? defaultWorkspace
|
||||
const useSkill = (skill: SkillRecord): void => {
|
||||
setRoute('chat')
|
||||
window.dispatchEvent(new CustomEvent('ycode:select-skill', { detail: { skill, owner: currentSessionId ?? `new-${draftId}` } }))
|
||||
}
|
||||
const creationIntent = (text: string, skill?: Pick<SkillRecord, 'id' | 'name'>): void => {
|
||||
const state = useChatStore.getState()
|
||||
const settings = useSettingsStore.getState()
|
||||
const current = state.sessions.find(session => session.id === state.currentSessionId)
|
||||
state.startTask(current ? { workspaceId: current.workspaceId, cwd: current.cwd, modelId: current.modelId ?? settings.selectedModelId ?? undefined } : { ...state.draftContext, expertId: null, ...(settings.selectedModelId ? { modelId: settings.selectedModelId } : {}) })
|
||||
setRoute('chat')
|
||||
// The chat pane remains mounted. Publish after its new draft owner commits.
|
||||
const owner = `new-${useChatStore.getState().draftId}`
|
||||
requestAnimationFrame(() => window.dispatchEvent(new CustomEvent('ycode:composer-intent', { detail: { text, owner, skill } })))
|
||||
}
|
||||
const useExpert = async (id: string, initPrompt?: string): Promise<void> => {
|
||||
const state = useChatStore.getState(), owner = state.currentSessionId ?? `new-${state.draftId}`
|
||||
const returnOwner = expertReturnOwner.current
|
||||
if (returnOwner && returnOwner !== owner) throw new Error('任务已切换,请重新打开专家选择')
|
||||
await state.summonExpert(id, { newTask: !returnOwner && !!state.currentSessionId, initPrompt })
|
||||
expertReturnOwner.current = undefined
|
||||
setRoute('chat')
|
||||
}
|
||||
useEffect(() => {
|
||||
const open = (event: Event): void => {
|
||||
const detail = (event as CustomEvent<{ tab?: 'experts' | 'skills' | 'connectors'; page?: 'mine' | 'market'; returnOwner?: string }>).detail
|
||||
expertReturnOwner.current = detail?.tab === 'experts' ? detail.returnOwner : undefined
|
||||
const tab = detail?.tab
|
||||
if (tab && ['experts', 'skills', 'connectors'].includes(tab)) setCapabilityTab(tab)
|
||||
if (detail?.page) setCapabilityExpertPage(detail.page)
|
||||
setRoute('capabilities')
|
||||
}
|
||||
window.addEventListener('ycode:open-capabilities', open)
|
||||
return () => window.removeEventListener('ycode:open-capabilities', open)
|
||||
}, [])
|
||||
const rightWidth = narrow.isNarrowForDetail
|
||||
? resolveDetailDrawerWidth({ containerWidth: viewportWidth, sidebarWidth: narrow.isNarrowForSidebar ? 0 : sidebarWidth, isMobile: narrow.isMobile, preferredWidth: panel.width })
|
||||
: resolveDetailPanelLayoutWidths({ containerWidth: viewportWidth, sidebarWidth: leftWidth, requestedDetailWidth: panel.width ?? Vw.MIN_WIDTH })?.detailWidth ?? Vw.MIN_WIDTH
|
||||
const motion = useSourcePanelMotion(panelOwner, rightVisible, rightWidth, !narrow.isNarrowForDetail && !resizing)
|
||||
const rightDrawer = useSourceDrawerMotion(panelOwner, rightVisible, narrow.isNarrowForDetail)
|
||||
const leftDrawer = useSourceDrawerMotion(panelOwner, leftDrawerOpen, narrow.isNarrowForSidebar)
|
||||
const backdropMounted = leftDrawer.mounted || (rightDrawer.mounted && !panel.fullscreen)
|
||||
const backdropVisible = leftDrawer.visible || (rightDrawer.visible && !panel.fullscreen)
|
||||
const setRightVisible = (value: boolean | ((previous: boolean) => boolean)): void => {
|
||||
const next = typeof value === 'function' ? value(rightVisible) : value
|
||||
useWorkbenchStore.getState().setPanel(panelOwner, { open: next, width: panel.width ?? Vw.MIN_WIDTH })
|
||||
}
|
||||
const setRightWidth = (requested: number): void => {
|
||||
const width = narrow.isNarrowForDetail ? resolveDetailDrawerWidth({ containerWidth: viewportWidth, sidebarWidth: leftWidth, preferredWidth: requested, isMobile: narrow.isMobile })
|
||||
: resolveDetailPanelLayoutWidths({ containerWidth: viewportWidth, sidebarWidth: leftWidth, requestedDetailWidth: requested })?.detailWidth ?? Vw.MIN_WIDTH
|
||||
useWorkbenchStore.getState().setPanel(panelOwner, { width, rememberedWidth: width })
|
||||
}
|
||||
useEffect(() => {
|
||||
const resize = (): void => { setViewportWidth(window.innerWidth); setNarrow(previous => resolveNarrowState(previous, window.innerWidth)) }
|
||||
window.addEventListener('resize', resize)
|
||||
return () => window.removeEventListener('resize', resize)
|
||||
}, [])
|
||||
useEffect(() => { setLeftDrawerOpen(false); setResizing(false) }, [panelOwner])
|
||||
useEffect(() => {
|
||||
const showArtifacts = (event: Event): void => {
|
||||
if ((event as CustomEvent<{ sessionId?: string | null }>).detail?.sessionId !== currentSessionId) return
|
||||
const store = useWorkbenchStore.getState()
|
||||
const previous = store.sessions[panelOwner]
|
||||
const overview = previous?.tabs.find(tab => tab.kind === 'newTab')
|
||||
if (overview) store.select(panelOwner, overview.id)
|
||||
else store.openView(panelOwner, 'newTab', false)
|
||||
store.setPanel(panelOwner, { open: true, width: previous?.width ?? Vw.MIN_WIDTH })
|
||||
}
|
||||
window.addEventListener('ycode:open-artifacts', showArtifacts)
|
||||
return () => window.removeEventListener('ycode:open-artifacts', showArtifacts)
|
||||
}, [panelOwner, currentSessionId])
|
||||
|
||||
const init = useChatStore((s) => s.init)
|
||||
const applyEvent = useChatStore((s) => s.applyEvent)
|
||||
const loadSettings = useSettingsStore((s) => s.load)
|
||||
const openSettings = (): void => {
|
||||
if (window.ycode.app.openSettings) void window.ycode.app.openSettings('model')
|
||||
else setSettingsOpen(true)
|
||||
}
|
||||
const host = useMemo<WorkbenchHost>(() => ({
|
||||
createBrowser: (sessionId) => window.ycode.browser.create(sessionId),
|
||||
closeBrowser: (id, sessionId) => window.ycode.browser.close(id, sessionId),
|
||||
renderBrowser: (id, sessionId) => <NativeBrowserPane id={id} sessionId={sessionId} onTitleChange={(title) => useWorkbenchStore.getState().labelHost('browser', id, title)} />,
|
||||
createTerminal: (sessionId) => window.ycode.terminal.create(sessionId),
|
||||
closeTerminal: (id, sessionId) => window.ycode.terminal.close(id, sessionId),
|
||||
renderTerminal: (id, sessionId) => <NativeTerminalPane id={id} sessionId={sessionId} theme={theme}
|
||||
onAddSelectionToChat={(text) => window.dispatchEvent(new CustomEvent('ycode:quote-text', { detail: { text, sessionId } }))}
|
||||
onCreateTerminal={async () => {
|
||||
const chat = useChatStore.getState(), owner = workbenchOwner(chat.currentSessionId, chat.draftId)
|
||||
const terminal = await window.ycode.terminal.create(chat.currentSessionId ?? undefined)
|
||||
useWorkbenchStore.getState().openHost(owner, 'terminal', terminal.id, terminal.title)
|
||||
}} />
|
||||
}), [theme])
|
||||
useEffect(() => {
|
||||
const closeRemovedHosts = useWorkbenchStore.subscribe((next, previous) => {
|
||||
const retained = new Set(Object.values(next.sessions).flatMap(panel => panel.tabs.flatMap(tab => tab.hostId ? [tab.hostId] : [])))
|
||||
for (const [owner, panel] of Object.entries(previous.sessions)) for (const tab of panel.tabs) {
|
||||
if (!tab.hostId || retained.has(tab.hostId)) continue
|
||||
const sessionId = owner.startsWith('draft:') ? undefined : owner
|
||||
if (tab.kind === 'browser') void window.ycode.browser.close(tab.hostId, sessionId).catch(() => undefined)
|
||||
if (tab.kind === 'terminal') void window.ycode.terminal.close(tab.hostId, sessionId).catch(() => undefined)
|
||||
}
|
||||
})
|
||||
const stopChat = useChatStore.subscribe((next, previous) => {
|
||||
const retainedSessions = new Set(next.sessions.map(session => session.id))
|
||||
for (const session of previous.sessions) if (!retainedSessions.has(session.id)) useWorkbenchStore.getState().drop(session.id)
|
||||
const previousOwner = workbenchOwner(previous.currentSessionId, previous.draftId)
|
||||
const nextOwner = workbenchOwner(next.currentSessionId, next.draftId)
|
||||
if (previousOwner === nextOwner || !previousOwner.startsWith('draft:')) return
|
||||
const panel = useWorkbenchStore.getState().sessions[previousOwner]
|
||||
if (!panel) return
|
||||
const createdSession = next.currentSessionId && !previous.sessions.some(session => session.id === next.currentSessionId)
|
||||
if (!createdSession || !next.currentSessionId) { useWorkbenchStore.getState().drop(previousOwner); return }
|
||||
const sessionId = next.currentSessionId
|
||||
void Promise.all(panel.tabs.flatMap<Promise<unknown>>(tab => tab.hostId && tab.kind === 'browser'
|
||||
? [window.ycode.browser.rebind(tab.hostId, undefined, sessionId)]
|
||||
: tab.hostId && tab.kind === 'terminal' ? [window.ycode.terminal.rebind(tab.hostId, undefined, sessionId)] : []))
|
||||
.then(() => useWorkbenchStore.getState().adoptDraft(previousOwner, sessionId))
|
||||
.catch((error) => {
|
||||
for (const tab of panel.tabs) if (tab.hostId) {
|
||||
if (tab.kind === 'browser') void window.ycode.browser.close(tab.hostId, sessionId).catch(() => undefined)
|
||||
if (tab.kind === 'terminal') void window.ycode.terminal.close(tab.hostId, sessionId).catch(() => undefined)
|
||||
}
|
||||
useWorkbenchStore.getState().drop(previousOwner)
|
||||
useChatStore.setState({ error: String(error.message ?? error) })
|
||||
})
|
||||
})
|
||||
return () => { closeRemovedHosts(); stopChat() }
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
const onPreview = (event: Event): void => {
|
||||
const detail = (event as CustomEvent<{ path?: string; range?: ResourceRange; resource?: WorkspaceResource }>).detail
|
||||
if (typeof detail?.path !== 'string') return
|
||||
const chat = useChatStore.getState()
|
||||
const owner = workbenchOwner(chat.currentSessionId, chat.draftId)
|
||||
const previous = useWorkbenchStore.getState().sessions[owner]
|
||||
void useWorkbenchStore.getState().openResource(owner, chat.currentSessionId ?? undefined, detail.path, detail.range, detail.resource)
|
||||
const sidebar = document.querySelector('.app-shell')?.getBoundingClientRect().width ?? window.innerWidth
|
||||
const left = document.querySelector('.sidebar')?.getBoundingClientRect().width ?? Rw.EXPANDED_WIDTH
|
||||
useWorkbenchStore.getState().setPanel(owner, { open: true, width: resolveRememberedDetailWidth([previous?.rememberedWidth, previous?.width], sidebar - left) })
|
||||
}
|
||||
window.addEventListener('ycode:preview-resource', onPreview)
|
||||
return () => window.removeEventListener('ycode:preview-resource', onPreview)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
void loadSettings()
|
||||
void init()
|
||||
const unsubscribe = window.ycode.app.onSettingsChanged?.(() => {
|
||||
void loadSettings()
|
||||
void window.ycode.session.list().then((sessions) => useChatStore.setState({ sessions }))
|
||||
})
|
||||
// 订阅主进程流式事件;返回的取消函数在卸载时调用
|
||||
const stopEvents = window.ycode.chat.onEvent(applyEvent)
|
||||
return () => { stopEvents(); unsubscribe?.() }
|
||||
}, [applyEvent, init, loadSettings])
|
||||
|
||||
useEffect(() => { localStorage.setItem('ycode:sidebar-width', String(sidebarWidth)) }, [sidebarWidth])
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('ycode:sidebar-collapsed', sidebarCollapsed ? '1' : '0')
|
||||
}, [sidebarCollapsed])
|
||||
|
||||
// 拖拽调宽:面板在右侧,鼠标左移 = 变宽
|
||||
const dragRef = useRef<{ side: 'left' | 'right'; startX: number; startWidth: number } | null>(null)
|
||||
useEffect(() => {
|
||||
if (!resizing) return
|
||||
const onMove = (e: MouseEvent): void => {
|
||||
const drag = dragRef.current
|
||||
if (!drag) return
|
||||
if (drag.side === 'right') setRightWidth(drag.startWidth + drag.startX - e.clientX)
|
||||
else {
|
||||
const width = drag.startWidth + e.clientX - drag.startX
|
||||
const action = resolveSidebarDragAction(width, sidebarCollapsed)
|
||||
if (action === 'collapse') setSidebarCollapsed(true)
|
||||
else if (action !== 'keep-collapsed') { setSidebarCollapsed(false); setSidebarWidth(clampSidebarWidth(width)) }
|
||||
}
|
||||
}
|
||||
const onUp = (): void => {
|
||||
dragRef.current = null
|
||||
setResizing(false)
|
||||
}
|
||||
window.addEventListener('mousemove', onMove)
|
||||
window.addEventListener('mouseup', onUp)
|
||||
document.body.style.cursor = 'col-resize'
|
||||
document.body.style.userSelect = 'none'
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', onMove)
|
||||
window.removeEventListener('mouseup', onUp)
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
}
|
||||
}, [resizing, sidebarCollapsed, viewportWidth, leftWidth, panelOwner])
|
||||
|
||||
const startResize = (e: React.MouseEvent): void => {
|
||||
e.preventDefault()
|
||||
dragRef.current = { side: 'right', startX: e.clientX, startWidth: rightWidth }
|
||||
setResizing(true)
|
||||
}
|
||||
|
||||
// Ctrl+, 打开设置
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent): void => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === ',') {
|
||||
e.preventDefault()
|
||||
openSettings()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', onKey)
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [])
|
||||
|
||||
// 全局字号档位:读取存档 + 绑定 Ctrl+-/+/0
|
||||
useEffect(() => initFontSize(), [])
|
||||
|
||||
return (
|
||||
<div className="app-root">
|
||||
<TitleBar
|
||||
onOpenSettings={openSettings}
|
||||
onShowAbout={() => setAboutOpen(true)}
|
||||
onToggleSidebar={() => narrow.isNarrowForSidebar ? setLeftDrawerOpen(value => !value) : setSidebarCollapsed(value => !value)}
|
||||
onFocusSearch={() => {
|
||||
// 收起态先展开,再让侧栏聚焦搜索框
|
||||
setSidebarCollapsed(false)
|
||||
if (narrow.isNarrowForSidebar) setLeftDrawerOpen(true)
|
||||
window.dispatchEvent(new CustomEvent('ycode:focus-search'))
|
||||
}}
|
||||
onFilter={() => window.dispatchEvent(new CustomEvent('ycode:not-implemented', { detail: '筛选' }))}
|
||||
/>
|
||||
<div
|
||||
ref={motion.containerRef}
|
||||
className={`app-shell source-detail-layout detail-panel-motion-${motion.phase}`}
|
||||
data-right={motion.layoutOpen && !narrow.isNarrowForDetail ? 'shown' : 'hidden'}
|
||||
data-drawer={narrow.isNarrowForDetail ? 'true' : undefined}
|
||||
data-resizing={resizing ? 'true' : undefined}
|
||||
style={
|
||||
{
|
||||
'--yc-layout-right': `${rightWidth}px`,
|
||||
'--wb-detail-panel-presentation-width': `${motion.presentationWidth}px`,
|
||||
'--yc-layout-main': `${Math.max(0, viewportWidth - leftWidth - (motion.layoutOpen && !narrow.isNarrowForDetail ? rightWidth : 0))}px`,
|
||||
'--yc-layout-sidebar': `${leftWidth}px`
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{narrow.isNarrowForSidebar ? <div className="app-shell__sidebar-space" /> : <Sidebar
|
||||
activeRoute={route} onNavigate={next => { expertReturnOwner.current = undefined; setRoute(next) }}
|
||||
collapsed={sidebarCollapsed}
|
||||
onToggleCollapse={() => setSidebarCollapsed((v) => !v)}
|
||||
/>}
|
||||
{!narrow.isNarrowForSidebar && <div className="app-shell__sidebar-resizer" role="separator" aria-orientation="vertical" aria-label="调整侧栏宽度"
|
||||
onMouseDown={event => { event.preventDefault(); dragRef.current = { side: 'left', startX: event.clientX, startWidth: leftWidth }; setResizing(true) }}
|
||||
onDoubleClick={() => { setSidebarCollapsed(false); setSidebarWidth(Rw.EXPANDED_WIDTH) }} />}
|
||||
<ChatPane
|
||||
hidden={route !== 'chat'}
|
||||
darkMode={theme === 'dark'}
|
||||
rightVisible={rightVisible}
|
||||
onToggleRight={() => setRightVisible((v) => !v)}
|
||||
onOpenSettings={openSettings}
|
||||
/>
|
||||
{route === 'capabilities' && <CapabilityCenter initialTab={capabilityTab} initialExpertPage={capabilityExpertPage} cwd={capabilityCwd} onBackToChat={() => setRoute('chat')} onUseSkill={useSkill}
|
||||
onCreateSkill={() => creationIntent('帮我创建一个技能。请先和我确认用途、触发条件和交付要求,再在当前工作区的 .agents/skills/ 中创建包含 SKILL.md 的技能目录。')}
|
||||
onFindSkill={() => creationIntent('帮我查找适合当前需求的技能。请先询问我希望完成什么任务。')}
|
||||
onUseExpert={useExpert} onEditExpert={id => creationIntent(`帮我修改专家:[${id}],增加/优化[请补充你希望新增/优化的技能或知识领域等]方面的能力`, { id: 'builtin:expert-manager', name: 'expert-manager' })}
|
||||
onCreateExpert={() => creationIntent('帮我创建一个 XXX 专家,擅长 XXXXX。我的经验是:[请补充你的行业背景、相关经验]', { id: 'builtin:expert-manager', name: 'expert-manager' })} />}
|
||||
{backdropMounted && <button className={`app-shell__drawer-backdrop _drawerBackdrop_11lon_150${backdropVisible ? ' _drawerBackdropOpen_11lon_163' : ''}`} aria-label="关闭抽屉" onClick={() => { setLeftDrawerOpen(false); setRightVisible(false) }} />}
|
||||
{leftDrawer.mounted && <div className={`app-shell__left-drawer _gridViewDrawerItem_11lon_203 _drawerItemLeft_11lon_210${leftDrawer.visible ? ' _gridViewDrawerItemOpen_11lon_245' : ''}`} style={{ width: sidebarWidth }}><Sidebar activeRoute={route} onNavigate={next => { expertReturnOwner.current = undefined; setRoute(next); setLeftDrawerOpen(false) }} collapsed={false} onToggleCollapse={() => setLeftDrawerOpen(false)} /></div>}
|
||||
{(narrow.isNarrowForDetail ? rightDrawer.mounted : motion.mounted) ? (
|
||||
<>
|
||||
{!narrow.isNarrowForDetail && <div
|
||||
className="app-shell__resizer"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label="调整详情面板宽度"
|
||||
title="拖动调整宽度,双击还原"
|
||||
onMouseDown={startResize}
|
||||
onDoubleClick={() => setRightWidth(Math.floor((viewportWidth - leftWidth) / 2))}
|
||||
/>}
|
||||
<div data-view-id="detail-panel" className={`app-shell__detail-slot${narrow.isNarrowForDetail ? ` app-shell__detail-slot--drawer _gridViewDrawerItem_11lon_203 _drawerItemRight_11lon_217${rightDrawer.visible ? ' _gridViewDrawerItemOpen_11lon_245' : ''}${narrow.isMobile ? ' _drawerItemFullScreen_11lon_238' : ''}` : ''}`} style={{ width: rightWidth }}>
|
||||
<div className="detail-panel-container"><RightSidebar
|
||||
theme={theme}
|
||||
onToggleTheme={() => setTheme(theme === 'light' ? 'dark' : 'light')}
|
||||
onClose={() => { useWorkbenchStore.getState().fullscreen(panelOwner, false); setRightVisible(false) }}
|
||||
host={host}
|
||||
/></div></div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
{settingsOpen ? (
|
||||
<SettingsDialog
|
||||
theme={theme}
|
||||
onThemeChange={setTheme}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
{aboutOpen ? <AboutDialog onClose={() => setAboutOpen(false)} /> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import type { SkillRecord } from '@shared/capabilities'
|
||||
import SkillsPage from './SkillsPage'
|
||||
import ConnectorsPage from './ConnectorsPage'
|
||||
import ExpertsPage from './ExpertsPage'
|
||||
import { BackIcon, UnifiedMarketHeader } from './assets/source-ui.mjs'
|
||||
import classes from './assets/source-classes.json'
|
||||
import './capability-center-source.css'
|
||||
|
||||
type Tab = 'experts' | 'skills' | 'connectors'
|
||||
interface Slots { toolbar: HTMLDivElement | null; title: HTMLDivElement | null; setSubpage(value: boolean): void }
|
||||
const HeaderSlots = createContext<Slots | null>(null)
|
||||
|
||||
/** The selected page owns its search and management actions; the shell owns the single source topbar. */
|
||||
export function CapabilityToolbar({ children }: { children: ReactNode }): React.JSX.Element | null {
|
||||
const slots = useContext(HeaderSlots)
|
||||
return slots?.toolbar ? createPortal(children, slots.toolbar) : null
|
||||
}
|
||||
|
||||
export function CapabilitySubpageHeader({ title, onBack }: { title: string; onBack(): void }): React.JSX.Element | null {
|
||||
const slots = useContext(HeaderSlots)
|
||||
useEffect(() => { slots?.setSubpage(true); return () => slots?.setSubpage(false) }, [slots?.setSubpage])
|
||||
return slots?.title ? createPortal(<button type="button" className={classes.tz.subpageNav} onClick={onBack} aria-label="返回专家中心">
|
||||
<span className={classes.tz.subpageBack} aria-hidden="true"><BackIcon width={20} height={20} /></span>
|
||||
<span className={classes.tz.subpageTitle}>{title}</span>
|
||||
</button>, slots.title) : null
|
||||
}
|
||||
|
||||
export interface CapabilityCenterProps {
|
||||
initialTab?: Tab
|
||||
initialExpertPage?: 'market' | 'mine'
|
||||
cwd?: string
|
||||
onBackToChat(): void
|
||||
onUseSkill?(skill: SkillRecord): void
|
||||
onCreateSkill?(): void
|
||||
onFindSkill?(): void
|
||||
onCreateExpert?(): void
|
||||
onUseExpert?(id: string, initPrompt?: string): Promise<void>
|
||||
onEditExpert?(id: string): void
|
||||
}
|
||||
|
||||
export default function CapabilityCenter(props: CapabilityCenterProps): React.JSX.Element {
|
||||
const [tab, setTab] = useState<Tab>(props.initialTab ?? 'experts')
|
||||
const [toolbar, setToolbar] = useState<HTMLDivElement | null>(null)
|
||||
const [title, setTitle] = useState<HTMLDivElement | null>(null)
|
||||
const [subpage, setSubpage] = useState(false)
|
||||
const [theme, setTheme] = useState(() => document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light')
|
||||
useEffect(() => { if (props.initialTab) setTab(props.initialTab) }, [props.initialTab])
|
||||
useEffect(() => {
|
||||
const observer = new MutationObserver(() => setTheme(document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light'))
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] })
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
const c = classes.tz
|
||||
return <div className="yc-capability-center" data-capability-tab={tab} data-electron-desktop="true" data-platform="win">
|
||||
<HeaderSlots.Provider value={{ toolbar, title, setSubpage }}>
|
||||
<section className={`${c.page} expert-center-page expert-center-${theme}`}>
|
||||
<div className={c.layout}><div className={c.main}>
|
||||
<header className={`workbuddy-topbar ${c.topbar} ec-topbar${subpage ? ` ${c.topbarSubpage} ec-topbar--subpage` : ''}`}>
|
||||
<div className={`${c.topbarLeft} ec-topbar__left`}>
|
||||
{!subpage && <UnifiedMarketHeader activeTab={tab} onTabChange={setTab} />}
|
||||
<div ref={setTitle} className="capability-title-slot" />
|
||||
</div>
|
||||
<div ref={setToolbar} className={`${c.topbarRight} ec-topbar__right${subpage ? ` ${c.topbarRightMyExperts}` : ''}`} />
|
||||
</header>
|
||||
<div className="capability-page-host" role="tabpanel" aria-label={{ experts: '专家', skills: '技能', connectors: '连接器' }[tab]}>
|
||||
{tab === 'experts' && <ExpertsPage initialPage={props.initialExpertPage} onCreateExpert={props.onCreateExpert} onUseExpert={props.onUseExpert} onEditExpert={props.onEditExpert} onBackToChat={props.onBackToChat} />}
|
||||
{tab === 'skills' && <SkillsPage cwd={props.cwd} onUseSkill={props.onUseSkill} onCreateSkill={props.onCreateSkill} onFindSkill={props.onFindSkill} />}
|
||||
{tab === 'connectors' && <ConnectorsPage />}
|
||||
</div>
|
||||
</div></div>
|
||||
</section>
|
||||
</HeaderSlots.Provider>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// @ts-nocheck
|
||||
/* REA ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb; skills-panel-CvMlV3m8.js SHA256 b30c43c11ec476db639d6be58065d927411845487d34b0ea8e839d1c6eb7a238. Literal isolated SVG function bodies; original global initialization is not executed. */
|
||||
import {jsx,jsxs} from 'react/jsx-runtime'
|
||||
// UTF-16 [25480,25793)
|
||||
export const ChevronIcon=({expanded:e})=>jsx(`svg`,{width:`16`,height:`16`,viewBox:`0 0 16 16`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,className:`mcp-chevron ${e?`mcp-chevron--expanded`:``}`,children:jsx(`path`,{d:`M6 4L10 8L6 12`,stroke:`currentColor`,strokeWidth:`1.5`,strokeLinecap:`round`,strokeLinejoin:`round`})})
|
||||
// UTF-16 [25803,26182)
|
||||
export const CopyIcon=()=>jsxs(`svg`,{width:`14`,height:`14`,viewBox:`0 0 16 16`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children:[jsx(`rect`,{x:`5`,y:`5`,width:`9`,height:`9`,rx:`1`,stroke:`currentColor`,strokeWidth:`1.2`}),jsx(`path`,{d:`M11 5V3C11 2.44772 10.5523 2 10 2H3C2.44772 2 2 2.44772 2 3V10C2 10.5523 2.44772 11 3 11H5`,stroke:`currentColor`,strokeWidth:`1.2`})]})
|
||||
// UTF-16 [48320,48579)
|
||||
export const ConnectorAddIcon=()=>jsx(`svg`,{className:`connector-connect-btn__icon-add`,viewBox:`0 0 16 16`,fill:`none`,"aria-hidden":`true`,children:jsx(`path`,{d:`M8.6 8.6V13H7.4V8.6H3V7.4H7.4V3H8.6V7.4H13V8.6H8.6Z`,fill:`currentColor`,fillOpacity:`1`,fillRule:`evenodd`})})
|
||||
// UTF-16 [48601,49163)
|
||||
export const ConnectorLoadingIcon=()=>jsx(`svg`,{className:`connector-connect-btn__icon-loading`,viewBox:`0 0 16 16`,fill:`none`,"aria-hidden":`true`,children:jsx(`path`,{d:`M8.6 1.5V5.5H7.4V1.5H8.6ZM10.4649 7.2697L13.9291 5.2697L13.329 4.2305L9.8649 6.2305L10.4649 7.2697ZM5.5351 7.2698L2.07097 5.2698L2.67097 4.2305L6.1351 6.2305L5.5351 7.2698ZM9.8649 9.7697L13.329 11.7697L13.9291 10.7305L10.4649 8.7305L9.8649 9.7697ZM6.1351 9.7697L2.67097 11.7697L2.07097 10.7305L5.5351 8.7305L6.1351 9.7697ZM8.6 10.5V14.5H7.4V10.5H8.6Z`,fill:`currentColor`,fillOpacity:`1`,fillRule:`evenodd`})})
|
||||
// UTF-16 [51019,51823)
|
||||
export const ConnectorCancelIcon=()=>jsx(`svg`,{className:`connector-connect-btn__icon-cancel`,viewBox:`0 0 16 16`,fill:`none`,"aria-hidden":`true`,children:jsx(`path`,{d:`M15 8Q15 5.1005 12.9497 3.0503Q10.8995 1 8 1Q5.1005 1 3.0503 3.0503Q1 5.1005 1 8Q1 10.8995 3.0503 12.9497Q5.1005 15 8 15Q9.2074 15 10.345 14.5955Q11.4826 14.1911 12.4189 13.4289L11.5685 12.5784Q10.0372 13.8 8 13.8Q5.5976 13.8 3.8988 12.1012Q2.2 10.4024 2.2 8Q2.2 5.5976 3.8988 3.8988Q5.5976 2.2 8 2.2Q10.4024 2.2 12.1012 3.8988Q13.8 5.5976 13.8 8Q13.8 10.052 12.5606 11.5907L13.4109 12.441Q14.1815 11.502 14.5908 10.3584Q15 9.2147 15 8ZM10.9243 10.0757L8.8485 8L10.9243 5.9243L10.0757 5.0757L8 7.1515L5.9243 5.0757L5.0757 5.9243L7.1515 8L5.0757 10.0757L5.9243 10.9243L8 8.8485L10.0757 10.9243L10.9243 10.0757Z`,fill:`currentColor`,fillRule:`evenodd`})})
|
||||
// UTF-16 [57928,58455)
|
||||
export const RefreshIcon=()=>jsxs(`svg`,{className:`connector-connect-btn__icon-refresh`,width:`16`,height:`16`,viewBox:`0 0 16 16`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children:[jsx(`path`,{d:`M14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2C10.0503 2 11.8645 3.00442 12.9497 4.55`,stroke:`currentColor`,strokeWidth:`1.25`,strokeLinecap:`round`,strokeLinejoin:`round`}),jsx(`path`,{d:`M14 2V5.5H10.5`,stroke:`currentColor`,strokeWidth:`1.5`,strokeLinecap:`round`,strokeLinejoin:`round`})]})
|
||||
// UTF-16 [58466,58738)
|
||||
export const CloseIcon=()=>jsxs(`svg`,{width:`16`,height:`16`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[jsx(`line`,{x1:`18`,y1:`6`,x2:`6`,y2:`18`}),jsx(`line`,{x1:`6`,y1:`6`,x2:`18`,y2:`18`})]})
|
||||
// UTF-16 [60072,60865)
|
||||
export const ConnectorDetailTryIcon=()=>jsxs(`svg`,{viewBox:`0 0 14.4531 14.6504`,fill:`none`,"aria-hidden":`true`,children:[jsx(`path`,{d:`M4.86 14.079 C5.72 12.849 6.52 11.959 7.53 11.959 C9.58 11.959 11.34 11.709 12.47 10.569 C13.65 9.389 13.85 7.839 13.85 6.279 C13.85 4.719 13.65 3.169 12.47 1.989 C11.27 0.799 9.39 0.599 7.23 0.599 C5.07 0.599 3.18 0.799 1.98 1.989 C0.80 3.169 0.60 4.719 0.60 6.279 C0.60 7.839 0.80 9.389 1.98 10.569 C2.65 11.239 3.63 11.669 4.73 11.839`,stroke:`currentColor`,strokeWidth:`1.05`,strokeLinecap:`butt`,strokeLinejoin:`round`}),jsx(`path`,{d:`M4.603 4.72V7.72`,stroke:`currentColor`,strokeWidth:`1.05`,strokeLinecap:`butt`}),jsx(`path`,{d:`M10.727 5.021L8.13 6.22L10.727 7.421`,stroke:`currentColor`,strokeWidth:`1.05`,strokeLinecap:`butt`,strokeLinejoin:`round`})]})
|
||||
// UTF-16 [61255,61918)
|
||||
export const ConnectorDetailUnbindIcon=()=>jsx(`svg`,{viewBox:`0 0 15.8208 14.7305`,fill:`none`,"aria-hidden":`true`,children:jsxs(`g`,{transform:`matrix(0.866025 0.5 -0.5 0.866025 4.57577 -3.12676)`,stroke:`currentColor`,strokeWidth:`1.25`,strokeLinecap:`butt`,strokeLinejoin:`round`,children:[jsx(`path`,{d:`M5.3942 5.895V3.26 Q5.3942 0.6 8.0542 0.6 Q10.7142 0.6 10.7142 3.26 V5.895`}),jsx(`path`,{d:`M5.3942 9.035V11.67 Q5.3942 14.33 8.0542 14.33 Q10.7142 14.33 10.7142 11.67 V9.035`}),jsx(`path`,{d:`M1.4273 3.4069L3.6331 5.6127 M14.7724 3.4069L12.5666 5.6127 M0 7.4647H3.5658 M12.6338 7.4647H16.1996 M1.6368 11.651L3.7945 9.3982 M14.5628 11.651L12.4051 9.3982`})]})})
|
||||
// UTF-16 [62475,62930)
|
||||
export const ConnectorEmptyIcon=({width:e=48,height:t=48})=>jsxs(`svg`,{width:e,height:t,viewBox:`0 0 48 48`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children:[jsx(`rect`,{x:`8`,y:`8`,width:`32`,height:`32`,rx:`8`,stroke:`currentColor`,strokeWidth:`2`,strokeDasharray:`4 4`}),jsx(`path`,{d:`M20 20L28 28M28 20L20 28`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`}),jsx(`circle`,{cx:`24`,cy:`24`,r:`8`,stroke:`currentColor`,strokeWidth:`2`})]})
|
||||
export const SOURCE_BUDDY_ICON="data:image/svg+xml,%3csvg%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cg%20clip-path='url(%23clip0_100_1124)'%3e%3crect%20width='24'%20height='24'%20rx='5.14286'%20fill='url(%23paint0_linear_100_1124)'/%3e%3cg%20filter='url(%23filter0_f_100_1124)'%3e%3ccircle%20cx='18.3915'%20cy='24.4723'%20r='7.30463'%20fill='%23FFE355'%20fill-opacity='0.49'/%3e%3c/g%3e%3cpath%20d='M16.8325%201.49185C17.0548%201.29241%2017.0679%201.2839%2017.2311%201.27411C17.4955%201.25478%2017.7382%201.38174%2018.1504%201.75707C19.1137%202.63236%2020.4543%204.4325%2021.2883%205.96929L21.6111%206.5655L22.0665%206.79192C22.5058%207.01393%2023.2257%207.46951%2023.5267%207.71363C23.6628%207.82617%2023.6824%207.82826%2023.8239%207.77323C24.4626%207.52448%2025.3778%207.85405%2026.1849%208.62819C26.9115%209.32456%2027.608%2010.5149%2027.8746%2011.508C27.9135%2011.6679%2027.9648%2012.0114%2027.9836%2012.2671C28.0444%2013.1656%2027.7559%2013.8837%2027.201%2014.2085C27.088%2014.2738%2027.0807%2014.2915%2027.0838%2014.5739C27.1094%2015.9195%2026.746%2017.2627%2026.0174%2018.5725C25.1949%2020.0431%2023.7309%2021.5646%2021.749%2022.9978C20.6847%2023.7724%2018.1656%2025.2395%2017.027%2025.7546C14.2996%2026.9824%2012.1128%2027.4537%2010.2136%2027.221C9.08082%2027.0837%207.7985%2026.6415%207.03988%2026.1283C6.84042%2025.9904%206.80877%2025.9813%206.65653%2026.0248C5.8443%2026.2581%204.78008%2025.779%203.87639%2024.7759C3.51596%2024.3748%202.93382%2023.3904%202.74525%2022.8636C2.30913%2021.6307%202.39666%2020.5184%202.97762%2019.8539C3.12772%2019.6828%203.13247%2019.6752%203.0997%2019.3877C3.04555%2018.9166%203.02112%2018.219%203.04586%2017.769L3.06482%2017.3487L2.43386%2016.2324C1.45682%2014.4939%200.836658%2013.0338%200.597232%2011.9185C0.470862%2011.307%200.478498%2011.036%200.633714%2010.8352C0.728206%2010.7139%201.03828%2010.588%201.41189%2010.519C2.35259%2010.354%204.40363%2010.504%206.68539%2010.9068L6.92189%2010.9473L7.44287%2010.4864C8.30763%209.72042%208.88266%209.29135%209.94174%208.63098C11.0456%207.94031%2012.2916%207.37199%2013.6946%206.9221L14.1444%206.77741L14.392%206.12815C15.278%203.78949%2016.1856%202.0654%2016.8325%201.49185ZM9.41133%2013.476C8.41006%2014.054%207.90943%2014.3432%207.54156%2014.6671C6.05186%2015.9789%205.49506%2018.0569%206.12928%2019.9378C6.28591%2020.4021%206.57475%2020.9027%207.15249%2021.9034C7.73039%2022.9043%208.01983%2023.4053%208.34364%2023.7732C9.65542%2025.2628%2011.7334%2025.8196%2013.6143%2025.1854C14.0788%2025.0288%2014.5795%2024.7398%2015.5808%2024.1617L21.3402%2020.8365C22.3412%2020.2586%2022.8421%2019.9692%2023.2099%2019.6454C24.6996%2018.3336%2025.2564%2016.2556%2024.6222%2014.3747C24.4656%2013.9103%2024.1764%2013.4093%2023.5985%2012.4083C23.0207%2011.4075%2022.7316%2010.9071%2022.4079%2010.5393C21.0961%209.04964%2019.0181%208.49284%2017.1372%209.12707C16.6728%209.28371%2016.1717%209.57283%2015.1707%2010.1508L9.41133%2013.476Z'%20fill='url(%23paint1_linear_100_1124)'/%3e%3crect%20x='9.9668'%20y='17.5005'%20width='2.27533'%20height='4.72569'%20rx='1.13767'%20transform='rotate(-30%209.9668%2017.5005)'%20fill='white'/%3e%3crect%20x='16.1055'%20y='13.9562'%20width='2.27533'%20height='4.72569'%20rx='1.13767'%20transform='rotate(-30%2016.1055%2013.9562)'%20fill='white'/%3e%3c/g%3e%3cdefs%3e%3cfilter%20id='filter0_f_100_1124'%20x='4.84364'%20y='10.9244'%20width='27.0959'%20height='27.0958'%20filterUnits='userSpaceOnUse'%20color-interpolation-filters='sRGB'%3e%3cfeFlood%20flood-opacity='0'%20result='BackgroundImageFix'/%3e%3cfeBlend%20mode='normal'%20in='SourceGraphic'%20in2='BackgroundImageFix'%20result='shape'/%3e%3cfeGaussianBlur%20stdDeviation='3.12164'%20result='effect1_foregroundBlur_100_1124'/%3e%3c/filter%3e%3clinearGradient%20id='paint0_linear_100_1124'%20x1='12'%20y1='0'%20x2='12'%20y2='24'%20gradientUnits='userSpaceOnUse'%3e%3cstop%20stop-color='%230EC8A9'/%3e%3cstop%20offset='1'%20stop-color='%2301C886'/%3e%3c/linearGradient%3e%3clinearGradient%20id='paint1_linear_100_1124'%20x1='8.92652'%20y1='6.00417'%20x2='19.5466'%20y2='24.3986'%20gradientUnits='userSpaceOnUse'%3e%3cstop%20stop-color='white'%20stop-opacity='0.8'/%3e%3cstop%20offset='0.437689'%20stop-color='white'/%3e%3c/linearGradient%3e%3cclipPath%20id='clip0_100_1124'%3e%3crect%20width='24'%20height='24'%20rx='5.14286'%20fill='white'/%3e%3c/clipPath%3e%3c/defs%3e%3c/svg%3e"
|
||||
|
||||
/* REA ev_e91bdc6e46275f4460985cfff25fd7f7bc334afc45f49fac1935de1249f4cb22, node821 */
|
||||
export const ConnectorEditIcon=()=>jsx("svg",{"viewBox":"0 0 16 16","fill":"none","width":"16","height":"16","className":"wb-icon",children:jsx("path",{"fill":"currentColor","fillRule":"evenodd","transform":"matrix(1 0 0 1 0.999868 1.49989)","d":"M0.3011 12.4122Q0.6022 12.7102 1.0178 12.6286L1.9012 12.4553Q4.4733 11.9508 6.3267 10.0973L11.9021 4.5219Q12.678 3.7461 12.678 2.6489Q12.678 1.5517 11.9021 0.7758Q11.1263 0 10.0291 0Q8.9319 0 8.1561 0.7758L2.6125 6.3194Q0.7248 8.2071 0.2379 10.8318L0.0773 11.6977Q0 12.1142 0.3011 12.4122ZM11.0536 3.6734L10.7156 4.0114L8.6671 1.9618L9.0046 1.6244Q9.429 1.2 10.0291 1.2Q10.6292 1.2 11.0536 1.6244Q11.478 2.0487 11.478 2.6489Q11.478 3.249 11.0536 3.6734ZM1.3645 11.3378L1.6702 11.2778Q3.8834 10.8436 5.4782 9.2488L9.8671 4.86L7.8186 2.8103L3.461 7.1679Q1.8368 8.7922 1.4178 11.0507L1.3645 11.3378ZM6.0001 11.3L13.0001 11.3L13.0001 12.5L6.0001 12.5L6.0001 11.3Z"})})
|
||||
/* REA ev_b89281177f6a1cf2e4697887d576b8ddf7d811a8fa2d1148b7024a4ec898906f, literal modal SVGs */
|
||||
export const McpAddIcon=()=>jsx("svg",{"viewBox":"0 0 16 16","fill":"none","width":"16","height":"16","className":"wb-icon",children:jsx("path",{"d":"M8 3V13M3 8H13","stroke":"currentColor","strokeWidth":"1.5","strokeLinecap":"round"})})
|
||||
export const McpHubIcon=()=>jsx("svg",{"viewBox":"0 0 16 16","fill":"currentColor","width":"16","height":"16","className":"wb-icon",children:jsx("path",{"transform":"matrix(1 0 0 1 2.57586 2.39996)","fillRule":"evenodd","d":"M9.8242 6.6001L9.8242 5.109Q9.8243 3.305 9.7462 2.7323Q9.7081 2.4537 9.6375 2.2354L0.8485 11.0243L0 10.1758L8.789 1.3868Q8.5706 1.3162 8.292 1.2782Q7.7193 1.2 5.9153 1.2001L4.4243 1.2001L4.4243 0L5.9152 0Q7.8008 -0 8.4543 0.0892Q9.5761 0.2423 10.179 0.8453Q10.782 1.4482 10.9351 2.57Q11.0243 3.2235 11.0243 5.1091L11.0243 6.6001L9.8242 6.6001Z"})})
|
||||
export const McpCloseIcon=()=>jsx("svg",{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"none",children:jsx("path",{"d":"M12 4L4 12M4 4L12 12","stroke":"currentColor","strokeWidth":"1.5","strokeLinecap":"round","strokeLinejoin":"round"})})
|
||||
export const McpSearchIcon=()=>jsx("svg",{"viewBox":"0 0 16 16","fill":"none",children:jsx("path",{"d":"M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z","fill":"currentColor"})})
|
||||
export const McpClearIcon=()=>jsx("svg",{"viewBox":"0 0 16 16","fill":"none",children:jsx("path",{"d":"M8 8.707l3.646 3.647.708-.707L8.707 8l3.647-3.646-.707-.708L8 7.293 4.354 3.646l-.707.708L7.293 8l-3.646 3.646.707.708L8 8.707z","fill":"currentColor"})})
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useCallback, useEffect, useRef, useState, type ComponentProps, type ComponentType } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import type { McpServerView } from '@shared/capabilities'
|
||||
import { CapabilityToolbar } from './CapabilityCenter'
|
||||
import { SearchIcon, ClearFilledIcon } from './SkillSourceIcons'
|
||||
import { ChevronIcon, CloseIcon, ConnectorAddIcon, ConnectorEmptyIcon, RefreshIcon, CopyIcon, ConnectorEditIcon, McpAddIcon, McpHubIcon, McpCloseIcon, McpSearchIcon, McpClearIcon, SOURCE_BUDDY_ICON } from './ConnectorSourceIcons'
|
||||
import { MonacoEditor, configureMonaco, themeManager } from '../workbuddy/workbench/source-code-assets/wrapper.mjs'
|
||||
import { mcpEditorDocument, parseMcpEditor, mcpIconColor } from './connectors-helpers'
|
||||
import '../workbuddy/workbench/sourceCode.css'
|
||||
import './connectors-source.css'
|
||||
import './connectors-adapters.css'
|
||||
|
||||
function Search({ value, onChange, placeholder, mcp = false }: { value: string; onChange(value: string): void; placeholder: string; mcp?: boolean }): React.JSX.Element {
|
||||
if (mcp) return <div className="cb-search-input cb-search-input--small"><div className="cb-search-input__icon"><McpSearchIcon /></div><input className="cb-search-input__input" value={value} onChange={event => onChange(event.target.value)} placeholder={placeholder} aria-label={placeholder} /><button className={'cb-search-input__clear' + (value ? ' cb-search-input__clear--visible' : '')} aria-label="清除搜索" onClick={() => onChange('')}><McpClearIcon /></button></div>
|
||||
return <span className="wb-input-wrapper wb-input-wrapper--medium yc-mcp-search"><span className="wb-input__prefix"><SearchIcon /></span><input className="wb-input__inner" value={value} onChange={event => onChange(event.target.value)} placeholder={placeholder} aria-label={placeholder} />{value && <button className="yc-mcp-search-clear" aria-label="清除搜索" onClick={() => onChange('')}><ClearFilledIcon /></button>}</span>
|
||||
}
|
||||
function SourceButton({ children, icon, onClick, disabled = false, primary = false }: { children: React.ReactNode; icon?: React.ReactNode; onClick(): void; disabled?: boolean; primary?: boolean }): React.JSX.Element {
|
||||
return <button type="button" className={`wb-button wb-button--${primary ? 'primary' : 'secondary'} wb-button--medium`} onClick={onClick} disabled={disabled}>{icon && <span className="wb-button__icon wb-button__icon--left">{icon}</span>}<span className="wb-button__content">{children}</span></button>
|
||||
}
|
||||
function JsonEditor({ content, setContent }: { content: string; setContent(value: string): void }): React.JSX.Element {
|
||||
const [ready, setReady] = useState(false), [failed, setFailed] = useState(false)
|
||||
useEffect(() => { let active = true; void configureMonaco().then(() => { if (active) { themeManager().setTheme(document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light'); setReady(true) } }).catch(() => { if (active) setFailed(true) }); return () => { active = false } }, [])
|
||||
// The recovered source wrapper already provides onChange; the preview-only declaration is narrower.
|
||||
const EditableMonaco = MonacoEditor as ComponentType<ComponentProps<typeof MonacoEditor> & { onChange(value: string | undefined): void }>
|
||||
return failed ? <textarea className="mcp-config-textarea" aria-label="MCP JSON 配置" spellCheck={false} value={content} onChange={event => setContent(event.target.value)} /> : !ready ? <div className="mcp-config-editor-loading">加载编辑器…</div> : <EditableMonaco value={content} onChange={value => setContent(value || '')} language="json" readOnly={false} minimap={false} lineNumbers="on" wordWrap="on" fontSize={13} height="100%" onMount={editor => {
|
||||
editor.updateOptions({ readOnly: false, automaticLayout: true, scrollBeyondLastLine: false, padding: { top: 8, bottom: 8 } })
|
||||
}} />
|
||||
}
|
||||
function ServerEditor({ server, onSaved, onBack }: { server?: McpServerView; onSaved(): Promise<void>; onBack(): void }): React.JSX.Element {
|
||||
const initial = useRef(mcpEditorDocument(server)), [content, setContent] = useState(initial.current), [saving, setSaving] = useState(false), [error, setError] = useState('')
|
||||
const dirty = content !== initial.current
|
||||
const back = (): void => { if (!saving && (!dirty || window.confirm('配置尚未保存,确定返回吗?'))) onBack() }
|
||||
const save = async (): Promise<void> => {
|
||||
if (saving) return
|
||||
setError('')
|
||||
let input
|
||||
try { input = parseMcpEditor(content, server) } catch (reason) { setError((reason as Error).message); return }
|
||||
setSaving(true)
|
||||
try { await window.ycode.mcp.save(input); await onSaved(); onBack() }
|
||||
catch (reason) { setError((reason as Error).message || '保存配置失败') }
|
||||
finally { setSaving(false) }
|
||||
}
|
||||
return <div className="mcp-config-editor"><div className="mcp-config-editor-header"><button className="mcp-config-editor-back" onClick={back} disabled={saving}>‹ 返回列表</button><div className="mcp-config-editor-header-actions"><SourceButton onClick={back} disabled={saving}>取消</SourceButton><SourceButton onClick={() => void save()} primary disabled={saving || !dirty}>{saving ? '保存中…' : '保存'}</SourceButton></div></div>
|
||||
<div className="mcp-config-editor-path"><span className="mcp-config-editor-path-label">{server ? server.name : '添加 MCP'}</span>{dirty && <span className="mcp-config-editor-unsaved">未保存</span>}</div>
|
||||
<p className="yc-mcp-editor-note">每次配置一个 MCP 服务。已有环境变量和 Headers 留空保持不变;保存后需批准变更的配置。</p>{error && <div className="yc-mcp-error" role="alert">{error}</div>}
|
||||
<div className="mcp-config-editor-body"><div className="mcp-config-editor-monaco-wrapper"><JsonEditor content={content} setContent={setContent} /></div></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default function ConnectorsPage(): React.JSX.Element {
|
||||
const [servers, setServers] = useState<McpServerView[]>([]), [query, setQuery] = useState(''), [listQuery, setListQuery] = useState('')
|
||||
const [loading, setLoading] = useState(true), [error, setError] = useState(''), [modal, setModal] = useState(false), [warningDismissed, setWarningDismissed] = useState(false)
|
||||
const [editor, setEditor] = useState<{ server?: McpServerView } | null>(null), [detail, setDetail] = useState<string | null>(null), [confirmRemove, setConfirmRemove] = useState<McpServerView | null>(null)
|
||||
const [expanded, setExpanded] = useState<Set<string>>(new Set()), [busy, setBusy] = useState<Set<string>>(new Set())
|
||||
const busyRef = useRef(new Set<string>()), alive = useRef(true), opener = useRef<HTMLButtonElement>(null), refreshGeneration = useRef(0)
|
||||
const refresh = useCallback(async (): Promise<void> => {
|
||||
const generation = ++refreshGeneration.current
|
||||
const items = await window.ycode.mcp.list()
|
||||
if (alive.current && generation === refreshGeneration.current) setServers(items)
|
||||
}, [])
|
||||
useEffect(() => { alive.current = true; void refresh().catch(reason => setError(reason.message || '加载 MCP 配置失败')).finally(() => setLoading(false)); return () => { alive.current = false } }, [refresh])
|
||||
useEffect(() => { if (!servers.some(server => server.status === 'connecting')) return; const timer = setInterval(() => { void refresh().catch(() => undefined) }, 3000); return () => clearInterval(timer) }, [servers, refresh])
|
||||
const action = async (id: string, operation: () => Promise<unknown>): Promise<void> => {
|
||||
if (busyRef.current.has(id)) return
|
||||
busyRef.current.add(id); setBusy(new Set(busyRef.current)); setError('')
|
||||
try { await operation() } catch (reason) { if (alive.current) setError((reason as Error).message || 'MCP 操作失败') }
|
||||
finally { try { await refresh() } catch { if (alive.current) setError('刷新 MCP 配置失败') }; busyRef.current.delete(id); if (alive.current) setBusy(new Set(busyRef.current)) }
|
||||
}
|
||||
const close = (): void => { if (editor) { if (window.confirm('离开配置编辑器?未保存的修改将丢失。')) setEditor(null); return }; setModal(false); requestAnimationFrame(() => opener.current?.focus()) }
|
||||
const color = (server: McpServerView): string => !server.enabled ? '#8c8c8c' : server.status === 'connected' ? '#52c41a' : server.status === 'connecting' ? '#faad14' : server.status === 'error' ? '#ff6b6b' : '#8c8c8c'
|
||||
const status = (server: McpServerView): string => !server.enabled ? '已禁用' : ({ connected: '已连接', connecting: '连接中', disconnected: '未连接', error: '连接失败' })[server.status]
|
||||
const icon = (server: McpServerView): React.JSX.Element => <span style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', background: mcpIconColor(server.name) }}>{server.name.charAt(0).toUpperCase()}</span>
|
||||
const matches = (server: McpServerView, needle: string): boolean => server.name.toLowerCase().includes(needle.trim().toLowerCase())
|
||||
const visible = servers.filter(server => matches(server, query)), listed = servers.filter(server => matches(server, listQuery)), selected = servers.find(server => server.id === detail)
|
||||
const enabledTools = servers.filter(server => server.enabled).reduce((sum, server) => sum + server.tools.filter(tool => tool.enabled).length, 0)
|
||||
const approve = (server: McpServerView): Promise<void> => action(server.id, async () => { await window.ycode.mcp.trust(server.id, server.configHash); await window.ycode.mcp.setEnabled(server.id, true) })
|
||||
const portalHost = document.querySelector('.yc-capability-center .expert-center-page') ?? document.body
|
||||
return <div className="ycode-connectors connectors-page"><div className="connector-panel">
|
||||
<CapabilityToolbar><div className="ycode-connectors connectors-toolbar"><div className="ec-search-wrapper"><Search value={query} onChange={setQuery} placeholder="搜索连接器" /></div><button ref={opener} className="wb-button wb-button--primary wb-button--medium connector-topbar-action-btn" onClick={() => { setModal(true); void refresh().catch(() => undefined) }}><span className="wb-button__icon wb-button__icon--left"><ConnectorEditIcon /></span><span className="wb-button__content">自定义连接器</span></button></div></CapabilityToolbar>
|
||||
<div className="connector-panel-content">{error && <div className="yc-mcp-error" role="alert">{error}</div>}{loading ? <div className="connector-panel-empty">加载中…</div> : !visible.length ? <div className="connector-panel-empty"><ConnectorEmptyIcon /><p className="connector-panel-empty-title">{query ? '未找到连接器' : '暂无连接器'}</p><p className="connector-panel-empty-desc">{query ? '试试其他关键词' : '连接器目录尚未接入,可添加并管理自定义 MCP 服务。'}</p></div> : <div className="connector-grid">{visible.map(server => <div key={server.id} className="connector-card connector-card--installed" role="button" tabIndex={0} onClick={() => setDetail(server.id)} onKeyDown={event => { if (event.key === 'Enter') setDetail(server.id) }}><div className="connector-card-content"><div className="connector-card-icon">{icon(server)}</div><div className="connector-card-main"><div className="connector-card-name-row"><div className="connector-card-name">{server.name}</div><span className={`connector-card-status-dot connector-card-status-dot--${server.status}`} title={status(server)} /></div><div className="connector-card-desc">{server.tools.length ? `${server.tools.filter(tool => tool.enabled).length}/${server.tools.length} 个工具已启用` : '自定义 MCP 服务'}</div></div><div className="connector-card-action" onClick={event => event.stopPropagation()}><button className="connector-connect-btn" disabled={busy.has(server.id)} aria-label={`管理 ${server.name}`} onClick={() => { setModal(true); setExpanded(new Set([server.id])) }}><ConnectorAddIcon /></button></div></div></div>)}</div>}</div>
|
||||
</div>
|
||||
{modal && createPortal(<div className="ycode-connectors"><div className="mcp-modal-mask"><section className="mcp-modal" role="dialog" aria-modal="true" aria-label="MCP 服务管理"><header className="mcp-modal-header"><div className="mcp-modal-header-left"><div className="mcp-modal-header-text"><h2 className="mcp-modal-title">MCP 服务管理</h2><p className="mcp-modal-subtitle">安装 MCP 服务,为 AI 扩展更多工具能力</p></div></div><div className="mcp-modal-header-right"><SourceButton icon={<McpAddIcon />} onClick={() => setEditor({})} disabled={!!editor}>添加 MCP</SourceButton><button className="mcp-modal-close" aria-label="关闭 MCP 服务管理" onClick={close}><McpCloseIcon /></button></div></header><div className="mcp-modal-body">{editor ? <ServerEditor key={editor.server?.id || 'new'} server={editor.server} onSaved={refresh} onBack={() => setEditor(null)} /> : <div className="mcp-panel">
|
||||
<div className="mcp-panel-search" style={{ display: 'flex', alignItems: 'center', gap: 8 }}><div style={{ flex: 1 }}><Search mcp value={listQuery} onChange={setListQuery} placeholder="搜索MCP" /></div><SourceButton icon={<McpHubIcon />} onClick={() => void window.ycode.resources.openExternal('https://cloud.tencent.com/developer/mcp').catch(() => setError('无法打开 MCP Hub'))}>MCP Hub</SourceButton></div>
|
||||
{servers.length > 0 && <div className="mcp-panel-section-header"><h3 className="mcp-panel-section-title">我的 MCP<span className="mcp-installed-badge">({listed.length})</span></h3><span className="mcp-panel-status-summary">{servers.filter(server => server.enabled).length} 启用{servers.some(server => server.status === 'error') && <span className="status-count-error"> · {servers.filter(server => server.status === 'error').length} 失败</span>}</span></div>}
|
||||
{enabledTools > 80 && !warningDismissed && <div className="mcp-tools-warning"><div className="mcp-tools-warning-content"><span className="mcp-tools-warning-icon">⚠</span><div className="mcp-tools-warning-text"><strong>启用工具数量过多</strong><p>启用超过 80 个工具可能影响 AI 回复质量,建议禁用不常用的工具。</p></div></div><button className="mcp-tools-warning-dismiss" onClick={() => setWarningDismissed(true)}>知道了</button></div>}
|
||||
{error && <div className="yc-mcp-error" role="alert">{error}</div>}
|
||||
<div className="mcp-panel-content">{!listed.length ? <div className="mcp-panel-empty"><ConnectorEmptyIcon /><p className="mcp-panel-empty-title">{listQuery ? '未找到相关 MCP' : '暂无 MCP 服务'}</p><p className="mcp-panel-empty-desc">{listQuery ? '试试其他关键词' : '添加 MCP 服务,为 AI 扩展更多工具能力'}</p>{!listQuery && <SourceButton onClick={() => setEditor({})}>添加 MCP</SourceButton>}</div> : <div className="mcp-panel-list">{listed.map(server => {
|
||||
const open = expanded.has(server.id), working = busy.has(server.id)
|
||||
return <div className={'mcp-item' + (server.status === 'error' ? ' mcp-item--error' : '')} key={server.id}><div className="mcp-item-header" onClick={() => setExpanded(previous => { const next = new Set(previous); if (next.has(server.id)) next.delete(server.id); else next.add(server.id); return next })}><ChevronIcon expanded={open} /><div className="mcp-item-icon" style={{ background: mcpIconColor(server.name) }}>{server.name.charAt(0).toUpperCase()}</div><div className="mcp-item-info"><div className="mcp-item-name">{server.name}<span className={'mcp-item-status' + (server.status === 'connecting' ? ' mcp-item-status--connecting' : '')} style={{ backgroundColor: color(server) }} title={status(server)} /></div><div className="mcp-item-meta">{server.tools.length ? `${server.tools.filter(tool => tool.enabled).length}/${server.tools.length} 个工具已启用` : '工具'}</div></div><div className="mcp-item-actions" onClick={event => event.stopPropagation()}>{open && <><button className="mcp-item-action" title="编辑配置" aria-label={`编辑 ${server.name}`} onClick={() => setEditor({ server })} disabled={working}>编辑</button>{server.enabled && server.trusted && <button className="mcp-item-action" title="重新连接" aria-label={`重新连接 ${server.name}`} disabled={working || server.status === 'connecting'} onClick={() => void action(server.id, async () => { await window.ycode.mcp.disconnect(server.id); await window.ycode.mcp.connect(server.id) })}><RefreshIcon /></button>}<button className="mcp-item-action" title="删除" aria-label={`删除 ${server.name}`} onClick={() => setConfirmRemove(server)} disabled={working}>删除</button></>}<span className="mcp-toggle-wrap"><button className={'mcp-toggle' + (server.enabled && server.trusted ? ' mcp-toggle--checked' : '') + (!server.trusted || working ? ' mcp-toggle--disabled' : '')} type="button" role="switch" aria-label={`${server.name} 启用`} aria-checked={server.enabled && server.trusted} disabled={!server.trusted || working} onClick={() => void action(server.id, () => window.ycode.mcp.setEnabled(server.id, !server.enabled))}><span className="mcp-toggle-thumb" /></button></span></div></div>
|
||||
{!server.trusted && <div className="mcp-item-approval"><div className="mcp-item-approval-text">此 MCP 配置尚未批准</div><div className="mcp-item-approval-actions"><button className="mcp-item-approval-btn mcp-item-approval-btn--approve" disabled={working} onClick={() => void approve(server)}>批准配置</button></div></div>}
|
||||
{server.error && <div className="mcp-item-error"><div className="mcp-item-error-content">{server.error}</div><div className="mcp-item-error-actions"><button className="mcp-item-error-link" disabled={working || !server.trusted} onClick={() => void action(server.id, () => window.ycode.mcp.connect(server.id))}><RefreshIcon />重新连接</button><button className="mcp-item-error-link" onClick={() => void navigator.clipboard.writeText(server.error!).catch(() => setError('复制失败'))}><CopyIcon />复制日志</button></div></div>}
|
||||
{open && server.tools.length > 0 && <div className="mcp-item-tools">{server.tools.map(tool => <span className="mcp-item-tool-wrap" key={tool.name} title={tool.description}><button className={'mcp-item-tool' + (!tool.enabled ? ' mcp-item-tool--disabled' : '')} disabled={working || !server.enabled || !server.trusted} onClick={() => void action(server.id, () => window.ycode.mcp.setToolEnabled(server.id, tool.name, !tool.enabled))}>{tool.name}</button></span>)}</div>}
|
||||
</div>
|
||||
})}</div>}</div>
|
||||
</div>}</div></section></div>{confirmRemove && <div className="yc-mcp-confirm-mask"><section className="yc-mcp-confirm" role="alertdialog" aria-modal="true" aria-label="删除 MCP"><h3>删除 MCP 服务</h3><p>确定删除“{confirmRemove.name}”吗?</p><div><SourceButton onClick={() => setConfirmRemove(null)} disabled={busy.has(confirmRemove.id)}>取消</SourceButton><SourceButton primary disabled={busy.has(confirmRemove.id)} onClick={() => void action(confirmRemove.id, async () => { await window.ycode.mcp.remove(confirmRemove.id); setConfirmRemove(null) })}>删除</SourceButton></div></section></div>}</div>, portalHost)}
|
||||
{selected && createPortal(<div className="ycode-connectors"><div className="connector-detail-overlay connector-detail-modal-mask" onClick={event => { if (event.target === event.currentTarget) setDetail(null) }}><section className="connector-detail-modal connector-detail-modal--connected" role="dialog" aria-modal="true" aria-label={selected.name}><button className="connector-detail-close" type="button" aria-label="关闭详情" onClick={() => setDetail(null)}><CloseIcon /></button><div className="connector-detail-modal-body"><div className="connector-detail-flow"><div className="connector-detail-flow-avatar connector-detail-flow-avatar--workbuddy"><img src={SOURCE_BUDDY_ICON} alt="" draggable={false} /></div><span className="connector-detail-flow-icon"><span /><span /><span /></span><div className="connector-detail-flow-avatar connector-detail-flow-avatar--connector">{icon(selected)}</div></div><h3 className="connector-detail-name">{selected.name}</h3><p className="connector-detail-desc">{status(selected)} · {selected.tools.length} 个工具</p><div className="connector-detail-actions"><button className="connector-detail-btn connector-detail-btn--try" onClick={() => { setDetail(null); setModal(true); setExpanded(new Set([selected.id])) }}>管理 MCP</button></div>{selected.tools.length > 0 && <div className="connector-detail-expand"><div className="connector-detail-tools">{selected.tools.map(tool => <div className="connector-detail-tool-item" key={tool.name}><span className="connector-detail-tool-name">{tool.name}</span><span className="connector-detail-tool-desc">{tool.description}</span></div>)}</div></div>}</div></section></div></div>, portalHost)}
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { useCallback, useEffect, useRef, useState, type InputHTMLAttributes } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { CapabilitySubpageHeader, CapabilityToolbar } from './CapabilityCenter'
|
||||
import { AddIcon, ExpertCard, ExpertIcon, ExpertListEmptyState, ExpertSearchBar, MyExpertIcon, filterExpertsByKeyword, setExpertSearchPrimitive } from './assets/source-ui.mjs'
|
||||
import classes from './assets/source-classes.json'
|
||||
import { IconClose, IconSearchTopbar } from '../components/icons'
|
||||
import './expert-source.css'
|
||||
import { CreatedExpertsPanel, CreatedExpertSourceProvider } from './assets/created-expert-source.mjs'
|
||||
import './created-expert-source.css'
|
||||
import { expertText, type ExpertRecord } from '@shared/capabilities'
|
||||
|
||||
/** Transcribed only from the visible REA screenshot. These are references, never installed expert IDs. */
|
||||
const REFERENCE_EVIDENCE = 'ev_20de2f04c99b1c27bc39b61f5e2f5a430c3c7029b09afb76b363652eb4fadc89'
|
||||
interface ReferenceExpert extends Record<string, unknown> {
|
||||
id: string; name: { zh: string }; profession: { zh: string }; description: { zh: string }; tags: string[]; expertType: 'agent'
|
||||
}
|
||||
const references: ReferenceExpert[] = [
|
||||
['高校创业导师备课易', '面向高校创业教师与项目导师,…', ['创业教学', '项目辅导', '可编辑交付'], 12.72],
|
||||
['高级开发工程师', '10年以上全栈经验,精通多种语…', ['全栈开发', '架构设计', '代码质量'], 934.08],
|
||||
['升学备考通', '查询高考数据并指导志愿填报,…', ['高考真题', '一分一段', '志愿填报'], 13.43],
|
||||
['公益专家', '腾讯公益助手。找项目、做捐赠…', ['腾讯公益', '小红花花园', '项目捐赠'], 58.45],
|
||||
['自适应成果交付专家', '作为一个靠谱的AI合伙人,我的…', ['成果交付', '经营决策', '研究创作'], 25.63],
|
||||
['资讯速递专家', '一句话查到每天精选的 AI 模型/…', ['AI 资讯', '每日简报', 'AI 行业动态'], 13.75],
|
||||
['《和平精英》吉事通', '《和平精英》游戏知识助手,解…', ['游戏百科', '枪械装备', '玩法战术'], 1.08],
|
||||
['全栈开发专家', '全栈开发专家,精通前后端架构…', ['全栈开发', '后端架构', '接口集成'], 89.18],
|
||||
['创业伙伴', '林老师分身+读书伙伴。送《创业…', ['创业判断', 'GTM落地', '客户心声'], 8.31]
|
||||
].map(([title, description, tags, countInTenThousands], index) => ({
|
||||
id: `rea-reference-${index + 1}`, name: { zh: title as string }, profession: { zh: title as string },
|
||||
description: { zh: description as string }, tags: tags as string[], expertType: 'agent',
|
||||
// Reconstructs the visible formatted number only; this is not a live or exact backend count.
|
||||
useCount: (countInTenThousands as number) * 10000
|
||||
}))
|
||||
const categories = ['全部', 'OPC·一人公司', '腾讯专家', '高校新生攻略', '产品设计', '技术工程', '金融投资', '全球发展', '教育学习', '游戏空间', '数据智能']
|
||||
|
||||
interface SearchProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
|
||||
value: string; onChange(event: React.ChangeEvent<HTMLInputElement>): void; allowClear?: boolean; clearAriaLabel?: string; variant?: string
|
||||
}
|
||||
function SearchPrimitive({ allowClear: _clear, clearAriaLabel, variant: _variant, ...props }: SearchProps): React.JSX.Element {
|
||||
const input = useRef<HTMLInputElement>(null)
|
||||
return <span className="wb-input-wrapper wb-input-wrapper--medium">
|
||||
<span className="wb-input__prefix"><IconSearchTopbar className="wb-icon" /></span>
|
||||
<input {...props} ref={input} className="wb-input__inner" aria-label={props.placeholder ?? '搜索专家'} />
|
||||
<button type="button" className={`wb-input__icon-button wb-input__clear${props.value ? '' : ' wb-input__clear--hidden'}`}
|
||||
aria-label={clearAriaLabel ?? '清除搜索'} tabIndex={props.value ? 0 : -1}
|
||||
onClick={() => { if (input.current) { input.current.value = ''; props.onChange({ target: input.current } as React.ChangeEvent<HTMLInputElement>); input.current.focus() } }}>
|
||||
<IconClose className="wb-icon" />
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
setExpertSearchPrimitive(SearchPrimitive)
|
||||
|
||||
function LocalExpertDetail({ id, onClose, onUse }: { id: string; onClose(): void; onUse(id: string): void }): React.JSX.Element {
|
||||
const [packageDocument, setDocument] = useState<Awaited<ReturnType<typeof window.ycode.experts.read>> | null>(null), [error, setError] = useState('')
|
||||
const close = useRef<HTMLButtonElement>(null), c = classes._L
|
||||
useEffect(() => { let alive = true; void window.ycode.experts.read(id).then(value => { if (alive) setDocument(value) }).catch(error => { if (alive) setError(String(error)) }); return () => { alive = false } }, [id])
|
||||
useEffect(() => {
|
||||
const previous = document.activeElement as HTMLElement | null; close.current?.focus()
|
||||
const keydown = (event: KeyboardEvent): void => {
|
||||
if (event.key === 'Escape' && !event.isComposing) { event.preventDefault(); event.stopPropagation(); onClose() }
|
||||
if (event.key === 'Tab') { const buttons = [...(close.current?.closest('section')?.querySelectorAll<HTMLButtonElement>('button:not(:disabled)') ?? [])]; if (event.shiftKey && document.activeElement === buttons[0]) { event.preventDefault(); buttons.at(-1)?.focus() } else if (!event.shiftKey && document.activeElement === buttons.at(-1)) { event.preventDefault(); buttons[0]?.focus() } }
|
||||
}
|
||||
globalThis.document.addEventListener('keydown', keydown, true)
|
||||
return () => { globalThis.document.removeEventListener('keydown', keydown, true); previous?.focus() }
|
||||
}, [onClose])
|
||||
const expert = packageDocument?.expert
|
||||
return <div className={classes.NR.overlay} onMouseDown={event => { if (event.target === event.currentTarget) onClose() }}><section className={`${classes.NR.card} ${classes.NR.expertView}`} role="dialog" aria-modal="true" aria-label="专家详情">
|
||||
<div className={c.top}><div className={`${c.avatar} ${c.avatarFallback}`}>{expert?.avatar ? <img src={expert.avatar} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : <ExpertIcon width={32} height={32} />}</div><div className={c.info}><div className={c.titleRow}><div className={c.role}><span className={c.rolePrimary}>{expertText(expert?.profession) || expertText(expert?.displayName) || id}</span></div><div className={c.actions}><button ref={close} className="capability-icon-button" onClick={onClose} aria-label="关闭专家详情"><IconClose /></button></div></div><button className={c.headerSummonBtn} disabled={!expert || !!expert.error || expert.expertType === 'team'} onClick={() => onUse(id)}>召唤</button></div></div>
|
||||
<div className={c.scrollShell}><div className={c.body}><div className={c.content}><p className={c.desc}>{expertText(expert?.displayDescription) || expert?.description}</p><div className={c.tagsList}>{expert?.tags?.map((tag, index) => <span className={c.tagChip} key={index}>{expertText(tag)}</span>)}</div><pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'inherit', fontSize: 14 }}>{packageDocument?.prompt}</pre></div>{(error || expert?.error) && <p role="alert">{error || expert?.error}</p>}</div></div>
|
||||
</section></div>
|
||||
}
|
||||
|
||||
function ExpertReferenceDetail({ expert, onClose }: { expert: ReferenceExpert; onClose(): void }): React.JSX.Element {
|
||||
const c = classes._L
|
||||
const close = useRef<HTMLButtonElement>(null)
|
||||
useEffect(() => {
|
||||
const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null
|
||||
close.current?.focus()
|
||||
const escape = (event: KeyboardEvent): void => {
|
||||
if (event.key === 'Escape' && !event.isComposing) { event.preventDefault(); event.stopPropagation(); onClose() }
|
||||
if (event.key === 'Tab') { event.preventDefault(); close.current?.focus() }
|
||||
}
|
||||
document.addEventListener('keydown', escape, true)
|
||||
return () => { document.removeEventListener('keydown', escape, true); previous?.focus() }
|
||||
}, [onClose])
|
||||
return <div className={classes.NR.overlay} onMouseDown={event => { if (event.target === event.currentTarget) onClose() }}>
|
||||
<section className={`${classes.NR.card} ${classes.NR.expertView}`} role="dialog" aria-modal="true" aria-label={`${expert.name.zh} · 缓存参考`}>
|
||||
<div className={c.top}>
|
||||
<div className={`${c.avatar} ${c.avatarFallback}`}><ExpertIcon width={32} height={32} /></div>
|
||||
<div className={c.info}><div className={c.titleRow}><div className={c.role}><span className={c.rolePrimary}>{expert.name.zh}</span></div>
|
||||
<div className={c.actions}><button ref={close} className="capability-icon-button" onClick={onClose} aria-label="关闭专家详情"><IconClose /></button></div>
|
||||
</div><button className={c.headerSummonBtn} disabled title="专家包暂不可用">仅供参考</button></div>
|
||||
</div>
|
||||
<div className={c.scrollShell}><div className={c.body}>
|
||||
<div className={c.content}><p className={c.desc}>{expert.description.zh}</p>
|
||||
<div className={c.tagsList}>{expert.tags.map(tag => <span className={c.tagChip} key={tag}>{tag}</span>)}</div>
|
||||
</div>
|
||||
<p className="capability-service-note" role="note">缓存参考目录,专家包暂不可用。完整介绍尚未获取,当前无法召唤。</p>
|
||||
</div></div>
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default function ExpertsPage({ onCreateExpert, onBackToChat, onUseExpert, onEditExpert, initialPage }: { onCreateExpert?(): void; onBackToChat?(): void; onUseExpert?(id: string, initPrompt?: string): Promise<void>; onEditExpert?(id: string): void; initialPage?: 'market' | 'mine' }): React.JSX.Element {
|
||||
const [page, setPage] = useState<'market' | 'mine'>(initialPage ?? 'market')
|
||||
const [localExperts, setLocalExperts] = useState<ExpertRecord[]>([])
|
||||
const [localError, setLocalError] = useState('')
|
||||
const refresh = useCallback(async (): Promise<void> => { try { setLocalExperts(await window.ycode.experts.list()); setLocalError('') } catch (error) { setLocalError(String(error)) } }, [])
|
||||
useEffect(() => { void refresh() }, [refresh, page])
|
||||
useEffect(() => { if (initialPage) setPage(initialPage) }, [initialPage])
|
||||
useEffect(() => { const refreshEvent = (): void => { void refresh() }; window.addEventListener('ycode:experts-changed', refreshEvent); const dispose = window.ycode.chat.onEvent(event => { if (['done', 'cancelled', 'error'].includes(event.type)) refreshEvent() }); return () => { dispose(); window.removeEventListener('ycode:experts-changed', refreshEvent) } }, [refresh])
|
||||
const [mineMode, setMineMode] = useState<'created' | 'purchased'>('created')
|
||||
const [query, setQuery] = useState('')
|
||||
const [group, setGroup] = useState<'expert' | 'team'>('expert')
|
||||
const [sort, setSort] = useState('reco')
|
||||
const [category, setCategory] = useState('全部')
|
||||
const [showReference, setShowReference] = useState(false)
|
||||
const [selected, setSelected] = useState<ReferenceExpert | null>(null)
|
||||
const closeDetail = useCallback(() => setSelected(null), [])
|
||||
const categoryStrip = useRef<HTMLDivElement>(null)
|
||||
const source = classes
|
||||
const results = group === 'expert' && category === '全部' ? filterExpertsByKeyword(references, query, 'zh') : []
|
||||
const go = (target: 'market' | 'mine'): void => { setPage(target); setQuery(''); setSelected(null) }
|
||||
const sourceExperts = localExperts.map(expert => ({ ...expert, description: expert.displayDescription || expert.description }))
|
||||
const sourceSwitch = <div className="wb-segmented wb-segmented--small" role="radiogroup" aria-label="我的专家来源">
|
||||
<div className="wb-segmented__thumb wb-segmented__thumb--measured" style={{ width: 72, transform: `translateX(${mineMode === 'created' ? 0 : 74}px)` }} />
|
||||
{(['created', 'purchased'] as const).map(mode => <button key={mode} role="radio" aria-checked={mineMode === mode} className={`wb-segmented__item wb-segmented__item--string-label${mineMode === mode ? ' wb-segmented__item--active' : ''}`} onClick={() => { setMineMode(mode); setQuery('') }}><span className="wb-segmented__item-label">{mode === 'created' ? '我创建的' : '我购买的'}</span></button>)}
|
||||
</div>
|
||||
return <>
|
||||
{page === 'mine' && <CapabilitySubpageHeader title="我的专家" onBack={() => go('market')} />}
|
||||
<CapabilityToolbar>
|
||||
<ExpertSearchBar value={query} onChange={setQuery} placeholder={page === 'mine' ? mineMode === 'purchased' ? '搜索我购买的专家' : '搜索我创建的专家' : '搜索专家'} />
|
||||
{page === 'market' ? <button className={`wb-button wb-button--secondary wb-button--medium ${source.tz.topbarActionBtn}`} onClick={() => go('mine')}>
|
||||
<span className="wb-button__icon wb-button__icon--left"><MyExpertIcon width={16} height={16} /></span><span className="wb-button__content">我的专家</span>
|
||||
</button> : mineMode === 'created' && <button className={`wb-button wb-button--primary wb-button--medium ${source.tz.topbarActionBtn}`} disabled={!onCreateExpert} onClick={onCreateExpert} title="通过对话创建专家"><span className="wb-button__icon wb-button__icon--left"><AddIcon width={16} height={16} /></span><span className="wb-button__content">创建专家</span></button>}
|
||||
</CapabilityToolbar>
|
||||
<div className={`${source.tz.mainScroll}${page === 'mine' ? ` ${source.tz.myExpertsScroll}` : ` ${source.tz.mainScrollSplit}`} capability-expert-body`} data-expert-page={page} data-reference-evidence={showReference ? REFERENCE_EVIDENCE : undefined}>
|
||||
<div className={page === 'mine' ? 'ec-my-panel' : 'capability-market-panel'}>
|
||||
{!(page === 'mine' && mineMode === 'created') && <div className={page === 'mine' ? 'ec-my-created-toolbar' : source.EI.row}>
|
||||
<div className={`${source.EI.tabs}${page === 'mine' ? ' ec-my-created-tabs' : ''}`} role="tablist" aria-label="专家类型">{(['expert', 'team'] as const).map(tab => <button type="button" role="tab" aria-selected={group === tab}
|
||||
className={`${source.EI.tab}${group === tab ? ` ${source.EI.active}` : ''}`} onClick={() => setGroup(tab)} key={tab}><span className={page === 'mine' ? 'ec-my-created-tab-label' : undefined}>{tab === 'expert' ? '专家' : '专家团'}</span>{page === 'mine' && <span className="ec-my-created-tab-count">0</span>}</button>)}</div>
|
||||
{page === 'market' && <div className={source.EI.sortGroup} aria-label="排序">{[['reco', '综合'], ['popular', '最热'], ['newest', '最新']].map(([id, label]) => <button type="button" className={`${source.EI.sortBtn}${sort === id ? ` ${source.EI.active}` : ''}`}
|
||||
key={id} onClick={() => setSort(id)} aria-pressed={sort === id} title={showReference ? '只读归档未保存其他排序结果' : undefined}>{label}</button>)}</div>}
|
||||
{page === 'mine' && (mineMode === 'purchased' || group !== 'team') && <div className="ec-my-toolbar-right">
|
||||
<div className="wb-segmented wb-segmented--small" role="radiogroup" aria-label="我的专家来源">
|
||||
<div className="wb-segmented__thumb wb-segmented__thumb--measured" style={{ width: 72, transform: `translateX(${mineMode === 'created' ? 0 : 74}px)` }} />
|
||||
{(['created', 'purchased'] as const).map(mode => <button key={mode} role="radio" aria-checked={mineMode === mode} className={`wb-segmented__item wb-segmented__item--string-label${mineMode === mode ? ' wb-segmented__item--active' : ''}`}
|
||||
onClick={() => { setMineMode(mode); setQuery('') }}><span className="wb-segmented__item-label">{mode === 'created' ? '我创建的' : '我购买的'}</span></button>)}
|
||||
</div>
|
||||
</div>}
|
||||
</div>}
|
||||
{localError && <p className="capability-service-note" role="alert">{localError}</p>}
|
||||
{page === 'mine' ? mineMode === 'purchased' ? <div className="ec-purchased-empty ec-purchased-empty--error"><p className="ec-purchased-empty-text">已购买专家目录暂不可用</p></div> : <CreatedExpertSourceProvider host={{ locale: 'zh', notify: message => setLocalError(previous => previous || message), openPath: async root => { const expert = localExperts.find(item => item.expertRootDir === root); if (!expert) throw Error('专家目录不存在'); await window.ycode.experts.openPath(expert.id) }, renderPreview: ({ expert, onClose, onSummon }) => createPortal(<div className="yc-capability-center capability-detail-host"><div className={`expert-center-${document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light'}`}>
|
||||
<LocalExpertDetail id={expert.id} onClose={onClose} onUse={id => { onSummon(id) }} />
|
||||
</div></div>, document.body) }}>
|
||||
<CreatedExpertsPanel createdExperts={sourceExperts} activeTab={group} onTabChange={setGroup} searchKeyword={query} toolbarExtra={group === 'team' ? undefined : sourceSwitch}
|
||||
onUse={async (id, initPrompt) => { try { setLocalError(''); const expert = localExperts.find(item => item.id === id); if (expert?.error) throw Error(expert.error); if (!onUseExpert) throw Error('专家会话不可用'); await onUseExpert(id, initPrompt) } catch (error) { setLocalError(String(error)); throw error } }} onEdit={onEditExpert ? expert => onEditExpert(expert.id) : undefined}
|
||||
onDelete={async id => { try { await window.ycode.experts.remove(id); await refresh(); window.dispatchEvent(new Event('ycode:experts-changed')); return true } catch (error) { setLocalError(String(error)); return false } }} />
|
||||
</CreatedExpertSourceProvider> : <>
|
||||
<div className={source.UF.wrap}><div className={source.UF.tabs} ref={categoryStrip} role="tablist" aria-label="专家分类"
|
||||
onWheel={event => { if (categoryStrip.current && Math.abs(event.deltaY) > Math.abs(event.deltaX)) { categoryStrip.current.scrollLeft += event.deltaY } }}>
|
||||
{categories.map(label => <button type="button" role="tab" aria-selected={category === label} className={`${source.UF.tab}${category === label ? ` ${source.UF.active}` : ''}`}
|
||||
onClick={() => setCategory(label)} key={label}>{label}</button>)}
|
||||
</div></div>
|
||||
<div className="capability-service-note capability-catalog-status" role="status">
|
||||
<span>{showReference ? '参考目录 · 只读,未安装' : '在线专家目录尚未接入'}</span>
|
||||
<button className="capability-text-button" onClick={() => setShowReference(value => !value)}>{showReference ? '关闭参考目录' : '查看缓存参考目录'}</button>
|
||||
</div>
|
||||
{showReference && results.length > 0 && sort === 'reco' ? <div className={source.lI.content}><div className={source.lI.grid}>
|
||||
{results.map((expert, index) => <ExpertCard key={expert.id} expert={expert} cardPosition={index} onSummon={() => setSelected(expert)} onExpertClick={() => setSelected(expert)}
|
||||
appearance="center" highlightKeyword={query} hideTopSummonButton priorityAvatarLoad isFirst={index === 0} />)}
|
||||
</div></div> : <div className="capability-expert-empty">
|
||||
<ExpertListEmptyState showIcon message={!showReference ? '在线专家目录暂不可用' : query ? '未找到归档中的专家' : '此归档未保存该分类或排序的专家'} />
|
||||
{!showReference && <><button className="wb-button wb-button--secondary wb-button--medium" onClick={() => go('mine')}>查看我的专家</button>
|
||||
{onBackToChat && <button className="capability-text-button" onClick={onBackToChat}>返回对话</button>}</>}
|
||||
</div>}
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
{selected && createPortal(<div className="yc-capability-center capability-detail-host"><div className={`expert-center-${document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light'}`}>
|
||||
<ExpertReferenceDetail expert={selected} onClose={closeDetail} />
|
||||
</div></div>, document.body)}
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/* REA ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb; contexts-BMeWSUHk.js 5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b. Literal SVG bodies recovered from archived source. */
|
||||
import { useId, type SVGProps } from 'react'
|
||||
import { jsx as sourceJsx, jsxs as sourceJsxs } from 'react/jsx-runtime'
|
||||
// UTF-16 [4192064, 4193924)
|
||||
export const SkillsIcon = (e: SVGProps<SVGSVGElement>) =>sourceJsx(`svg`,{width:16,height:16,xmlns:"http://www.w3.org/2000/svg",...e,children:sourceJsx(`path`,{fill:`currentColor`,fillRule:`evenodd`,transform:`matrix(1 0 0 1 0.485779 0.117911)`,d:`M4.6063 4.6987Q4.7494 4.7505 4.8255 4.7783Q5.7193 5.1048 6.5463 5.5015L0.59 11.4578Q0 12.0478 0 12.8821Q0 13.7164 0.59 14.3064Q1.1799 14.8963 2.0142 14.8963Q2.8485 14.8963 3.4385 14.3064L8.8444 8.9005L7.9958 8.052L2.59 13.4578Q2.3515 13.6963 2.0142 13.6963Q1.677 13.6963 1.4385 13.4578Q1.2 13.2193 1.2 12.8821Q1.2 12.5448 1.4385 12.3064L7.6601 6.0847Q8.8609 6.7696 9.9038 7.6204Q12.3371 9.6056 13.2462 12.0051Q13.3286 12.2429 13.5609 12.3395Q13.7872 12.4495 14.0199 12.3536Q14.194 12.2902 14.2984 12.1372Q14.4068 11.987 14.4072 11.8017Q14.459 8.4358 12.2057 5.3174Q9.969 2.2218 6.2641 0.4907Q5.6636 0.2101 5.415 0.1409Q4.9087 0 4.4826 0.1859Q4.1015 0.3522 3.8452 0.7962Q3.7204 1.0124 3.4651 1.6298L3.4638 1.633L3.4222 1.7337Q3.4095 1.7643 3.3849 1.8238Q3.1054 2.4987 3.0381 2.7676Q2.8998 3.3203 3.1034 3.7425Q3.2831 4.1152 3.7555 4.3571Q3.9811 4.4726 4.6063 4.6987ZM10.6624 6.6906Q8.315 4.7755 5.2372 3.6511Q5.1593 3.6227 5.0145 3.5703Q4.2127 3.2802 4.1843 3.2213Q4.1416 3.1328 4.4936 2.2829Q4.5183 2.2232 4.5311 2.1922L4.5728 2.0915L4.5741 2.0883Q4.8934 1.3159 4.9625 1.2858Q5.0496 1.2478 5.7561 1.5779Q9.1787 3.1771 11.233 6.0201Q12.1806 7.3316 12.6737 8.6809Q11.8234 7.6378 10.6624 6.6906ZM10.0312 10.5647C9.9585 10.3032 9.5796 10.3284 9.4539 10.6029L8.8284 11.9696C8.7889 12.0557 8.7157 12.1262 8.6286 12.163L7.2451 12.7441C6.9676 12.861 6.9301 13.2405 7.1879 13.3222L8.473 13.7281C8.5541 13.7537 8.6138 13.8161 8.6366 13.8983L8.998 15.1995C9.0706 15.461 9.4496 15.4358 9.5753 15.1612L10.2008 13.7945C10.2404 13.7084 10.3135 13.6379 10.4006 13.6012L11.7831 13.0201C12.0609 12.9032 12.0984 12.5235 11.8403 12.442L10.5562 12.0361C10.4752 12.0105 10.4155 11.9488 10.3926 11.8669L10.0312 10.5647Z`})})
|
||||
// UTF-16 [4198498, 4200400)
|
||||
export const PinIcon = (e: SVGProps<SVGSVGElement>) =>{let t=`wb-pin-clip-${useId()}`;return sourceJsxs(`svg`,{width:16,height:16,xmlns:"http://www.w3.org/2000/svg",...e,viewBox:`0 0 16 16`,children:[sourceJsx(`clipPath`,{id:t,children:sourceJsx(`rect`,{width:`16`,height:`16`})}),sourceJsx(`g`,{clipPath:`url(#${t})`,children:sourceJsx(`path`,{fill:`currentColor`,fillRule:`evenodd`,transform:`matrix(0.707107 0.707107 -0.707107 0.707107 9.66 -1.44)`,d:`M8.9091 1.7259C9.0313 2.1014 9.0313 2.5676 9.0313 3.5L9.0313 3.9998C9.0313 4.6592 9.0313 4.9889 9.0829 5.3072C9.1518 5.7316 9.2887 6.1423 9.4883 6.5232C9.638 6.8088 9.8359 7.0725 10.2316 7.6C10.7349 8.271 11.0022 8.6051 11.0295 8.9079C11.0634 9.2833 10.8774 9.6554 10.5568 9.8535C10.3173 10.0015 9.9329 10.001 9.2114 10.0002L9.2087 10.0002L9.208 10.0002C9.1513 10.0001 9.0926 10 9.0317 10L7.3316 10L7.3316 8.8L9.0317 8.8Q9.0949 8.8 9.2128 8.8002Q9.457 8.8005 9.628 8.7982Q9.5272 8.66 9.3804 8.4649Q9.3093 8.3703 9.2717 8.3201Q8.62 7.4515 8.4255 7.0803Q8.0336 6.3326 7.8984 5.4994Q7.8313 5.0857 7.8313 3.9998L7.8313 3.5Q7.8313 2.2917 7.768 2.0971Q7.5633 1.468 6.9341 1.2633Q6.7396 1.2 5.5313 1.2Q4.323 1.2 4.1284 1.2633Q3.4992 1.468 3.2946 2.0971Q3.2313 2.2917 3.2313 3.5L3.2313 4.0002Q3.2313 5.0862 3.1641 5.4999Q3.029 6.3325 2.6376 7.0797Q2.4431 7.451 1.7916 8.3199Q1.754 8.37 1.6829 8.4647Q1.5361 8.6599 1.4352 8.7982Q1.6064 8.8005 1.8507 8.8002Q1.9695 8.8 2.0318 8.8L6.1316 8.8L6.1316 10L6.1313 10L6.1317 17L4.9317 17.0001L4.9313 10L2.0318 10C1.9696 10 1.9098 10.0001 1.8521 10.0002C1.1304 10.001 0.7459 10.0015 0.5064 9.8534C0.1859 9.6553 0 9.2835 0.0338 8.9083C0.061 8.6054 0.3283 8.2712 0.8316 7.6C1.2272 7.0723 1.425 6.8085 1.5746 6.5229C1.774 6.1422 1.9108 5.7319 1.9796 5.3077C2.0313 4.9894 2.0313 4.6597 2.0313 4.0002L2.0313 3.5C2.0313 2.5676 2.0313 2.1014 2.1534 1.7259C2.4 0.9678 2.9991 0.3688 3.7572 0.1222C4.1327 0 4.5989 0 5.5313 0C6.4637 0 6.9299 0 7.3054 0.1222C8.0634 0.3688 8.6625 0.9678 8.9091 1.7259Z`})})]})}
|
||||
// UTF-16 [4200401, 4202089)
|
||||
export const UnpinIcon = (e: SVGProps<SVGSVGElement>) =>sourceJsx(`svg`,{width:16,height:16,xmlns:"http://www.w3.org/2000/svg",...e,viewBox:`0 0 16 16`,children:sourceJsx(`path`,{fill:`currentColor`,fillRule:`evenodd`,transform:`matrix(1 0 0 1 1.12637 0.941389)`,d:`M13.6129 5.1387C13.4338 5.4905 13.1041 5.8202 12.4448 6.4795L12.0914 6.8329C11.6251 7.2992 11.392 7.5323 11.2035 7.7939C10.952 8.1427 10.7584 8.5299 10.6303 8.9404C10.6282 8.9471 10.6261 8.9538 10.6241 8.9605L13.7979 12.1344L12.9494 12.9829L0.9494 0.9829L1.7979 0.1344L5.0142 3.3507C5.021 3.3486 5.0279 3.3465 5.0347 3.3444C5.4448 3.2162 5.8317 3.0227 6.1803 2.7715C6.4419 2.5829 6.6751 2.3498 7.1414 1.8834L7.4951 1.5297C8.1544 0.8704 8.484 0.5408 8.8359 0.3617C9.5463 0 10.3935 0 11.104 0.3617C11.4558 0.5408 11.7855 0.8704 12.4448 1.5297C13.1041 2.1891 13.4338 2.5187 13.6129 2.8706C13.9746 3.581 13.9746 4.4282 13.6129 5.1387ZM5.9457 4.2821Q6.4391 4.0641 6.882 3.745Q7.222 3.4999 7.9899 2.732L8.3436 2.3783Q9.198 1.5239 9.3803 1.4311Q9.9699 1.1309 10.5596 1.4311Q10.7419 1.5239 11.5963 2.3783Q12.4507 3.2327 12.5435 3.415Q12.8437 4.0046 12.5435 4.5942Q12.4507 4.7766 11.5963 5.631L11.2429 5.9844Q10.475 6.7522 10.23 7.0922Q9.9106 7.5353 9.6926 8.029L5.9457 4.2821ZM5.7982 9.0253L6.6451 8.1785L2.3466 3.88C2.3144 3.8988 2.2852 3.9192 2.2584 3.9415C1.9692 4.183 1.8377 4.5774 1.9243 4.944C1.9889 5.2182 2.2611 5.4897 2.772 5.9994C2.8129 6.0401 2.8553 6.0824 2.8992 6.1263L4.9495 8.1766L0 13.1267L0.8486 13.9751L5.798 9.0251L5.7982 9.0253ZM7.4936 9.027L10.0951 11.6286C10.0764 11.6606 10.0561 11.6898 10.0338 11.7164C9.7923 12.0059 9.3977 12.1374 9.0309 12.0508C8.7569 11.9861 8.4854 11.714 7.9759 11.2032L7.974 11.2013L7.9734 11.2008C7.9334 11.1607 7.892 11.1191 7.8489 11.076L6.6468 9.8739L7.4936 9.027Z`})})
|
||||
// UTF-16 [4203071, 4204899)
|
||||
export const SettingsIcon = (e: SVGProps<SVGSVGElement>) =>sourceJsx(`svg`,{width:16,height:16,xmlns:"http://www.w3.org/2000/svg",...e,children:sourceJsx(`path`,{fill:`currentColor`,fillRule:`evenodd`,transform:`matrix(1 0 0 1 0.709401 1.33782)`,d:`M13.6555 4.3622Q14.1634 5.2418 14.3164 5.5797Q14.5812 6.1642 14.5812 6.6622Q14.5812 7.1602 14.3164 7.7447Q14.1634 8.0826 13.6555 8.9622L12.4649 11.0244Q11.9571 11.904 11.741 12.2055Q11.3672 12.727 10.9359 12.976Q10.5046 13.225 9.866 13.288Q9.4969 13.3244 8.4812 13.3244L6.1 13.3244Q5.0843 13.3244 4.7152 13.288Q4.0766 13.225 3.6453 12.976Q3.214 12.727 2.8402 12.2055Q2.6241 11.904 2.1163 11.0244L0.9257 8.9622Q0.4178 8.0826 0.2648 7.7447Q0 7.1602 0 6.6622Q0 6.1642 0.2648 5.5797Q0.4178 5.2418 0.9257 4.3622L2.1163 2.3Q2.6241 1.4204 2.8402 1.1189Q3.214 0.5973 3.6453 0.3483Q4.0766 0.0993 4.7152 0.0364Q5.0843 0 6.1 0L8.4812 0Q9.4969 0 9.866 0.0364Q10.5046 0.0993 10.9359 0.3483Q11.3672 0.5973 11.741 1.1189Q11.7677 1.1562 11.7989 1.2023L8.8058 1.2023Q8.6509 1.2 8.4812 1.2L6.1 1.2Q4.5702 1.2 4.2453 1.3876Q3.9204 1.5751 3.1555 2.9L1.9649 4.9622Q1.2 6.287 1.2 6.6622Q1.2 7.0373 1.9649 8.3622L3.1555 10.4244Q3.9204 11.7492 4.2453 11.9368Q4.5702 12.1244 6.1 12.1244L8.4812 12.1244Q10.011 12.1244 10.3359 11.9368Q10.6608 11.7492 11.4257 10.4244L12.6163 8.3622Q13.3812 7.0373 13.3812 6.6622Q13.3812 6.287 12.6163 4.9622L11.4257 2.9Q11.2681 2.627 11.1292 2.4023L12.524 2.4023L13.6555 4.3622ZM7.2911 3.6622Q8.5337 3.6622 9.4124 4.5409Q10.2911 5.4195 10.2911 6.6622Q10.2911 7.9048 9.4124 8.7835Q8.5337 9.6622 7.2911 9.6622Q6.0484 9.6622 5.1698 8.7835Q4.2911 7.9048 4.2911 6.6622Q4.2911 5.4195 5.1698 4.5409Q6.0484 3.6622 7.2911 3.6622ZM7.2911 4.8622Q6.5455 4.8622 6.0183 5.3894Q5.4911 5.9166 5.4911 6.6622Q5.4911 7.4078 6.0183 7.935Q6.5455 8.4622 7.2911 8.4622Q8.0367 8.4622 8.5639 7.935Q9.0911 7.4078 9.0911 6.6622Q9.0911 5.9166 8.5639 5.3894Q8.0367 4.8622 7.2911 4.8622Z`})})
|
||||
// UTF-16 [4197943, 4198217)
|
||||
export const SearchIcon = (e: SVGProps<SVGSVGElement>) =>sourceJsxs(`svg`,{width:16,height:16,xmlns:"http://www.w3.org/2000/svg",...e,viewBox:`0 0 16 16`,fill:`none`,children:[sourceJsx(`circle`,{cx:`7`,cy:`7`,r:`5.25`,stroke:`currentColor`,strokeWidth:`1.4`}),sourceJsx(`path`,{d:`M11 11l3 3`,stroke:`currentColor`,strokeWidth:`1.4`,strokeLinecap:`round`})]})
|
||||
// UTF-16 [4198218, 4198497)
|
||||
export const ClearFilledIcon = (e: SVGProps<SVGSVGElement>) =>sourceJsxs(`svg`,{width:16,height:16,xmlns:"http://www.w3.org/2000/svg",...e,viewBox:`0 0 16 16`,fill:`none`,children:[sourceJsx(`circle`,{cx:`8`,cy:`8`,r:`6.5`,fill:`currentColor`}),sourceJsx(`path`,{d:`M5.75 5.75l4.5 4.5M10.25 5.75l-4.5 4.5`,stroke:`#FFFFFF`,strokeWidth:`1.4`,strokeLinecap:`round`})]})
|
||||
@@ -0,0 +1,125 @@
|
||||
import { cloneElement, useCallback, useEffect, useMemo, useRef, useState, type ReactElement, type ReactNode } from 'react'
|
||||
import type { SkillDocument, SkillRecord } from '@shared/capabilities'
|
||||
import { CapabilitySubpageHeader, CapabilityToolbar } from './CapabilityCenter'
|
||||
import { SkillsIcon, PinIcon, UnpinIcon, SearchIcon, ClearFilledIcon } from './SkillSourceIcons'
|
||||
import { CardFrame, CardMoreIcon, PlusIcon, TrashIcon, FolderIcon, CloseIcon, UseSkillIcon, DocIcon, setSkillPrimitives } from './skills-source.mjs'
|
||||
import MarkdownContent from '../components/MarkdownContent'
|
||||
import './skills-source.css'
|
||||
import './skills-host.css'
|
||||
|
||||
export interface SkillsPageProps { cwd?: string; onUseSkill?(skill: SkillRecord): void; onCreateSkill?(): void; onFindSkill?(): void }
|
||||
function Avatar({ name, size = 48, borderRadius = 16 }: { name: string; size?: number; borderRadius?: number }): React.JSX.Element {
|
||||
return <span className="yc-skill-avatar" aria-hidden style={{ width: size, height: size, borderRadius }}>{Array.from(name.trim())[0]?.toUpperCase() ?? 'S'}</span>
|
||||
}
|
||||
function Tooltip({ children, content }: { children: ReactElement<{ title?: string }>; content: ReactNode }): React.JSX.Element { return cloneElement(children, { title: typeof content === 'string' ? content : undefined }) }
|
||||
function BatchCheckbox({ selected }: { selected: boolean }): React.JSX.Element { return <input className="sm-card-batch-checkbox" type="checkbox" checked={selected} readOnly tabIndex={-1} aria-hidden /> }
|
||||
setSkillPrimitives({ Avatar, Tooltip, Checkbox: BatchCheckbox })
|
||||
function Button({ children, primary = false, className = '', ...props }: React.ButtonHTMLAttributes<HTMLButtonElement> & { primary?: boolean }): React.JSX.Element {
|
||||
return <button type="button" {...props} className={`wb-button wb-button--${primary ? 'primary' : 'secondary'} wb-button--medium ${className}`}><span className="wb-button__content">{children}</span></button>
|
||||
}
|
||||
function Switch({ skill, disabled, large, onToggle }: { skill: SkillRecord; disabled?: boolean; large?: boolean; onToggle(): void }): React.JSX.Element {
|
||||
return <label className={`wb-switch${large ? ' wb-switch--large sm-skill-detail-toggle' : ''}`} title={skill.error ?? (skill.enabled ? '禁用技能' : '启用技能')}><span className="wb-switch__control"><input className="wb-switch__input" type="checkbox" role="switch" aria-label={`${skill.enabled ? '禁用' : '启用'} ${skill.name}`} checked={skill.enabled} disabled={disabled || !!skill.error} onChange={onToggle} /><span className="wb-switch__track"><span className="wb-switch__thumb" /></span></span></label>
|
||||
}
|
||||
function Search({ value, onChange }: { value: string; onChange(value: string): void }): React.JSX.Element {
|
||||
return <div className="ec-search-wrapper"><span className="wb-input-wrapper wb-input-wrapper--medium"><span className="wb-input__prefix"><SearchIcon /></span><input className="wb-input__inner" aria-label="搜索技能" placeholder="搜索技能" value={value} onChange={event => onChange(event.target.value)} /><button type="button" className={`wb-input__icon-button wb-input__clear${value ? '' : ' wb-input__clear--hidden'}`} aria-label="清空技能搜索" tabIndex={value ? 0 : -1} onClick={() => onChange('')}><ClearFilledIcon /></button></span></div>
|
||||
}
|
||||
/** Source layout and SVGs are recovered; native dialogs, storage, and accessibility are local host adapters. */
|
||||
const modalStack: HTMLElement[] = []
|
||||
function useModalFocus(ref: React.RefObject<HTMLElement | null>, close: () => void): void {
|
||||
const onClose = useRef(close); onClose.current = close
|
||||
useEffect(() => {
|
||||
const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null
|
||||
const element = ref.current
|
||||
if (element) modalStack.push(element)
|
||||
ref.current?.querySelector<HTMLElement>('button:not(:disabled),input:not(:disabled)')?.focus()
|
||||
const keyboard = (event: KeyboardEvent): void => {
|
||||
if (modalStack.at(-1) !== ref.current) return
|
||||
if (event.isComposing) return
|
||||
if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); onClose.current() }
|
||||
if (event.key === 'Tab') {
|
||||
const targets = [...(ref.current?.querySelectorAll<HTMLElement>('button:not(:disabled),input:not(:disabled),[tabindex="0"],a[href]') ?? [])].filter(el => el.getClientRects().length && getComputedStyle(el).visibility !== 'hidden')
|
||||
const index = targets.indexOf(document.activeElement as HTMLElement)
|
||||
if (targets.length && (index < 0 || event.shiftKey && index === 0 || !event.shiftKey && index === targets.length - 1)) { event.preventDefault(); targets[event.shiftKey ? targets.length - 1 : 0].focus() }
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', keyboard, true)
|
||||
return () => { document.removeEventListener('keydown', keyboard, true); if (element) { const index = modalStack.indexOf(element); if (index >= 0) modalStack.splice(index, 1) }; if (previous?.isConnected) previous.focus() }
|
||||
}, [ref])
|
||||
}
|
||||
function ImportModal({ busy, error, onClose, onImport }: { busy: boolean; error: string; onClose(): void; onImport(overwrite: boolean, kind: 'directory' | 'zip'): void }): React.JSX.Element {
|
||||
const dialog = useRef<HTMLDivElement>(null), [overwrite, setOverwrite] = useState(false)
|
||||
useModalFocus(dialog, () => { if (!busy) onClose() })
|
||||
return <div className="skill-upload-modal-mask" onClick={event => { if (event.target === event.currentTarget && !busy) onClose() }}><div ref={dialog} className="skill-upload-modal" role="dialog" aria-modal="true" aria-label="导入技能">
|
||||
<div className="skill-upload-modal-header"><span>导入技能</span><button type="button" className="skill-upload-modal-close" aria-label="关闭导入技能" disabled={busy} onClick={onClose}><CloseIcon /></button></div>
|
||||
<button type="button" className={'skill-upload-drop-zone' + (busy ? ' skill-upload-drop-zone--disabled' : '')} aria-label="选择文件夹" disabled={busy} onClick={() => onImport(overwrite, 'directory')}>
|
||||
{busy && <span className="skill-upload-overlay"><span className="skill-upload-overlay-spinner" /><span className="skill-upload-overlay-text">正在导入技能…</span></span>}
|
||||
<span className="skill-upload-drop-icon"><svg width="32" height="32" viewBox="0 0 32 32" fill="none"><rect x="3" y="6" width="26" height="20" rx="2" stroke="currentColor" strokeWidth="1.6" /><path d="M3 11h26" stroke="currentColor" strokeWidth="1.6" /><path d="M10 6V3M22 6V3" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" /><path d="M16 17v5M13 20l3-3 3 3" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" /></svg></span><span className="skill-upload-drop-title">选择技能文件夹</span>
|
||||
</button>
|
||||
<div className="skill-upload-requirements"><div className="skill-upload-requirements-title">技能文件要求</div><ul className="skill-upload-requirements-list"><li>文件夹根目录包含 SKILL.md</li><li>SKILL.md 包含 name 和 description 元数据</li></ul><button type="button" className="skill-upload-pick-zip-link" disabled={busy} onClick={() => onImport(overwrite, 'zip')}>选择 ZIP 文件</button></div>
|
||||
<label className="yc-skill-overwrite"><input type="checkbox" checked={overwrite} disabled={busy} onChange={event => setOverwrite(event.target.checked)} />替换已导入的同名技能</label>
|
||||
<p className="yc-skill-local-note">导入到本机技能库;当前未连接远程安全扫描服务。</p>{error && <p className="yc-skill-error" role="alert">{error}</p>}
|
||||
</div></div>
|
||||
}
|
||||
function Detail({ skill, document, busy, error, onClose, onToggle, onUse, onExport, onRemove }: { skill: SkillRecord; document: SkillDocument | null; busy: boolean; error: string; onClose(): void; onToggle(): void; onUse?(): void; onExport(): void; onRemove(): void }): React.JSX.Element {
|
||||
const dialog = useRef<HTMLDivElement>(null), scroll = useRef<HTMLDivElement>(null), [fades, setFades] = useState({ top: false, bottom: false })
|
||||
useModalFocus(dialog, onClose)
|
||||
const measure = useCallback(() => { const e = scroll.current; if (e) setFades({ top: e.scrollTop > 1, bottom: e.scrollHeight - e.scrollTop - e.clientHeight > 1 }) }, [])
|
||||
useEffect(() => { measure(); const observer = new ResizeObserver(measure); if (scroll.current) observer.observe(scroll.current); return () => observer.disconnect() }, [measure, document])
|
||||
return <div className="sm-skill-detail-overlay" onClick={event => { if (event.target === event.currentTarget) onClose() }}><div className="sm-skill-detail-card is-skill-view" ref={dialog} role="dialog" aria-modal="true" aria-label={skill.name}>
|
||||
<div className="sm-skill-detail-view"><div className="sm-skill-detail-header"><div className="sm-skill-detail-icon-wrap"><Avatar name={skill.name} size={68} /></div><div className="sm-skill-detail-header-body"><div className="sm-skill-detail-title-row"><h2>{skill.name}</h2><div className="sm-skill-detail-secondary"><Switch skill={skill} large disabled={busy} onToggle={onToggle} /><button type="button" className="sm-skill-detail-close" aria-label="关闭技能详情" onClick={onClose}><CloseIcon /></button></div></div><div className="sm-skill-detail-action-row">
|
||||
<button className="sm-skill-detail-btn sm-skill-detail-btn--primary" aria-label="使用技能" disabled={busy || !skill.enabled || !skill.userInvocable || !onUse} onClick={onUse}><UseSkillIcon />使用技能</button>
|
||||
{skill.scope === 'user' && <><Button disabled={busy || !!skill.error} onClick={onExport}><FolderIcon />导出</Button><button className="sm-skill-detail-more-btn" aria-label="卸载技能" disabled={busy} onClick={onRemove}><TrashIcon /></button></>}
|
||||
</div></div></div>
|
||||
<div className={`sm-skill-detail-scroll-shell${fades.top ? ' has-top-fade' : ''}${fades.bottom ? ' has-bottom-fade' : ''}`}><div className="sm-skill-detail-scroll-fade sm-skill-detail-scroll-fade-top" /><div ref={scroll} onScroll={measure} className="sm-skill-detail-scroll-body"><div className="sm-skill-detail-intro"><div className="sm-skill-detail-desc-wrap"><p className="sm-skill-detail-desc">{skill.description || skill.error}</p></div><div className="sm-skill-detail-tags"><span className="sm-skill-detail-chip">{skill.scope === 'workspace' ? '空间技能' : '本地导入'}</span>{skill.version && <span className="sm-skill-detail-chip">v{skill.version}</span>}{skill.disableModelInvocation && <span className="sm-skill-detail-chip">仅手动调用</span>}{!skill.userInvocable && <span className="sm-skill-detail-chip">仅模型调用</span>}</div></div>
|
||||
{error && <p className="yc-skill-error" role="alert">{error}</p>}
|
||||
<section className="sm-skill-detail-overview"><div className="sm-skill-detail-section-header sm-skill-detail-overview-header"><h3 className="sm-skill-detail-section-title"><span className="sm-skill-detail-section-title-icon"><DocIcon /></span>技能说明</h3></div><div className="sm-skill-detail-preview-shell"><div className="sm-skill-detail-overview-body sm-skill-detail-preview">{document ? <MarkdownContent content={document.content} imageUrlResolver={async () => undefined} /> : <p>{skill.error || '正在读取技能说明…'}</p>}</div></div></section>
|
||||
{document && <section className="sm-skill-detail-more-section"><h3 className="sm-skill-detail-section-title">技能文件</h3><div className="yc-skill-files">{document.files.map(file => <code key={file}>{file}</code>)}</div><div className="sm-skill-detail-more-bar" title={skill.path}>{skill.path}</div></section>}
|
||||
</div><div className="sm-skill-detail-scroll-fade sm-skill-detail-scroll-fade-bottom" /></div></div>
|
||||
</div></div>
|
||||
}
|
||||
export default function SkillsPage({ cwd, onUseSkill, onCreateSkill, onFindSkill }: SkillsPageProps): React.JSX.Element {
|
||||
const [view, setView] = useState<'market' | 'installed'>('market'), [items, setItems] = useState<SkillRecord[]>([]), [query, setQuery] = useState(''), [category, setCategory] = useState('推荐'), [loading, setLoading] = useState(true), [busy, setBusy] = useState(false), [error, setError] = useState(''), [notice, setNotice] = useState(''), [addOpen, setAddOpen] = useState(false), [importOpen, setImportOpen] = useState(false), [importError, setImportError] = useState(''), [selectedId, setSelectedId] = useState<string | null>(null), [document, setDocument] = useState<SkillDocument | null>(null), [detailError, setDetailError] = useState(''), [menuId, setMenuId] = useState<string | null>(null), [batch, setBatch] = useState(false), [selected, setSelected] = useState<string[]>([]), [pendingRemove, setPendingRemove] = useState<SkillRecord[]>([])
|
||||
const currentCwd = useRef(cwd); currentCwd.current = cwd
|
||||
const generation = useRef(0), mounted = useRef(true), add = useRef<HTMLDivElement>(null), removeDialog = useRef<HTMLDivElement>(null)
|
||||
const key = 'ycode.skills.menu.v1:' + (cwd ?? '').replaceAll('\\', '/').toLowerCase()
|
||||
const [pinned, setPinned] = useState<string[]>([])
|
||||
useEffect(() => { if (!notice) return; const timer = window.setTimeout(() => setNotice(''), 3000); return () => window.clearTimeout(timer) }, [notice])
|
||||
useEffect(() => { try { setPinned(readPinned(key)) } catch { setPinned([]) } }, [key])
|
||||
useEffect(() => { mounted.current = true; return () => { mounted.current = false; generation.current++ } }, [])
|
||||
const reload = useCallback(async () => { if (currentCwd.current !== cwd) return; const current = ++generation.current; const values = await window.ycode.skills.list(cwd); if (mounted.current && currentCwd.current === cwd && current === generation.current) setItems(values) }, [cwd])
|
||||
useEffect(() => { setLoading(true); setQuery(''); setSelectedId(null); setSelected([]); setMenuId(null); setError(''); setNotice(''); void reload().catch(reason => { if (mounted.current) setError(String(reason.message ?? reason)) }).finally(() => { if (mounted.current) setLoading(false) }) }, [reload])
|
||||
useEffect(() => { if (!addOpen && !menuId) return; const close = (event: PointerEvent): void => { if (add.current?.contains(event.target as Node) || (event.target as Element)?.closest?.('.sm-card-menu-wrap')) return; setAddOpen(false); setMenuId(null) }; const keydown = (event: KeyboardEvent): void => { if (event.key === 'Escape') { setAddOpen(false); setMenuId(null) } }; documentGlobal().addEventListener('pointerdown', close); window.addEventListener('keydown', keydown); return () => { documentGlobal().removeEventListener('pointerdown', close); window.removeEventListener('keydown', keydown) } }, [addOpen, menuId])
|
||||
const current = items.find(item => item.id === selectedId) ?? null
|
||||
useEffect(() => { let active = true; setDocument(null); setDetailError(''); if (current && !current.error) void window.ycode.skills.read(current.id, cwd).then(value => { if (active) setDocument(value) }).catch(reason => { if (active) setDetailError(String(reason.message ?? reason)) }); return () => { active = false } }, [current?.id, current?.error, current?.enabled, cwd])
|
||||
const run = async (operation: () => Promise<unknown>): Promise<void> => { if (busy) return; setBusy(true); setError(''); setNotice(''); try { await operation() } catch (reason) { if (mounted.current) setError(reason instanceof Error ? reason.message : String(reason)) } finally { try { await reload() } catch (reason) { if (mounted.current) setError(reason instanceof Error ? reason.message : String(reason)) }; if (mounted.current) setBusy(false) } }
|
||||
const matches = useMemo(() => items.filter(item => `${item.name}\n${item.description}`.toLowerCase().includes(query.trim().toLowerCase())).sort((a, b) => Number(pinned.includes(b.id)) - Number(pinned.includes(a.id))), [items, query, pinned])
|
||||
const pin = (id: string): void => { const next = pinned.includes(id) ? pinned.filter(value => value !== id) : [...pinned, id]; setPinned(next); let memory = {}; try { memory = JSON.parse(localStorage.getItem(key) ?? '{}') } catch { /* Invalid optional local memory is discarded. */ } localStorage.setItem(key, JSON.stringify({ ...memory, pinned: next })) }
|
||||
const useSkill = (skill: SkillRecord): void => { if (skill.enabled && skill.userInvocable && !skill.error) onUseSkill?.(skill) }
|
||||
const importPackage = async (overwrite: boolean, kind: 'directory' | 'zip'): Promise<void> => { if (busy) return; setBusy(true); setImportError(''); try { const value = await window.ycode.skills.importPath(undefined, overwrite, kind); if (value) { await reload(); setImportOpen(false); setView('installed'); setNotice(`已导入 ${value.name}`) } } catch (reason) { setImportError(reason instanceof Error ? reason.message : String(reason)) } finally { setBusy(false) } }
|
||||
const toggle = (skill: SkillRecord): void => { void run(() => window.ycode.skills.setEnabled(skill.id, !skill.enabled, cwd)) }
|
||||
const exportPackage = (skill: SkillRecord): void => { void run(async () => { const target = await window.ycode.skills.export(skill.id); if (target) setNotice('已导出技能 ZIP') }) }
|
||||
const toggleSelected = (id: string): void => setSelected(values => values.includes(id) ? values.filter(value => value !== id) : [...values, id])
|
||||
return <div className="skill-market yc-skills-page">
|
||||
{view === 'installed' && <CapabilitySubpageHeader title={`已安装${items.length ? ` (${items.length})` : ''}`} onBack={() => { setView('market'); setQuery(''); setBatch(false) }} />}
|
||||
<CapabilityToolbar><Search value={query} onChange={setQuery} />{view === 'market' ? <Button className="sm-topbar-action-btn" aria-label="已安装技能" onClick={() => { setView('installed'); setQuery('') }}><SkillsIcon />已安装{items.length > 0 && <span className="sm-topbar-installed-count">({items.length})</span>}</Button> : <button className="sm-installed-batch-manage" onClick={() => { setBatch(value => !value); setSelected([]) }}>{batch ? '完成' : '批量管理'}</button>}
|
||||
<div ref={add} className="yc-skill-add"><Button primary className="sm-topbar-action-btn" aria-label="添加技能" aria-expanded={addOpen} onClick={() => setAddOpen(value => !value)}><PlusIcon />添加技能</Button>{addOpen && <div className="wb-popover wb-dropdown yc-skill-add-menu" role="menu" onKeyDown={event => { if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return; event.preventDefault(); const rows = [...event.currentTarget.querySelectorAll<HTMLButtonElement>('button:not(:disabled)')]; const index = rows.indexOf(window.document.activeElement as HTMLButtonElement); rows[event.key === 'Home' ? 0 : event.key === 'End' ? rows.length - 1 : (index + (event.key === 'ArrowUp' ? -1 : 1) + rows.length) % rows.length]?.focus() }}><div className="wb-dropdown__list">{[{ label: '查找技能', action: () => onFindSkill?.() }, { label: '上传技能', action: () => { setImportError(''); setImportOpen(true) } }, { label: '创建技能', action: () => onCreateSkill?.() }].map(row => <button type="button" role="menuitem" className="wb-dropdown__item" key={row.label} disabled={row.label === '查找技能' && !onFindSkill || row.label === '创建技能' && !onCreateSkill} onClick={() => { setAddOpen(false); add.current?.querySelector('button')?.focus(); row.action() }}><span className="wb-dropdown__label">{row.label}</span></button>)}</div></div>}</div>
|
||||
</CapabilityToolbar>
|
||||
<div className="yc-skills-scroll">{error && <div className="yc-skill-error" role="alert">{error}<button onClick={() => { void run(reload) }}>重试</button></div>}{notice && <p className="yc-skill-notice" role="status">{notice}</p>}
|
||||
{view === 'market' ? <><div className="sm-search-panel"><div className="sm-segment-bar"><div className="sm-segment-group" role="tablist" aria-label="技能来源">{['推荐', 'SkillHub', '套件'].map(value => <button key={value} role="tab" aria-selected={category === value} className={'sm-segment-item' + (category === value ? ' sm-segment-item--active' : '')} onClick={() => setCategory(value)}>{value}</button>)}</div></div><div className="sm-empty yc-skill-market-unavailable"><SkillsIcon width={32} height={32} /><p>当前未连接{category === '推荐' ? '技能市场' : category}目录服务</p><p>你可以导入本地技能,或查看已安装技能。</p><Button onClick={() => { setView('installed'); setQuery('') }}>查看已安装技能</Button></div></div></> : <section className="sm-installed-section">
|
||||
{batch && <div className="sm-batch-toolbar"><div className="sm-batch-toolbar-left"><input type="checkbox" aria-label="全选技能" checked={matches.length > 0 && matches.every(item => selected.includes(item.id))} onChange={event => setSelected(event.target.checked ? matches.map(item => item.id) : [])} />已选择 {selected.length} 项</div><div className="sm-batch-toolbar-right"><Button disabled={busy || !selected.length} onClick={() => { void run(async () => { for (const item of items.filter(value => selected.includes(value.id) && !value.error)) await window.ycode.skills.setEnabled(item.id, true, cwd) }) }}>启用</Button><Button disabled={busy || !selected.length} onClick={() => { void run(async () => { for (const item of items.filter(value => selected.includes(value.id) && !value.error)) await window.ycode.skills.setEnabled(item.id, false, cwd) }) }}>禁用</Button><Button disabled={busy || !selected.some(id => items.some(item => item.id === id && item.scope === 'user'))} onClick={() => setPendingRemove(items.filter(item => selected.includes(item.id) && item.scope === 'user'))}>卸载</Button></div></div>}
|
||||
{loading ? <div className="sm-empty" role="status">加载中…</div> : !matches.length ? <div className="sm-empty">{query ? '没有找到相关技能' : '暂无已安装技能'}</div> : <div className="sm-installed-list">{(['workspace', 'user'] as const).map(scope => { const group = matches.filter(item => item.scope === scope); return !!group.length && <section key={scope} className="sm-installed-group"><div className="sm-installed-group-header"><div className="sm-installed-group-title"><span>{scope === 'workspace' ? '空间技能' : '自定义技能'}</span><span className="sm-installed-group-count">({group.length})</span></div></div><div className="sm-grid">{group.map(skill => <div className="yc-skill-card-keyboard" key={skill.id} data-skill-id={skill.id} role={batch ? undefined : 'button'} tabIndex={0} aria-label={`查看 ${skill.name}`} onKeyDown={event => { if (event.target === event.currentTarget && ['Enter', ' '].includes(event.key)) { event.preventDefault(); if (batch) toggleSelected(skill.id); else setSelectedId(skill.id) } }}>
|
||||
<CardFrame title={skill.name} description={skill.error ? `不可用:${skill.error}` : skill.description} installed installedListMode disabled={!skill.enabled} batchMode={batch} selected={selected.includes(skill.id)} onToggleSelect={() => toggleSelected(skill.id)} onOpen={() => setSelectedId(skill.id)} actions={!batch && <div className="sm-status" onClick={event => event.stopPropagation()}><div className="sm-card-menu-wrap"><button type="button" className="sm-card-menu-btn sm-card-more" aria-label={`更多 ${skill.name}`} aria-expanded={menuId === skill.id} onClick={() => setMenuId(menuId === skill.id ? null : skill.id)}><CardMoreIcon /></button>{menuId === skill.id && <div className="sm-card-menu" role="menu">{skill.enabled && skill.userInvocable && onUseSkill && <button className="sm-card-menu-item" onClick={() => { setMenuId(null); useSkill(skill) }}><UseSkillIcon /><span>使用技能</span></button>}<button className="sm-card-menu-item" onClick={() => { setMenuId(null); setSelectedId(skill.id) }}><DocIcon /><span>查看详情</span></button>{skill.scope === 'user' && <><button className="sm-card-menu-item" disabled={busy || !!skill.error} onClick={() => { setMenuId(null); exportPackage(skill) }}><FolderIcon /><span>导出</span></button><button className="sm-card-menu-item sm-card-menu-item--danger" disabled={busy} onClick={() => { setMenuId(null); setPendingRemove([skill]) }}><TrashIcon /><span>卸载技能</span></button></>}</div>}</div><button type="button" className={'sm-card-pin' + (pinned.includes(skill.id) ? ' sm-card-pin--active' : '')} aria-label={pinned.includes(skill.id) ? '取消置顶' : '置顶'} aria-pressed={pinned.includes(skill.id)} onClick={() => pin(skill.id)}>{pinned.includes(skill.id) ? <UnpinIcon /> : <PinIcon />}</button><span className="sm-card-switch-wrap"><Switch skill={skill} disabled={busy} onToggle={() => toggle(skill)} /></span></div>} />
|
||||
</div>)}</div></section> })}</div>}
|
||||
</section>}
|
||||
</div>
|
||||
{importOpen && <ImportModal busy={busy} error={importError} onClose={() => setImportOpen(false)} onImport={(overwrite, kind) => { void importPackage(overwrite, kind) }} />}
|
||||
{current && <Detail skill={current} document={document} busy={busy} error={detailError || error} onClose={() => setSelectedId(null)} onToggle={() => toggle(current)} onUse={onUseSkill ? () => useSkill(current) : undefined} onExport={() => exportPackage(current)} onRemove={() => setPendingRemove([current])} />}
|
||||
{pendingRemove.length > 0 && <RemoveConfirm skills={pendingRemove} busy={busy} dialogRef={removeDialog} onClose={() => setPendingRemove([])} onConfirm={() => { void run(async () => { for (const skill of pendingRemove) await window.ycode.skills.remove(skill.id); setPendingRemove([]); setSelectedId(null); setSelected([]); setNotice('已卸载技能副本') }) }} />}
|
||||
</div>
|
||||
}
|
||||
function documentGlobal(): Document { return window.document }
|
||||
function readPinned(key: string): string[] { try { const values = JSON.parse(localStorage.getItem(key) ?? '{}')?.pinned; return Array.isArray(values) ? values.filter((id): id is string => typeof id === 'string').slice(0, 200) : [] } catch { return [] } }
|
||||
function RemoveConfirm({ skills, busy, dialogRef, onClose, onConfirm }: { skills: SkillRecord[]; busy: boolean; dialogRef: React.RefObject<HTMLDivElement | null>; onClose(): void; onConfirm(): void }): React.JSX.Element {
|
||||
useModalFocus(dialogRef, () => { if (!busy) onClose() })
|
||||
return <div className="sm-update-confirm-modal-mask"><div className="sm-update-confirm-modal" ref={dialogRef} role="dialog" aria-modal="true" aria-label="确认卸载技能"><div className="sm-update-confirm-modal-header"><span>卸载技能</span><button className="sm-update-confirm-modal-close" aria-label="关闭卸载确认" disabled={busy} onClick={onClose}><CloseIcon /></button></div><div className="sm-update-confirm-modal-content"><p className="sm-update-confirm-description">确定卸载{skills.length === 1 ? `「${skills[0].name}」` : `所选 ${skills.length} 项技能`}?仅删除本机导入副本,保留导入源。</p></div><div className="sm-update-confirm-modal-footer"><Button disabled={busy} onClick={onClose}>取消</Button><Button primary disabled={busy} aria-label="确认卸载" onClick={onConfirm}>{busy ? '正在卸载…' : '卸载'}</Button></div></div></div>
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ExpertChip } from './SourceActiveExpertRecovered.mjs'
|
||||
import { ActiveExpertHostContext } from './SourceActiveExpertRuntime'
|
||||
import './SourceActiveExpertSource.css'
|
||||
|
||||
export interface ActiveExpert {
|
||||
id: string
|
||||
name: string
|
||||
profession?: string
|
||||
avatarUrl?: string
|
||||
marketExpertId?: string
|
||||
expertType?: string
|
||||
assistantDisplayName?: string
|
||||
assistantProfession?: string
|
||||
assistantAvatarUrl?: string
|
||||
suppressAutoSummon?: boolean
|
||||
}
|
||||
export interface SourceActiveExpertProps {
|
||||
expert: ActiveExpert | null
|
||||
compact?: boolean
|
||||
disabled?: boolean
|
||||
preparingExpertId?: string
|
||||
/** Change this opaque key whenever the owner/session/draft changes. */
|
||||
scopeKey?: string
|
||||
/** Persist the actual deselection. Reject to restore the previous visible selection. */
|
||||
onRemove(): void | Promise<void>
|
||||
}
|
||||
/** The original chip is a status/removal control; opening the expert menu belongs to the + menu. */
|
||||
export default function SourceActiveExpert(props: SourceActiveExpertProps): React.JSX.Element {
|
||||
return <ActiveExpertOwner key={props.scopeKey ?? 'default'} {...props} />
|
||||
}
|
||||
function ActiveExpertOwner(props: SourceActiveExpertProps): React.JSX.Element {
|
||||
const latest = useRef(props); latest.current = props
|
||||
const alive = useRef(true), removing = useRef(false)
|
||||
const [pending, setPending] = useState(false)
|
||||
const [projection, setProjection] = useState<{ original: ActiveExpert | null; value: ActiveExpert | null } | null>(null)
|
||||
useEffect(() => { alive.current = true; return () => { alive.current = false } }, [])
|
||||
const current = props.expert
|
||||
const visible = projection?.original === current ? projection.value : current
|
||||
const setExpert = useCallback((value: ActiveExpert | null): void => {
|
||||
// Source rollback may finish after an unrelated controlled selection; never replace that newer owner/value.
|
||||
if (!alive.current || latest.current.expert !== current || (latest.current.disabled && value === null)) return
|
||||
setProjection({ original: current, value })
|
||||
}, [current])
|
||||
const onChange = useCallback(async (value: ActiveExpert | null): Promise<void> => {
|
||||
// The parent has already activated real selections. Avoid the source provider's second silent summon.
|
||||
if (value) return
|
||||
if (!alive.current || latest.current.expert !== current || latest.current.disabled || removing.current) throw Error('Expert removal unavailable')
|
||||
removing.current = true; setPending(true)
|
||||
try { await latest.current.onRemove() }
|
||||
finally { removing.current = false; if (alive.current) setPending(false) }
|
||||
}, [current])
|
||||
const disabled = !!props.disabled || pending
|
||||
const host = useMemo(() => ({ expert: visible, disabled, preparingExpertId: props.preparingExpertId, setExpert, onChange }), [visible, disabled, props.preparingExpertId, setExpert, onChange])
|
||||
return <span className="yc-source-active-expert" data-disabled={disabled || undefined} data-expert-owner={props.scopeKey}>
|
||||
<ActiveExpertHostContext.Provider value={host}><ExpertChip compact={props.compact} /></ActiveExpertHostContext.Provider>
|
||||
</span>
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
{
|
||||
"version": "5.8.2",
|
||||
"rootSha256": "035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918",
|
||||
"evidence": [
|
||||
"ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb",
|
||||
"ev_7e9604adb87a3d21570eb7a371001e48b3d316ee0388a03a97e2cb564d03aff7"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"file": "src-DbE1_mgM.js",
|
||||
"sha256": "6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4"
|
||||
},
|
||||
{
|
||||
"file": "src-CBaV_A3S.css",
|
||||
"sha256": "2090ed21045c8f8b0fc388d76424c8751bf18fcd065cbbd5d0ef485163a2e36c",
|
||||
"evidenceId": "ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94"
|
||||
},
|
||||
{
|
||||
"file": "src-CyXHA0IC.css",
|
||||
"sha256": "a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe",
|
||||
"evidenceId": "ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94"
|
||||
}
|
||||
],
|
||||
"bodies": [
|
||||
{
|
||||
"name": "getFloatingLayerState",
|
||||
"start": 372284,
|
||||
"end": 372413,
|
||||
"sha256": "3200be7f8878b496894417b4200f181da0310bd4d9a86538031fff5490019593"
|
||||
},
|
||||
{
|
||||
"name": "getGroupStack",
|
||||
"start": 372413,
|
||||
"end": 372544,
|
||||
"sha256": "0ccb6ed196d7bcd362cc8b99875e3a93dd2b740d220fa4b31ee3a62e7ae0bf30"
|
||||
},
|
||||
{
|
||||
"name": "nextZIndex",
|
||||
"start": 372544,
|
||||
"end": 372662,
|
||||
"sha256": "5c3ae1d84b1c46639e10c27017c242a99d01cdc9ba6d194d673b4a879353b107"
|
||||
},
|
||||
{
|
||||
"name": "useFloatingLayer",
|
||||
"start": 372662,
|
||||
"end": 373108,
|
||||
"sha256": "6ae4ec4b9f303a439b0e1949eb9d3cc08e455e23d7f457bce69f882a63db3bd2"
|
||||
},
|
||||
{
|
||||
"name": "TooltipLayer",
|
||||
"start": 373215,
|
||||
"end": 375023,
|
||||
"sha256": "c55300604aae8517ac952d8b146fc8e109e1fb0eabc4c08d1904de00340cc37a"
|
||||
},
|
||||
{
|
||||
"name": "isPressTransparent",
|
||||
"start": 375104,
|
||||
"end": 375213,
|
||||
"sha256": "63d6e285cee3badd0b4f64841c611e82042581a3261afe88c7a9d9ef684a449a"
|
||||
},
|
||||
{
|
||||
"name": "isHoverSuppressedByPress",
|
||||
"start": 375213,
|
||||
"end": 375298,
|
||||
"sha256": "a8c564a28713639be762ffd255c557d83e8caf74d813cb498136387aedc4cee2"
|
||||
},
|
||||
{
|
||||
"name": "suppressHoverUntilPointerMove",
|
||||
"start": 375298,
|
||||
"end": 375383,
|
||||
"sha256": "d7e98a1e55b12f14f47b8f6cea25f8e58f0b551b357e48e83c54738356f877b2"
|
||||
},
|
||||
{
|
||||
"name": "isElementTruncated",
|
||||
"start": 375383,
|
||||
"end": 375810,
|
||||
"sha256": "8cb03e9d9cf8f34cb6eb8c75235ba95eb2d01e40b6711224a9adcdfe9e39e9f9"
|
||||
},
|
||||
{
|
||||
"name": "isSubtreeTruncated",
|
||||
"start": 375810,
|
||||
"end": 375982,
|
||||
"sha256": "de0934388bb0cc24351701325c0da2408f42f3c22ae7cdd8159b80b20a940405"
|
||||
},
|
||||
{
|
||||
"name": "normalizeVisibleText",
|
||||
"start": 375982,
|
||||
"end": 376056,
|
||||
"sha256": "eddd4f9c021d7177f10e2a847673fb39c5bc06cc12196214de8f992a1b37df50"
|
||||
},
|
||||
{
|
||||
"name": "collectVisibleText",
|
||||
"start": 376056,
|
||||
"end": 376328,
|
||||
"sha256": "0144f46fa724fbe5a762a436d0f8b2b52eb9c5e1ee0c290e14c0cda138746aea"
|
||||
},
|
||||
{
|
||||
"name": "hasVisibleTriggerText",
|
||||
"start": 376328,
|
||||
"end": 376443,
|
||||
"sha256": "cbd4225281de854acf4714bf149fc05bf21b55e5436ec9aef5af069448789d21"
|
||||
},
|
||||
{
|
||||
"name": "hasTruncationStyle",
|
||||
"start": 376443,
|
||||
"end": 376809,
|
||||
"sha256": "324d26170052146a5b03aa30b3660b4c71cad675a72eac9833ce6f3c560e6147"
|
||||
},
|
||||
{
|
||||
"name": "shouldGateOnTruncation",
|
||||
"start": 376809,
|
||||
"end": 377038,
|
||||
"sha256": "08981d22ed572c364e16d781628fbf7b2b6f6749f0531c855225dcf1a6eba7d1"
|
||||
},
|
||||
{
|
||||
"name": "isEmptyContent",
|
||||
"start": 377473,
|
||||
"end": 377531,
|
||||
"sha256": "30195d0df73720cd7e520c098d6047f83ad0f9659f3aca89555e07cecabea68c"
|
||||
},
|
||||
{
|
||||
"name": "resolveTooltipThemeMode",
|
||||
"start": 377531,
|
||||
"end": 377668,
|
||||
"sha256": "5eb9fc6752aa8278774d50c8c3f84c627ac9c1f48fe65cd1ebb7a0ac0e880e7c"
|
||||
},
|
||||
{
|
||||
"name": "isVisibleFocus",
|
||||
"start": 377668,
|
||||
"end": 377803,
|
||||
"sha256": "8e93fc89bd9094a731d411ac94c026eec62ed088c0047cd05892e2ef19a93473"
|
||||
},
|
||||
{
|
||||
"name": "Tooltip$1",
|
||||
"start": 377803,
|
||||
"end": 380607,
|
||||
"sha256": "37d3c069c27d92d89ba7a6c194eff0bcbd56053549f3080f89581791dbab8ea1"
|
||||
},
|
||||
{
|
||||
"name": "LoadingSpinnerIcon",
|
||||
"start": 406658,
|
||||
"end": 407080,
|
||||
"sha256": "c1bb302defac0dfe395adbcd2bba21d09897cd375bae1ee51fc3dee7e033d13e"
|
||||
},
|
||||
{
|
||||
"name": "ChipCloseIcon",
|
||||
"start": 471321,
|
||||
"end": 471715,
|
||||
"sha256": "31f5b1b3de815a9fe0507e0655248028aec8ab023193f86f669969e328b8520d"
|
||||
},
|
||||
{
|
||||
"name": "toNonEmptyString",
|
||||
"start": 1146532,
|
||||
"end": 1146607,
|
||||
"sha256": "ceae5ac5fc26ec684d927f0d86c9cfe9e9d619ecf7e5df760046dc720668fde4"
|
||||
},
|
||||
{
|
||||
"name": "resolveAvatarFallbackSeed",
|
||||
"start": 1146607,
|
||||
"end": 1146738,
|
||||
"sha256": "bc897c063b63c8fb4c99c9e126772ad4cd58c66ff44fff3568546491d68c7383"
|
||||
},
|
||||
{
|
||||
"name": "getAvatarFallbackBackground",
|
||||
"start": 1146738,
|
||||
"end": 1146927,
|
||||
"sha256": "34b4dfc2402ebd8c79f5ca5a4c2f6e1999a7d100d3d81b0e20769b55bdb5ea31"
|
||||
},
|
||||
{
|
||||
"name": "isExpertSelectionDisplayable",
|
||||
"start": 1692572,
|
||||
"end": 1692722,
|
||||
"sha256": "be0f04cb0dcf484cb4fe6d53047e71da39285aad83a6d89b42c0c6867bd04f84"
|
||||
},
|
||||
{
|
||||
"name": "hasText",
|
||||
"start": 1692722,
|
||||
"end": 1692780,
|
||||
"sha256": "ab5e8a1e808c6cf96c8fe32cd769778b9b4b5bf77c5371b11ce173428e5e777d"
|
||||
},
|
||||
{
|
||||
"name": "isPreparingExpert",
|
||||
"start": 1692780,
|
||||
"end": 1692855,
|
||||
"sha256": "18c4a96f5199db655fd5e4dc15fbee72e118334549dbc7078d1977318e72cd11"
|
||||
},
|
||||
{
|
||||
"name": "resolveExpertChipAvatar",
|
||||
"start": 1692855,
|
||||
"end": 1693249,
|
||||
"sha256": "ec6c1e4ea41ea1afa00c1919bef5a3349119b36b31b6a41fe2536c66a76409ac"
|
||||
},
|
||||
{
|
||||
"name": "ExpertAvatar",
|
||||
"start": 1695006,
|
||||
"end": 1695726,
|
||||
"sha256": "fba624a7bcdebd650f4131eb7ae0fcd12694ef82cb884ccf423f7b4a0e87d09b"
|
||||
},
|
||||
{
|
||||
"name": "ExpertChip",
|
||||
"start": 1693365,
|
||||
"end": 1695005,
|
||||
"sha256": "47156dbb21de95c7ad01f0d8f53f626b36dbfc4436007f3a9b06598b3d9886da"
|
||||
}
|
||||
],
|
||||
"adaptations": [
|
||||
"React/JSX singleton and pinned YCode FloatingUI host dependency replace bundled imports; original dependency version is unknown.",
|
||||
"Original useInputBoxStore/useInputProvider/useInputBoxSelector are bound to typed controlled host. Non-null silent preparation is owned by the parent and is a no-op here; remove awaits the actual host and rolls back on failure.",
|
||||
"The original chip has no click-to-open-picker behavior. No new click behavior is introduced.",
|
||||
"Host scopeKey remount prevents stale clear rollback from crossing owners. Optional disabled state blocks removal during host busy/unavailable state.",
|
||||
"Tooltip portal and recovered CSS are scoped to this component; original tooltip function/layer and helper bodies are retained."
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"file": "src/renderer/src/capabilities/SourceActiveExpertRecovered.mjs",
|
||||
"bytes": 13417,
|
||||
"sha256": "aecff2c78c9d11216322fbc55927501fdbeaaf7e4edb1048449b575488a47ede"
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/capabilities/SourceActiveExpertRecovered.d.mts",
|
||||
"bytes": 624,
|
||||
"sha256": "b149e744c99c674b0520b8ac7d3e6a1caf396badb146870eae003032753eaccb"
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/capabilities/SourceActiveExpertSource.css",
|
||||
"bytes": 7357,
|
||||
"sha256": "cdfd89850faa769fe1bb7fad75e44ac5ba70d0e7c61c3bb59e99a9d32da49cb7"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
import type {ComponentType} from 'react';import type {ActiveExpert} from './SourceActiveExpert';export const ExpertChip:ComponentType<{compact?:boolean}>;export const ExpertAvatar:ComponentType<{avatarUrl?:string;name?:string;preparing?:boolean}>;export function isExpertSelectionDisplayable(expert:ActiveExpert):boolean;export function isPreparingExpert(expert:ActiveExpert,id?:string):boolean;export function resolveExpertChipAvatar(expert:ActiveExpert):{avatarUrl?:string;seed:string};export function resolveAvatarFallbackSeed(expert:ActiveExpert):string;export function getAvatarFallbackBackground(name?:string):string;
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as React from 'react';import * as JSX from 'react/jsx-runtime';import {useFloating,offset as D,flip as E,shift as O,arrow as k,autoUpdate as A,useInteractions,useDismiss,useTransitionStyles,FloatingPortal as NativeFloatingPortal} from '@floating-ui/react';
|
||||
import {useInputBoxStore,useInputProvider,useInputBoxSelector,useActiveExpertDisabled} from './SourceActiveExpertRuntime';
|
||||
const oG=React,Ei=React,mi=React,ci=React,Di=JSX,io={1:12,2:16},kM=8,gi=3,_i=8,li=9990,ui=10,di='default',fi='__ycodeActiveExpertFloatingLayer__',yi=4,bi=1000,xi=.1,Si='data-pointer-press-transparent',wi=new Set(['SVG','IMG','VIDEO','CANVAS','AUDIO']),Oi=new Map();let Ci=null;
|
||||
const usePrefixCls=()=>name=>'cr-'+name,clsx=(...values)=>values.flat().filter(Boolean).join(' '),FloatingPortal=({children})=>JSX.jsx(NativeFloatingPortal,{children:JSX.jsx('div',{className:'yc-source-active-expert','data-cr-theme':children?.props?.['data-cr-theme'],children})});
|
||||
function getFloatingLayerState(){let n=globalThis,a=n[fi];return(!a||!(a.groups instanceof Map))&&(n[fi]={groups:new Map}),n[fi]};
|
||||
function getGroupStack(n){let a=getFloatingLayerState(),c=a.groups.get(n);return c instanceof Set||(c=new Set,a.groups.set(n,c)),c};
|
||||
function nextZIndex(n,a,c){let u=getGroupStack(n);if(u.size===0)return a;let d=a;for(let n of u)n>d&&(d=n);return d+c};
|
||||
function useFloatingLayer(n,a){let c=(0,ci.useRef)(a?.base??li),u=(0,ci.useRef)(a?.groupKey??di),d=(0,ci.useRef)(null),[f,p]=(0,ci.useState)(c.current);return(0,ci.useLayoutEffect)(()=>{let f=getGroupStack(u.current);if(!n){d.current!==null&&(f.delete(d.current),d.current=null);return}let m=nextZIndex(u.current,c.current,a?.step??ui);return f.add(m),d.current=m,p(m),()=>{d.current!==null&&(f.delete(d.current),d.current=null)}},[n,a?.step]),f};
|
||||
function TooltipLayer({content:n,placement:a,offsetVal:c,showArrow:u,maxWidth:d,anchorRef:f,floatingId:p,visible:m,disableInteractions:h=!1,interactive:g=!1,tooltipClassName:_,themeMode:v,onInteractiveEnter:y,onInteractiveLeave:b,onExited:x,onRequestClose:S}){let C=usePrefixCls(),w=(0,mi.useRef)(null),[T,j]=(0,mi.useState)(!0),M=useFloatingLayer(T);(0,mi.useEffect)(()=>{j(m)},[m]);let{refs:N,floatingStyles:P,context:F,middlewareData:I}=useFloating({open:T,onOpenChange:(0,mi.useCallback)(n=>{n||S()},[S]),placement:a,middleware:(0,mi.useMemo)(()=>{let n=[D(c+(u?gi:0)),E(),O({padding:8})];return u&&n.push(k({element:w,padding:_i})),n},[c,u]),strategy:`fixed`,whileElementsMounted:A});(0,mi.useLayoutEffect)(()=>{N.setReference(f.current)},[]);let{getFloatingProps:L}=useInteractions([useDismiss(F,{enabled:!h,ancestorScroll:!0})]),{isMounted:R,styles:z}=useTransitionStyles(F,{duration:150,initial:{opacity:0},common:{transitionTimingFunction:`linear`}});if((0,mi.useEffect)(()=>{!T&&!R&&x()},[T,R,x]),!R)return null;let B=I.arrow?.x,V=I.arrow?.y,H=F.placement.split(`-`)[0],ee={top:`bottom`,right:`left`,bottom:`top`,left:`right`}[H];return JSX.jsx(FloatingPortal,{children: JSX.jsxs(`div`,{ref:N.setFloating,className:clsx(`cr-theme`,C(`tooltip`),C(`tooltip--placement-${H}`),g&&C(`tooltip--interactive`),_,`cb-font-size-fixed`),"data-cr-theme":v,style:{...P,...z,zIndex:M,...d==null?null:{maxWidth:`${d}px`}},...L({id:p,role:`tooltip`,onMouseEnter:g?y:void 0,onMouseLeave:g?b:void 0,onFocusCapture:g?y:void 0,onBlurCapture:g?n=>{n.currentTarget.contains(n.relatedTarget)||b?.()}:void 0}),children:[ JSX.jsx(`div`,{className:C(`tooltip__content`),children:n}),u&& JSX.jsx(`div`,{ref:w,className:C(`tooltip__arrow`),style:{left:B==null?``:`${B}px`,top:V==null?``:`${V}px`,[ee]:`-2.5px`}})]})})};
|
||||
function isPressTransparent(n){let a=n;return!a||typeof a.closest!=`function`?!1:a.closest(`[${Si}]`)!==null};
|
||||
function isHoverSuppressedByPress(){return Ci?Date.now()-Ci.at>bi?(Ci=null,!1):!0:!1};
|
||||
function suppressHoverUntilPointerMove(n){Ci={x:n.clientX,y:n.clientY,at:Date.now()}};
|
||||
function isElementTruncated(n){if(!n||n.nodeType!==1||!(`scrollWidth`in n))return!1;if(n.scrollWidth-n.clientWidth>xi||n.scrollHeight-n.clientHeight>xi)return!0;if(n.childElementCount===0&&hasTruncationStyle(n))try{let a=n.ownerDocument.createRange();a.selectNodeContents(n);let c=n.getBoundingClientRect().width,u=c>0?Math.max(c,n.clientWidth):n.clientWidth;return a.getBoundingClientRect().width-u>xi}catch{return!1}return!1};
|
||||
function isSubtreeTruncated(n){if(isElementTruncated(n))return!0;if(!n)return!1;for(let a of Array.from(n.querySelectorAll(`*`)))if(isElementTruncated(a))return!0;return!1};
|
||||
function normalizeVisibleText(n){return(n??``).replace(/\s+/g,` `).trim()};
|
||||
function collectVisibleText(n){if(n.nodeType===3)return n.nodeValue??``;if(n.nodeType!==1)return``;let a=n;if(wi.has(a.tagName.toUpperCase())||a.getAttribute(`aria-hidden`)===`true`)return``;let c=``;for(let n of Array.from(a.childNodes))c+=collectVisibleText(n);return c};
|
||||
function hasVisibleTriggerText(n){return!n||n.nodeType!==1?!1:normalizeVisibleText(collectVisibleText(n)).length>0};
|
||||
function hasTruncationStyle(n){let a=n.ownerDocument?.defaultView;if(typeof a?.getComputedStyle!=`function`)return!1;let c;try{c=a.getComputedStyle(n)}catch{return!1}if(c.textOverflow===`ellipsis`)return!0;let u=c.webkitLineClamp||c.getPropertyValue(`-webkit-line-clamp`);return u&&u!==`none`?!0:c.overflow===`hidden`||c.overflowX===`hidden`||c.overflowY===`hidden`};
|
||||
function shouldGateOnTruncation(n){if(!hasVisibleTriggerText(n)||!n)return!1;if(hasTruncationStyle(n))return!0;for(let a of Array.from(n.querySelectorAll(`*`)))if(hasVisibleTriggerText(a)&&hasTruncationStyle(a))return!0;return!1};
|
||||
function isEmptyContent(n){return n==null||n===``||n===!1};
|
||||
function resolveTooltipThemeMode(n){let a=n?.closest(`.cr-theme`)?.getAttribute(`data-cr-theme`);return a===`dark`||a===`light`?a:void 0};
|
||||
function isVisibleFocus(n){let a=n;if(!a||typeof a.matches!=`function`)return!0;try{return a.matches(`:focus-visible`)}catch{return!0}};
|
||||
function Tooltip$1({content:n,open:a,onOpenChange:c,disableInteractions:u=!1,children:d,placement:f=`top`,offset:p=2,delay:m=200,disabled:h,showArrow:g=!0,maxWidth:_,as:v=`div`,wrapperClassName:y,className:b,wrapperStyle:x,interactive:S=!1,tooltipClassName:C,wrapperProps:w,hideOnClick:T=!0,onlyWhenTruncated:E,exclusiveGroup:D}){let O=(0,Ei.useRef)(null),k=(0,Ei.useRef)(null),A=(0,Ei.useRef)(null),j=(0,Ei.useId)(),[M,N]=(0,Ei.useState)(!1),[P,F]=(0,Ei.useState)(!1),I=a??M,L=!h&&!isEmptyContent(n),R=(0,Ei.useCallback)(()=>{k.current!=null&&(window.clearTimeout(k.current),k.current=null)},[]),z=(0,Ei.useCallback)(()=>{A.current!=null&&(window.clearTimeout(A.current),A.current=null)},[]),B=(0,Ei.useCallback)(n=>{a===void 0&&N(n),c?.(n)},[a,c]);(0,Ei.useEffect)(()=>{if(!I||!D)return;let n=Oi.get(D)??new Set;n.forEach(n=>n());let closeSelf=()=>B(!1);return n.add(closeSelf),Oi.set(D,n),()=>{n.delete(closeSelf),n.size===0&&Oi.delete(D)}},[I,D,B]);let V=(0,Ei.useCallback)(()=>{R(),z(),!(T&&isHoverSuppressedByPress())&&((E??shouldGateOnTruncation(O.current))&&!isSubtreeTruncated(O.current)||(F(!0),B(!0)))},[z,R,T,E,B]),H=(0,Ei.useCallback)(()=>{R(),z(),B(!1)},[z,R,B]),ee=(0,Ei.useCallback)(()=>{z(),A.current=window.setTimeout(()=>{A.current=null,B(!1)},120)},[z,B]),te=(0,Ei.useCallback)(()=>F(!1),[]),ne=(0,Ei.useCallback)(()=>{if(z(),k.current==null){if(m<=0){V();return}k.current=window.setTimeout(()=>{k.current=null,V()},m)}},[z,m,V]),re=(0,Ei.useCallback)(()=>{if(S){ee();return}H()},[H,S,ee]),ie=(0,Ei.useCallback)(n=>{z(),isVisibleFocus(n.target)&&V()},[z,V]),ae=(0,Ei.useCallback)(()=>{if(S){ee();return}H()},[H,S,ee]),oe=(0,Ei.useCallback)(n=>{T&&(suppressHoverUntilPointerMove(n),H())},[H,T]);return(0,Ei.useEffect)(()=>()=>{R(),z()},[z,R]),(0,Ei.useEffect)(()=>{L&&I&&F(!0)},[L,I]),(0,Ei.useEffect)(()=>{u&&R()},[R,u]),(0,Ei.useEffect)(()=>{let n=O.current;if(L&&!u&&n)return n.addEventListener(`mouseenter`,ne),n.addEventListener(`mouseleave`,re),n.addEventListener(`focusin`,ie),n.addEventListener(`focusout`,ae),n.addEventListener(`pointerdown`,oe),()=>{n.removeEventListener(`mouseenter`,ne),n.removeEventListener(`mouseleave`,re),n.removeEventListener(`focusin`,ie),n.removeEventListener(`focusout`,ae),n.removeEventListener(`pointerdown`,oe)}},[u,L,ie,ae,ne,re,oe]),(0,Ei.useEffect)(()=>{!L&&I&&H()},[L,H,I]),L? JSX.jsxs(Di.Fragment,{children:[ JSX.jsx(v,{ref:O,className:clsx(y,b),style:x,"aria-describedby":I?j:void 0,...w,children:d}),P&& JSX.jsx(TooltipLayer,{content:n,placement:f,offsetVal:p,showArrow:g,maxWidth:_,anchorRef:O,floatingId:j,visible:I,disableInteractions:u,interactive:S,tooltipClassName:C,themeMode:resolveTooltipThemeMode(O.current),onInteractiveEnter:z,onInteractiveLeave:ee,onExited:te,onRequestClose:H})]}): JSX.jsx(Di.Fragment,{children:d})};
|
||||
function LoadingSpinnerIcon({size:n=`1`}){let a=io[n]??Number(n)??12;return JSX.jsxs(`svg`,{width:a,height:a,viewBox:`0 0 12 12`,fill:`none`,style:{animation:`cr-input-spin 0.8s linear infinite`},children:[ JSX.jsx(`circle`,{cx:`6`,cy:`6`,r:`5`,stroke:`currentColor`,strokeOpacity:`0.25`,strokeWidth:`1.5`}), JSX.jsx(`path`,{d:`M11 6a5 5 0 0 0-5-5`,stroke:`currentColor`,strokeWidth:`1.5`,strokeLinecap:`round`})]})};
|
||||
const ChipCloseIcon=()=> JSX.jsx(`svg`,{xmlns:`http://www.w3.org/2000/svg`,width:`16`,height:`16`,viewBox:`0 0 16 16`,fill:`none`,children: JSX.jsx(`path`,{fill:`currentColor`,fillRule:`evenodd`,transform:`matrix(1 0 0 1 2.5651 2.5651)`,d:`M6.3046 5.4349L10.8698 10L10 10.8698L5.4349 6.3046L0.8698 10.8698L0 10L4.5651 5.4349L0 0.8698L0.8698 0L5.4349 4.5651L10 0L10.8698 0.8698L6.3046 5.4349Z`})});
|
||||
function toNonEmptyString(n){if(typeof n==`string`)return n.trim()||void 0};
|
||||
function resolveAvatarFallbackSeed(n){return toNonEmptyString(n.profession)||toNonEmptyString(n.name)||toNonEmptyString(n.id)||`?`};
|
||||
function getAvatarFallbackBackground(n){let a=toNonEmptyString(n)??`?`,c=0;for(let n=0;n<a.length;n+=1)c=(c<<5)-c+a.charCodeAt(n)|0;return`var(--cr-avatar-fallback-bg-${Math.abs(c)%kM+1})`};
|
||||
function isExpertSelectionDisplayable(n){if(n.avatarUrl&&n.avatarUrl.trim())return!0;let a=n.name?.trim();return!!(a&&a!==n.id||n.profession?.trim())};
|
||||
function hasText(n){return typeof n==`string`&&!!n.trim()};
|
||||
function isPreparingExpert(n,a){return a?n.id===a||n.marketExpertId===a:!1};
|
||||
function resolveExpertChipAvatar(n){return n.expertType===`team`&&(hasText(n.assistantDisplayName)||hasText(n.assistantProfession)||hasText(n.assistantAvatarUrl))?{avatarUrl:hasText(n.assistantAvatarUrl)?n.assistantAvatarUrl:void 0,seed:resolveAvatarFallbackSeed({id:n.id,name:n.assistantDisplayName,profession:n.assistantProfession})}:{avatarUrl:n.avatarUrl,seed:resolveAvatarFallbackSeed(n)}};
|
||||
const ExpertAvatar=({avatarUrl:n,name:a,preparing:c})=>{let u=usePrefixCls(),[d,f]=(0,oG.useState)(null),p=!!n&&d!==n,m=clsx(u(`expert-chip-avatar`),c&&u(`expert-chip-avatar--preparing`));if(p)return JSX.jsxs(`span`,{className:m,"aria-hidden":`true`,children:[ JSX.jsx(`img`,{src:n,alt:``,onError:()=>f(n??null)}),c? JSX.jsx(`span`,{className:u(`expert-chip-avatar-spinner`),children: JSX.jsx(LoadingSpinnerIcon,{size:`1`})}):null]});let h=(a||`?`).charAt(0).toUpperCase();return JSX.jsxs(`span`,{className:m,"aria-hidden":`true`,style:{background:getAvatarFallbackBackground(a)},children:[h,c? JSX.jsx(`span`,{className:u(`expert-chip-avatar-spinner`),children: JSX.jsx(LoadingSpinnerIcon,{size:`1`})}):null]})};
|
||||
const ExpertChip=({compact:n})=>{let blocked=useActiveExpertDisabled(),a=usePrefixCls(),c=useInputBoxStore(),u=useInputProvider(`expert`),d=useInputBoxSelector(n=>n.draft.selections.expert),f=(0,oG.useRef)(void 0),p=u?.onChange;(0,oG.useEffect)(()=>{let n=d?.id;if(!n){f.current=void 0;return}if(d.suppressAutoSummon){f.current=void 0;return}f.current!==n&&(f.current=n,Promise.resolve(p?.(d,{silent:!0})).catch(()=>void 0))},[d,p]);let m=(0,oG.useRef)(null),h=(0,oG.useCallback)(async n=>{n.stopPropagation(),c.api.setExpert(null);try{await u?.onChange?.(null)}catch{c.api.setExpert(d??null)}},[c,u,d]);if(!d)return m.current=null,null;let g;if(isExpertSelectionDisplayable(d))m.current=d,g=d;else if(m.current)g=m.current;else return null;let _=g.profession?.trim(),v=g.name?.trim(),y=_||(v&&v!==g.id?v:``)||``,b=resolveExpertChipAvatar(g),x=isPreparingExpert(d,u?.preparingExpertId);return JSX.jsx(Tooltip$1,{content:n&&y?y:void 0,as:`span`,wrapperClassName:a(`chip-tooltip`),children: JSX.jsxs(`div`,{className:clsx(a(`chip-base`),a(`chip-base--static`),n&&a(`chip-base--compact`)),role:`status`,"aria-busy":x||void 0,children:[ JSX.jsx(`span`,{className:clsx(a(`chip-icon`),a(`chip-icon--default`)),"aria-hidden":`true`,children: JSX.jsx(ExpertAvatar,{avatarUrl:b.avatarUrl,name:b.seed,preparing:x})}), JSX.jsx(`button`,{type:`button`,className:clsx(a(`chip-icon`),a(`chip-icon--hover`),a(`chip-close-btn`)),onClick:h,disabled:blocked,"aria-label":`\u6E05\u9664\u4E13\u5BB6: ${y||g.id}`,children: JSX.jsx(ChipCloseIcon,{})}),!n&&y? JSX.jsx(Tooltip$1,{content:y,as:`span`,wrapperClassName:a(`chip-label`),onlyWhenTruncated:!0,children: JSX.jsx(`span`,{children:y})}):null]})})};
|
||||
if(typeof document!=='undefined'){document.addEventListener('pointerdown',n=>{Ci=isPressTransparent(n.target)?null:{x:n.clientX,y:n.clientY,at:Date.now()}},true);document.addEventListener('pointermove',n=>{if(Ci&&(Math.abs(n.clientX-Ci.x)>yi||Math.abs(n.clientY-Ci.y)>yi))Ci=null},true)}
|
||||
export {ExpertChip,ExpertAvatar,resolveAvatarFallbackSeed,getAvatarFallbackBackground,isExpertSelectionDisplayable,isPreparingExpert,resolveExpertChipAvatar};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
import type { ActiveExpert } from './SourceActiveExpert'
|
||||
|
||||
interface Host {
|
||||
expert: ActiveExpert | null
|
||||
disabled: boolean
|
||||
preparingExpertId?: string
|
||||
setExpert(expert: ActiveExpert | null): void
|
||||
onChange(expert: ActiveExpert | null): Promise<void>
|
||||
}
|
||||
export const ActiveExpertHostContext = createContext<Host | null>(null)
|
||||
function useHost(): Host { const host = useContext(ActiveExpertHostContext); if (!host) throw Error('Active expert host missing'); return host }
|
||||
export function useInputBoxStore(): { api: { setExpert: Host['setExpert'] } } { const host = useHost(); return { api: { setExpert: host.setExpert } } }
|
||||
export function useInputProvider(_name: string): { onChange: Host['onChange']; preparingExpertId?: string } { const host = useHost(); return { onChange: host.onChange, preparingExpertId: host.preparingExpertId } }
|
||||
export function useInputBoxSelector<T>(selector: (state: { draft: { selections: { expert: ActiveExpert | null } } }) => T): T { const host = useHost(); return selector({ draft: { selections: { expert: host.expert } } }) }
|
||||
export function useActiveExpertDisabled(): boolean { return useHost().disabled }
|
||||
@@ -0,0 +1,39 @@
|
||||
/* Source ExpertChip/Tooltip declarations recovered from fixed REA CSS; selectors scoped. */
|
||||
.yc-source-active-expert{--cr-internal-text-primary:#ffffffeb;--cr-internal-text-secondary:#ffffffa6;--cr-internal-tooltip-bg:#ffffffbf;--cr-internal-tooltip-text-color:#000;--cr-internal-avatar-fallback-bg-1:#4f5667;--cr-internal-avatar-fallback-bg-2:#5b526d;--cr-internal-avatar-fallback-bg-3:#6a4f65;--cr-internal-avatar-fallback-bg-4:#6b5b48;--cr-internal-avatar-fallback-bg-5:#4f665c;--cr-internal-avatar-fallback-bg-6:#4b5f73;--cr-internal-avatar-fallback-bg-7:#674f52;--cr-internal-avatar-fallback-bg-8:#4f6667;--cr-internal-chip-hover-bg:#ffffff14;--cr-internal-focus-border:#4da3e0;--cr-internal-color-on-accent:#fff;--cr-text-primary:var(--cr-internal-text-primary);--cr-text-secondary:var(--cr-internal-text-secondary);--cr-tooltip-bg:var(--cr-internal-tooltip-bg);--cr-tooltip-text-color:var(--cr-internal-tooltip-text-color);--cr-avatar-fallback-bg-1:var(--cr-internal-avatar-fallback-bg-1);--cr-avatar-fallback-bg-2:var(--cr-internal-avatar-fallback-bg-2);--cr-avatar-fallback-bg-3:var(--cr-internal-avatar-fallback-bg-3);--cr-avatar-fallback-bg-4:var(--cr-internal-avatar-fallback-bg-4);--cr-avatar-fallback-bg-5:var(--cr-internal-avatar-fallback-bg-5);--cr-avatar-fallback-bg-6:var(--cr-internal-avatar-fallback-bg-6);--cr-avatar-fallback-bg-7:var(--cr-internal-avatar-fallback-bg-7);--cr-avatar-fallback-bg-8:var(--cr-internal-avatar-fallback-bg-8);--cr-chip-hover-bg:var(--cr-internal-chip-hover-bg);--cr-focus-border:var(--cr-internal-focus-border);--cr-color-on-accent:var(--cr-internal-color-on-accent)}
|
||||
.cr-theme[data-cr-theme=light] .yc-source-active-expert,.yc-source-active-expert[data-cr-theme=light],.light .yc-source-active-expert,[data-theme=light] .yc-source-active-expert{--cr-internal-text-primary:#000000e6;--cr-internal-text-secondary:#000000b3;--cr-internal-tooltip-bg:#000000bf;--cr-internal-tooltip-text-color:#fff;--cr-internal-avatar-fallback-bg-1:#6366f1;--cr-internal-avatar-fallback-bg-2:#8b5cf6;--cr-internal-avatar-fallback-bg-3:#ec4899;--cr-internal-avatar-fallback-bg-4:#f59e0b;--cr-internal-avatar-fallback-bg-5:#10b981;--cr-internal-avatar-fallback-bg-6:#3b82f6;--cr-internal-avatar-fallback-bg-7:#ef4444;--cr-internal-avatar-fallback-bg-8:#14b8a6;--cr-internal-chip-hover-bg:#0000000f;--cr-internal-focus-border:#007fd4;--cr-internal-color-on-accent:#fff}
|
||||
.yc-source-active-expert .cr-tooltip{background:var(--cr-tooltip-bg);max-width:320px;color:var(--cr-tooltip-text-color);text-align:left;white-space:normal;word-break:break-word;backdrop-filter:blur(4px);pointer-events:none;border-radius:8px;padding:4px 8px;font-size:12px;font-weight:400;line-height:20px}
|
||||
@media (prefers-reduced-motion:reduce){.yc-source-active-expert .cr-tooltip{backdrop-filter:none}}
|
||||
.yc-source-active-expert .cr-tooltip--interactive{pointer-events:auto}
|
||||
.yc-source-active-expert .cr-tooltip__arrow{border:0 solid #0000;width:0;height:0;position:absolute}
|
||||
.yc-source-active-expert .cr-tooltip--placement-top .cr-tooltip__arrow{border-width:3px 5px 0;border-top-color:var(--cr-tooltip-bg)}
|
||||
.yc-source-active-expert .cr-tooltip--placement-bottom .cr-tooltip__arrow{border-width:0 5px 3px;border-bottom-color:var(--cr-tooltip-bg)}
|
||||
.yc-source-active-expert .cr-tooltip--placement-left .cr-tooltip__arrow{border-width:5px 0 5px 3px;border-left-color:var(--cr-tooltip-bg)}
|
||||
.yc-source-active-expert .cr-tooltip--placement-right .cr-tooltip__arrow{border-width:5px 3px 5px 0;border-right-color:var(--cr-tooltip-bg)}
|
||||
.yc-source-active-expert .cr-chip-tooltip{flex-shrink:0;display:inline-flex}
|
||||
.yc-source-active-expert .cr-chip-base{cursor:pointer;background:0 0;border:none;border-radius:999px;flex-shrink:0;align-items:center;gap:4px;height:32px;padding:0 8px;transition:background-color .15s;display:inline-flex}
|
||||
.yc-source-active-expert .cr-chip-base:focus-visible{outline:2px solid var(--cr-focus-border,#007fd4);outline-offset:-2px}
|
||||
.yc-source-active-expert .cr-chip-base--static{cursor:default}
|
||||
.yc-source-active-expert .cr-chip-base--static:focus-visible{outline:none}
|
||||
.yc-source-active-expert .cr-chip-base--static:hover,.yc-source-active-expert .cr-chip-base--static:focus-within{background:var(--cr-chip-hover-bg,#0000000f)}
|
||||
.yc-source-active-expert .cr-chip-base--static:hover .cr-chip-icon--default,.yc-source-active-expert .cr-chip-base--static:focus-within .cr-chip-icon--default{display:none}
|
||||
.yc-source-active-expert .cr-chip-base--static:hover .cr-chip-icon--hover,.yc-source-active-expert .cr-chip-base--static:focus-within .cr-chip-icon--hover{display:inline-flex}
|
||||
.yc-source-active-expert .cr-chip-base--static:hover .cr-chip-label,.yc-source-active-expert .cr-chip-base--static:focus-within .cr-chip-label{color:var(--cr-text-primary,#000000e6)}
|
||||
.yc-source-active-expert .cr-chip-base--hovered{background:var(--cr-chip-hover-bg,#0000000f)}
|
||||
.yc-source-active-expert .cr-chip-base--compact{border-radius:50%;gap:0}
|
||||
.yc-source-active-expert .cr-chip-icon{width:16px;height:16px;color:var(--cr-text-secondary,#00000080);flex-shrink:0;justify-content:center;align-items:center;display:flex}
|
||||
.yc-source-active-expert .cr-chip-icon--default{display:inline-flex}
|
||||
.yc-source-active-expert .cr-chip-icon--hover{color:var(--cr-text-primary,#000000e6);display:none}
|
||||
.yc-source-active-expert .cr-chip-icon--hovered{color:var(--cr-text-primary,#000000e6)}
|
||||
.yc-source-active-expert .cr-chip-close-btn{cursor:pointer;color:inherit;background:0 0;border:none;padding:0}
|
||||
.yc-source-active-expert .cr-chip-close-btn:focus-visible{outline:2px solid var(--cr-focus-border,#007fd4);outline-offset:1px;border-radius:4px}
|
||||
.yc-source-active-expert .cr-chip-label{color:var(--cr-text-secondary,#00000080);white-space:nowrap;user-select:none;text-overflow:ellipsis;max-width:140px;font-size:13px;font-weight:500;line-height:20px;overflow:hidden}
|
||||
.yc-source-active-expert .cr-chip-label--hovered{color:var(--cr-text-primary,#000000e6)}
|
||||
.yc-source-active-expert .cr-expert-chip-avatar{width:16px;height:16px;color:var(--cr-color-on-accent);text-transform:uppercase;border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;font-size:9px;font-weight:600;display:inline-flex;position:relative;overflow:hidden}
|
||||
.yc-source-active-expert .cr-expert-chip-avatar img{object-fit:cover;width:100%;height:100%;display:block}
|
||||
.yc-source-active-expert .cr-expert-chip-avatar--preparing img{opacity:.45}
|
||||
.yc-source-active-expert .cr-expert-chip-avatar-spinner{color:var(--cr-color-on-accent,#fff);pointer-events:none;justify-content:center;align-items:center;display:inline-flex;position:absolute;inset:0}
|
||||
.yc-source-active-expert .cr-expert-chip-avatar-spinner svg{width:10px;height:10px;animation:.8s linear infinite cr-expert-chip-spin}
|
||||
@keyframes cr-input-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}
|
||||
@keyframes cr-expert-chip-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}
|
||||
/* Explicit YCode host busy state, separate from original source declarations. */
|
||||
.yc-source-active-expert{display:inline-flex;flex-shrink:0}.yc-source-active-expert[data-disabled=true] .cr-chip-base--static .cr-chip-icon--default{display:inline-flex}.yc-source-active-expert[data-disabled=true] .cr-chip-base--static .cr-chip-icon--hover{display:none}.yc-source-active-expert[data-disabled=true] .cr-chip-base--static:hover{background:transparent}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { autoUpdate, flip, offset, shift, size, useFloating } from '@floating-ui/react'
|
||||
import { ExpertsSubmenu } from './expert-picker-source.mjs'
|
||||
import { ExpertPickerHostContext, readExpertPickerMemory, type ExpertPickerMemory } from './expert-picker-runtime'
|
||||
import './expert-picker-source.css'
|
||||
import './expert-picker-host.css'
|
||||
|
||||
export interface ExpertPickerItem { id: string; name: string; profession?: string; avatarUrl?: string; description?: string; disabled?: boolean; searchText?: string }
|
||||
export interface SourceExpertPickerProps<T extends ExpertPickerItem = ExpertPickerItem> {
|
||||
items: readonly T[]
|
||||
selectedId?: string | null
|
||||
busy?: boolean
|
||||
error?: string
|
||||
onSelect(item: T): Promise<void>
|
||||
onManage(): void
|
||||
onClose(): void
|
||||
anchor: HTMLElement | null
|
||||
/** The source add menu owns placement, pointer dismissal and focus for embedded children. */
|
||||
embedded?: boolean
|
||||
/** Include the actual account/space/session scope in this opaque key. */
|
||||
scopeKey: string
|
||||
}
|
||||
const emptyMemory: ExpertPickerMemory = { pinned: [], recent: [] }
|
||||
export default function SourceExpertPicker<T extends ExpertPickerItem>(props: SourceExpertPickerProps<T>): React.JSX.Element {
|
||||
const key = 'ycode.experts.menu.v1:' + props.scopeKey
|
||||
const [memoryState, setMemory] = useState(() => ({ key, value: readExpertPickerMemory(key) })), [pending, setPending] = useState(false), [localError, setLocalError] = useState('')
|
||||
const memory = memoryState.key === key ? memoryState.value : emptyMemory
|
||||
const latest = useRef(props); latest.current = props
|
||||
const pendingRef = useRef(false), alive = useRef(true), epoch = useRef(0), wrapper = useRef<HTMLDivElement>(null)
|
||||
const busy = !!props.busy || pending
|
||||
const items = useMemo(() => { const seen = new Set<string>(); return props.items.filter(item => item.id && !seen.has(item.id) && !!seen.add(item.id)) }, [props.items])
|
||||
useEffect(() => { setMemory({ key, value: readExpertPickerMemory(key) }); setLocalError(''); setPending(false); pendingRef.current = false; epoch.current++ }, [key])
|
||||
useEffect(() => { alive.current = true; return () => { alive.current = false; epoch.current++ } }, [])
|
||||
const remember = useCallback((value: ExpertPickerMemory): void => { setMemory({ key, value }); try { localStorage.setItem(key, JSON.stringify(value)) } catch { /* Optional menu history never prevents a real activation. */ } }, [key])
|
||||
const togglePin = useCallback((item: ExpertPickerItem): void => { if (pendingRef.current || latest.current.busy || item.disabled) return; remember({ ...memory, pinned: memory.pinned.includes(item.id) ? memory.pinned.filter(id => id !== item.id) : [item.id, ...memory.pinned].slice(0, 100) }) }, [remember, memory])
|
||||
const select = useCallback(async (item: ExpertPickerItem): Promise<boolean> => {
|
||||
const host = latest.current, actual = host.items.find(value => value.id === item.id)
|
||||
if (!actual || actual.disabled || pendingRef.current || host.busy) return false
|
||||
const scope = host.scopeKey, token = epoch.current
|
||||
pendingRef.current = true; setPending(true); setLocalError('')
|
||||
try {
|
||||
await host.onSelect(actual)
|
||||
if (alive.current && epoch.current === token && latest.current.scopeKey === scope) { remember({ ...memory, recent: [actual.id, ...memory.recent.filter(id => id !== actual.id)].slice(0, 30) }); host.onClose() }
|
||||
} catch (reason) { if (alive.current && epoch.current === token && latest.current.scopeKey === scope) setLocalError(reason instanceof Error ? reason.message : String(reason)) }
|
||||
finally { if (alive.current && epoch.current === token && latest.current.scopeKey === scope) { pendingRef.current = false; setPending(false) } }
|
||||
// Source search handler interprets false as keep-open. This host closes only after successful activation above.
|
||||
return false
|
||||
}, [remember, memory])
|
||||
const closeFromSource = useCallback(() => { if (!pendingRef.current && !latest.current.busy) latest.current.onClose() }, [])
|
||||
const manage = useCallback(() => { if (!pendingRef.current && !latest.current.busy) latest.current.onManage() }, [])
|
||||
const floating = useFloating({ open: !props.embedded, placement: 'right-start', strategy: 'fixed', whileElementsMounted: autoUpdate,
|
||||
middleware: [offset(4), flip({ fallbackPlacements: ['left-start', 'right', 'left'], padding: 8 }), shift({ padding: 8 }), size({ padding: 8, apply: ({ availableWidth, availableHeight, elements }) => { Object.assign(elements.floating.style, { maxWidth: `${Math.max(availableWidth, 160)}px`, maxHeight: `${Math.max(availableHeight, 200)}px` }) } })] })
|
||||
useLayoutEffect(() => { if (!props.embedded) floating.refs.setReference(props.anchor ?? { getBoundingClientRect: () => new DOMRect(8, Math.max(8, window.innerHeight - 64), 24, 24) }) }, [props.anchor, props.embedded, floating.refs])
|
||||
useLayoutEffect(() => { if (props.embedded) return; const frame = requestAnimationFrame(() => wrapper.current?.querySelector<HTMLInputElement>('input')?.focus()); return () => cancelAnimationFrame(frame) }, [props.scopeKey, props.embedded])
|
||||
useEffect(() => {
|
||||
if (props.embedded) return
|
||||
const outside = (event: PointerEvent): void => { const target = event.target as Node; if (!wrapper.current?.contains(target) && !latest.current.anchor?.contains(target)) latest.current.onClose() }
|
||||
document.addEventListener('pointerdown', outside)
|
||||
return () => { document.removeEventListener('pointerdown', outside); if (latest.current.anchor?.isConnected) latest.current.anchor.focus() }
|
||||
}, [props.embedded])
|
||||
const host = useMemo(() => ({ items, memory, busy, error: props.error, togglePin }), [items, memory, busy, props.error, togglePin])
|
||||
const handleKeyboard = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
if (event.nativeEvent.isComposing || event.keyCode === 229) return
|
||||
if (props.embedded) return
|
||||
if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); props.onClose(); return }
|
||||
if (event.key === 'Enter' || event.key === ' ') { const row = (event.target as Element).closest<HTMLElement>('[role="menuitem"]'); if (row && event.target === row && row.getAttribute('aria-disabled') !== 'true') { event.preventDefault(); row.click() }; return }
|
||||
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return
|
||||
if (event.target instanceof HTMLInputElement && ['Home', 'End'].includes(event.key)) return
|
||||
const rows = [...(wrapper.current?.querySelectorAll<HTMLElement>('[role="menuitem"]:not([aria-disabled="true"])') ?? [])]
|
||||
if (!rows.length) return
|
||||
event.preventDefault(); const index = rows.indexOf(document.activeElement as HTMLElement), next = event.key === 'Home' ? 0 : event.key === 'End' ? rows.length - 1 : index < 0 ? event.key === 'ArrowUp' ? rows.length - 1 : 0 : (index + (event.key === 'ArrowUp' ? -1 : 1) + rows.length) % rows.length
|
||||
rows[next]?.focus(); rows[next]?.scrollIntoView({ block: 'nearest' })
|
||||
}
|
||||
const content = <div className="yc-expert-picker" data-embedded={props.embedded || undefined} data-scope={props.scopeKey} ref={node => { wrapper.current = node; if (!props.embedded) floating.refs.setFloating(node) }} style={props.embedded ? undefined : { ...floating.floatingStyles, zIndex: 400 }} onKeyDown={handleKeyboard} aria-label="选择专家" aria-busy={busy}>
|
||||
<ExpertPickerHostContext.Provider value={host}><ExpertsSubmenu key={props.scopeKey} injection={{ selectedExpertId: props.selectedId, reselectSelected: true, expertScope: props.scopeKey, recentExperts: items, loadFailed: !!props.error, onSelect: busy ? undefined : select, onSummonSearchResult: busy ? undefined : select, onSummonMore: manage }} onCloseMenu={closeFromSource} /></ExpertPickerHostContext.Provider>
|
||||
{(props.error || localError) && <div className="yc-expert-picker-error" role="alert">{localError || props.error}</div>}
|
||||
</div>
|
||||
return props.embedded ? content : createPortal(content, props.anchor?.closest('.chat-pane') ?? document.querySelector('.chat-pane') ?? document.body)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import type { SkillRecord } from '@shared/capabilities'
|
||||
import { SkillsIcon, PinIcon, UnpinIcon, SearchIcon, ClearFilledIcon, SettingsIcon } from './SkillSourceIcons'
|
||||
import './skill-picker-source.css'
|
||||
import { UploadIcon } from '../workbuddy/composer/add-menu/source.mjs'
|
||||
import './composer-menu-embedded.css'
|
||||
|
||||
interface Props { cwd: string; anchor?: HTMLElement | null; embedded?: boolean; onSelect(skill: SkillRecord): void; onClose(): void; onImport?(): void }
|
||||
interface Memory { pinned: string[]; recent: string[] }
|
||||
const empty: Memory = { pinned: [], recent: [] }
|
||||
export default function SourceSkillPicker(props: Props): React.JSX.Element {
|
||||
// A new space owns fresh list/query/error state and its own persisted pin/recent memory.
|
||||
return <SkillPickerScope key={props.cwd.replaceAll('\\', '/').toLowerCase()} {...props} />
|
||||
}
|
||||
function SkillPickerScope({ cwd, anchor, embedded = false, onSelect, onClose, onImport }: Props): React.JSX.Element {
|
||||
const [items, setItems] = useState<SkillRecord[]>([]), [query, setQuery] = useState(''), [loading, setLoading] = useState(true), [error, setError] = useState('')
|
||||
const key = 'ycode.skills.menu.v1:' + cwd.replaceAll('\\', '/').toLowerCase()
|
||||
const [memory, setMemory] = useState<Memory>(() => { try { const value = JSON.parse(localStorage.getItem(key) ?? 'null'); return Array.isArray(value?.pinned) && Array.isArray(value?.recent) ? value : empty } catch { return empty } })
|
||||
const menu = useRef<HTMLDivElement>(null), search = useRef<HTMLInputElement>(null), composing = useRef(false)
|
||||
const latest = useRef({ anchor, onClose }); latest.current = { anchor, onClose }
|
||||
const [position, setPosition] = useState({ left: 0, top: 0 })
|
||||
useEffect(() => { let active = true; setItems([]); setError(''); setLoading(true); void window.ycode.skills.list(cwd).then(value => { if (active) setItems(value.filter(item => item.enabled && item.userInvocable)) }).catch(reason => { if (active) setError(String(reason.message ?? reason)) }).finally(() => { if (active) setLoading(false) }); return () => { active = false } }, [cwd])
|
||||
useEffect(() => { const frame = requestAnimationFrame(() => search.current?.focus()); return () => cancelAnimationFrame(frame) }, [])
|
||||
useEffect(() => {
|
||||
if (embedded) return
|
||||
const rect = anchor?.getBoundingClientRect()
|
||||
setPosition({ left: Math.max(8, Math.min(rect?.left ?? 300, window.innerWidth - 328)), top: Math.max(38, (rect?.top ?? window.innerHeight - 70) - 398) })
|
||||
const outside = (event: PointerEvent): void => { if (!menu.current?.contains(event.target as Node) && !latest.current.anchor?.contains(event.target as Node)) latest.current.onClose() }
|
||||
const resize = (): void => latest.current.onClose()
|
||||
document.addEventListener('pointerdown', outside); window.addEventListener('resize', resize)
|
||||
return () => { document.removeEventListener('pointerdown', outside); window.removeEventListener('resize', resize) }
|
||||
}, [anchor, embedded])
|
||||
const results = useMemo(() => items.filter(item => `${item.name}\n${item.description}`.toLowerCase().includes(query.trim().toLowerCase())).sort((a, b) => {
|
||||
const pinned = Number(memory.pinned.includes(b.id)) - Number(memory.pinned.includes(a.id))
|
||||
if (pinned) return pinned
|
||||
const rank = (id: string): number => { const index = memory.recent.indexOf(id); return index < 0 ? 1000 : index }
|
||||
return rank(a.id) - rank(b.id) || a.name.localeCompare(b.name)
|
||||
}), [items, query, memory])
|
||||
const remember = (next: Memory): void => { setMemory(next); try { localStorage.setItem(key, JSON.stringify(next)) } catch { /* Optional menu memory must not block adding a skill. */ } }
|
||||
const choose = (skill: SkillRecord): void => { remember({ ...memory, recent: [skill.id, ...memory.recent.filter(id => id !== skill.id)].slice(0, 30) }); onSelect(skill); onClose() }
|
||||
const content = <div className="yc-skill-picker" data-embedded={embedded || undefined}><div ref={menu} className="_panel_1mrhu_14" style={embedded ? undefined : { position: 'fixed', zIndex: 400, width: 320, maxHeight: 392, maxWidth: 'calc(100vw - 16px)', ...position }} role="menu" aria-label="选择技能" onKeyDown={event => {
|
||||
if (composing.current || event.nativeEvent.isComposing || event.keyCode === 229) { event.stopPropagation(); return }
|
||||
if (event.key === 'Escape') {
|
||||
if (query) { event.preventDefault(); event.stopPropagation(); setQuery(''); search.current?.focus(); return }
|
||||
if (!embedded) { event.preventDefault(); event.stopPropagation(); onClose() }
|
||||
return
|
||||
}
|
||||
if (event.target instanceof HTMLInputElement && ['Home', 'End'].includes(event.key)) { event.stopPropagation(); return }
|
||||
if (event.target instanceof HTMLButtonElement && ['Enter', ' '].includes(event.key)) { event.stopPropagation(); return }
|
||||
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) { event.preventDefault(); event.stopPropagation(); const rows = [...(menu.current?.querySelectorAll<HTMLElement>('[data-skill-option]') ?? [])]; if (!rows.length) return; let index = rows.indexOf(document.activeElement as HTMLElement); index = event.key === 'Home' ? 0 : event.key === 'End' ? rows.length - 1 : index < 0 ? event.key === 'ArrowUp' ? rows.length - 1 : 0 : (index + (event.key === 'ArrowUp' ? -1 : 1) + rows.length) % rows.length; rows[index]?.focus(); rows[index]?.scrollIntoView({ block: 'nearest' }) }
|
||||
}}>
|
||||
<div className="_search_1mrhu_59"><SearchIcon className="_searchIcon_1mrhu_71" /><input ref={search} autoFocus className="_searchInput_1mrhu_81" placeholder="搜索技能" value={query} onCompositionStart={() => { composing.current = true }} onCompositionEnd={() => { composing.current = false }} onChange={event => setQuery(event.target.value)} />{query && <button type="button" className="_searchClear_1mrhu_98" aria-label="清空搜索" onClick={() => { setQuery(''); search.current?.focus() }}><ClearFilledIcon /></button>}</div>
|
||||
<div className="_body_1mrhu_49">{loading || error || !results.length ? <div className="_empty_1mrhu_32" role={error ? 'alert' : undefined}>{loading ? '加载中…' : error || (query ? '没有找到相关技能' : '暂无可用技能')}</div> : <div className="_list_1mrhu_31">{results.map(skill => {
|
||||
const pinned = memory.pinned.includes(skill.id)
|
||||
return <div key={skill.id} data-skill-option data-skill-id={skill.id} className="_item_s54xa_16" role="menuitem" tabIndex={0} onClick={() => choose(skill)} onKeyDown={event => { if (event.target !== event.currentTarget) return; if (composing.current || event.nativeEvent.isComposing || event.keyCode === 229) { event.stopPropagation(); return }; if (['Enter', ' '].includes(event.key)) { event.preventDefault(); event.stopPropagation(); choose(skill) } }}><span className="_itemIcon_s54xa_49"><SkillsIcon /></span><div className="_itemBody_s54xa_63"><div className="_itemTitle_s54xa_72"><span className="_itemTitleText_s54xa_86">{skill.name}</span></div><div className="_itemDesc_s54xa_196">{skill.description}</div></div><button type="button" className={'_pinBtn_s54xa_219' + (pinned ? ' _pinned_s54xa_217' : '')} aria-pressed={pinned} aria-label={pinned ? '取消置顶' : '置顶'} onClick={event => { event.stopPropagation(); remember({ ...memory, pinned: pinned ? memory.pinned.filter(id => id !== skill.id) : [...memory.pinned, skill.id] }) }}><span className="_pinIconRest_s54xa_216"><PinIcon /></span><span className="_pinIconHover_s54xa_216"><UnpinIcon /></span></button></div>
|
||||
})}</div>}</div>
|
||||
<div className="_divider_1mrhu_25" /><div className="_actionGroup_1mrhu_224">{onImport && <button type="button" className="_actionRow_1mrhu_231" onClick={() => { onClose(); onImport() }}><span className="_actionIcon_1mrhu_257"><UploadIcon /></span><span className="_actionLabel_1mrhu_271">从本地添加技能</span></button>}<button type="button" className="_actionRow_1mrhu_231" onClick={() => { onClose(); window.dispatchEvent(new CustomEvent('ycode:open-capabilities', { detail: { tab: 'skills' } })) }}><span className="_actionIcon_1mrhu_257"><SettingsIcon /></span><span className="_actionLabel_1mrhu_271">管理技能</span></button></div>
|
||||
</div></div>
|
||||
return embedded ? content : createPortal(content, anchor?.closest('.chat-pane') ?? document.querySelector('.chat-pane') ?? document.body)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,9 @@
|
||||
import type {ComponentType,ReactNode} from 'react';
|
||||
export type ExpertLocaleText=string|{zh?:string;en?:string;[locale:string]:string|undefined};
|
||||
export interface CreatedExpertRecord {id:string;name:string;displayName?:ExpertLocaleText;profession?:ExpertLocaleText;description?:ExpertLocaleText;expertType?:'expert'|'team'|string;expertRootDir?:string;avatar?:string;version?:string;updatedAt?:number|string;tags?:unknown[];skills?:unknown[];[field:string]:unknown}
|
||||
export interface CreatedExpertPreviewProps {expert:CreatedExpertRecord;onClose():void;onSummon(id:string,unused?:unknown,initPrompt?:string):boolean|void}
|
||||
export interface CreatedExpertSourceHost {locale?:'zh'|'en';openPath?(path:string):Promise<string|void>;resolveAssetUrl?(value:string):string;notify?(message:string,type?:string):void;renderPreview?(props:CreatedExpertPreviewProps):ReactNode}
|
||||
export const CreatedExpertSourceProvider:ComponentType<{host?:CreatedExpertSourceHost;children?:ReactNode}>;
|
||||
export interface CreatedExpertCallbacks {onUse(id:string,initPrompt?:string):void|Promise<void>;onDelete?(id:string):boolean|void|Promise<boolean|void>;onEdit?(expert:CreatedExpertRecord):void|Promise<void>}
|
||||
export const CreatedExpertCard:ComponentType<CreatedExpertCallbacks&{expert:CreatedExpertRecord;isMenuOpen?:boolean;onToggleMenu?(id:string,open?:boolean):void;highlightKeyword?:string}>;
|
||||
export const CreatedExpertsPanel:ComponentType<CreatedExpertCallbacks&{createdExperts:CreatedExpertRecord[];activeTab:'expert'|'team';onTabChange(tab:'expert'|'team'):void;searchKeyword?:string;toolbarExtra?:ReactNode}>;
|
||||
@@ -0,0 +1,112 @@
|
||||
// Generated by scripts/rea/recover-created-experts.mjs; pinned REA source identities are in the adjacent manifest.
|
||||
import * as React from 'react';
|
||||
import * as JSX from 'react/jsx-runtime';
|
||||
import {createPortal} from 'react-dom';
|
||||
import {computePosition,offset,flip,shift,autoUpdate} from '@floating-ui/react';
|
||||
import {ExpertCard as ExpertCard$1,ExpertListEmptyState,filterExpertsByKeyword} from './source-ui.mjs';
|
||||
import '../created-expert-source.css';
|
||||
const dictionaries={"en":{"expertSection":"Experts","teamSection":"Expert Teams","more":"Show more","common.cancel":"Cancel","noTeamSearchResult":"No expert teams matching \"{keyword}\" found. Try another keyword.","noSearchResult":"No experts matching \"{keyword}\" found. Try another keyword.","myExperts.created.emptyTitle":"No experts created yet","myExperts.created.teamEmptyTitle":"No expert teams created yet","myExperts.created.deleteLocalAheadConfirmContent":"This expert has local updates that are not uploaded to cloud. It will be removed from this device, and you can choose whether to delete the cloud record as well.","myExperts.created.deleteCloudAheadConfirmContent":"The cloud version has newer updates. Deleting from this device will not affect the cloud record unless you also choose to delete it from cloud.","myExperts.created.deleteBothConfirmContent":"This expert will be removed from this device. It is also synced to cloud, and you can choose whether to delete the cloud record as well.","myExperts.created.deleteConfirmContent":"Are you sure you want to delete this expert? This cannot be undone.","myExperts.cloud.deleteConfirmContent":"This expert only exists in cloud. Deleting it cannot be undone.","myExperts.cloud.deleteConfirmTitle":"Delete Cloud Expert","myExperts.created.deleteConfirmTitle":"Delete Expert","myExperts.created.deleteCloudOption":"Also delete the cloud expert","myExperts.cloud.deleting":"Deleting...","myExperts.cloud.delete":"Delete cloud expert","myExperts.created.deleteConfirmBtn":"Delete","share.copyFailed":"Copy failed, please try again","myExperts.cloud.conflictConfirm":"The cloud version also changed. This will overwrite cloud content with this device. Continue?","myExperts.cloud.downloadFailed":"Failed to download expert","myExperts.created.openFolderFailed":"Failed to open folder","myExperts.created.ctxView":"View Expert","myExperts.created.ctxEdit":"Edit Expert","myExperts.created.ctxShare":"Share","myExperts.cloud.downloading":"Downloading...","myExperts.cloud.download":"Download to device","myExperts.cloud.uploading":"Uploading...","myExperts.cloud.upload":"Upload to cloud","myExperts.created.ctxOpenFolder":"Open Folder","myExperts.created.ctxDelete":"Delete","myExperts.cloudUpload.modal.quotaUnavailable":"Unable to get the cloud Expert quota. Please try again later.","myExperts.cloud.uploadAll":"Upload all to cloud"},"zh":{"expertSection":"专家","teamSection":"专家团","more":"显示更多","common.cancel":"取消","noTeamSearchResult":"没有找到与「{keyword}」匹配的专家团,试试其他关键词","noSearchResult":"没有找到与「{keyword}」匹配的专家,试试其他关键词","myExperts.created.emptyTitle":"还没有创建任何专家","myExperts.created.teamEmptyTitle":"还没有创建任何专家团","myExperts.created.deleteLocalAheadConfirmContent":"本机专家有未同步到云端的更新。删除后,本机专家将无法恢复,你可以选择是否同时删除云端记录。","myExperts.created.deleteCloudAheadConfirmContent":"云端存在更新版本。删除本机专家不会影响云端记录,如需彻底删除,请勾选同时删除云端专家。","myExperts.created.deleteBothConfirmContent":"删除后,本机专家将无法恢复。该专家也已同步到云端,你可以选择是否同时删除云端记录。","myExperts.created.deleteConfirmContent":"确定要删除这个专家吗?删除后无法恢复。","myExperts.cloud.deleteConfirmContent":"该专家仅存在于云端,删除后无法恢复。","myExperts.cloud.deleteConfirmTitle":"删除云端专家","myExperts.created.deleteConfirmTitle":"删除专家","myExperts.created.deleteCloudOption":"同时删除云端专家","myExperts.cloud.deleting":"删除中...","myExperts.cloud.delete":"删除云端专家","myExperts.created.deleteConfirmBtn":"删除","share.copyFailed":"复制失败,请重试","myExperts.cloud.conflictConfirm":"云端也有变化,本次会以本机内容覆盖云端,是否继续?","myExperts.cloud.downloadFailed":"下载专家失败","myExperts.created.openFolderFailed":"打开文件夹失败","myExperts.created.ctxView":"查看专家","myExperts.created.ctxEdit":"修改专家","myExperts.created.ctxShare":"分享","myExperts.cloud.downloading":"下载中...","myExperts.cloud.download":"下载到本机","myExperts.cloud.uploading":"上传中...","myExperts.cloud.upload":"上传到云端","myExperts.created.ctxOpenFolder":"打开文件夹","myExperts.created.ctxDelete":"删除","myExperts.cloudUpload.modal.quotaUnavailable":"暂无法获取云端专家配额,请稍后重试。","myExperts.cloud.uploadAll":"一键上传到云端"}};
|
||||
const EI={"row":"_row_ki2oo_1","tabs":"_tabs_ki2oo_8","actions":"_actions_ki2oo_14","tab":"_tab_ki2oo_8","active":"_active_ki2oo_42","sortGroup":"_sortGroup_ki2oo_45","sortBtn":"_sortBtn_ki2oo_57"},WS={"dialogOverlay":"_dialogOverlay_peiew_7","dialogContainer":"_dialogContainer_peiew_21","dialogOverlayContained":"_dialogOverlayContained_peiew_28","dialogContent":"_dialogContent_peiew_56","dialogClose":"_dialogClose_peiew_64","dialogHeader":"_dialogHeader_peiew_72","dialogDetailContent":"_dialogDetailContent_peiew_92","dialogFooter":"_dialogFooter_peiew_116","dialogFooterAlignStart":"_dialogFooterAlignStart_peiew_123","dialogFooterAlignCenter":"_dialogFooterAlignCenter_peiew_127","footerLeftSlot":"_footerLeftSlot_peiew_131","footerButtons":"_footerButtons_peiew_136","cancelButtonStatic":"_cancelButtonStatic_peiew_153","footerButtonsAlignStart":"_footerButtonsAlignStart_peiew_160","footerButtonsAlignCenter":"_footerButtonsAlignCenter_peiew_164","dontAskAgain":"_dontAskAgain_peiew_168","checkbox":"_checkbox_peiew_177","checkboxLabel":"_checkboxLabel_peiew_185","dialogContainerWorkbuddy":"_dialogContainerWorkbuddy_peiew_193"};
|
||||
const safeAsset=value=>/^(?:data:image\/(?:png|jpeg|webp|gif|svg\+xml);|blob:|file:|ycode:)/i.test(value||'')?value:'';
|
||||
const defaultRef={current:{}};
|
||||
function PortalScope({children}){
|
||||
const [theme,setTheme]=React.useState(()=>document.documentElement.dataset.theme==='dark'?'dark':'light');
|
||||
React.useEffect(()=>{const observer=new MutationObserver(()=>setTheme(document.documentElement.dataset.theme==='dark'?'dark':'light'));observer.observe(document.documentElement,{attributes:true,attributeFilter:['data-theme']});return()=>observer.disconnect()},[]);
|
||||
return JSX.jsx('div',{className:'yc-capability-center yc-created-expert-source expert-center-'+theme,style:{display:'contents'},children});
|
||||
}
|
||||
const PortalHost={createPortal:(children,container)=>createPortal(JSX.jsx(PortalScope,{children}),container)};
|
||||
function HostPopover({trigger,open,onOpenChange,disabled,placement='bottom-start',offsetDistance=4,className='',style,children,role='menu',ariaLabel,onFloatingMouseEnter,onFloatingMouseLeave}){
|
||||
const anchor=React.useRef(null),floating=React.useRef(null),id=React.useId(),[position,setPosition]=React.useState({left:0,top:0,visibility:'hidden'}),[phase,setPhase]=React.useState('initial'),[present,setPresent]=React.useState(open);
|
||||
React.useLayoutEffect(()=>{if(open){setPresent(true);setPhase('initial');const raf=requestAnimationFrame(()=>setPhase('open'));return()=>cancelAnimationFrame(raf)}setPhase('close');const timer=setTimeout(()=>setPresent(false),window.matchMedia('(prefers-reduced-motion: reduce)').matches?1:150);return()=>clearTimeout(timer)},[open]);
|
||||
React.useLayoutEffect(()=>{if(!present||!anchor.current||!floating.current)return;let cancelled=false;const update=()=>computePosition(anchor.current,floating.current,{strategy:'fixed',placement,middleware:[offset(offsetDistance),flip(),shift({padding:8})]}).then(({x,y})=>{if(!cancelled)setPosition({left:x,top:y,visibility:'visible'})});const stop=autoUpdate(anchor.current,floating.current,update);return()=>{cancelled=true;stop()}},[present,placement,offsetDistance]);
|
||||
React.useEffect(()=>{if(!open)return;const close=e=>{if(!anchor.current?.contains(e.target)&&!floating.current?.contains(e.target))onOpenChange?.(false)};const key=e=>{if(e.key==='Escape'){e.stopPropagation();onOpenChange?.(false);anchor.current?.focus()}if(['ArrowDown','ArrowUp','Home','End'].includes(e.key)&&floating.current?.contains(document.activeElement)){const items=[...floating.current.querySelectorAll('[role=menuitem]:not([aria-disabled=true])')];if(!items.length)return;e.preventDefault();let index=items.indexOf(document.activeElement);index=e.key==='Home'?0:e.key==='End'?items.length-1:(index+(e.key==='ArrowDown'?1:-1)+items.length)%items.length;items[index]?.focus()}};document.addEventListener('pointerdown',close,true);document.addEventListener('keydown',key,true);return()=>{document.removeEventListener('pointerdown',close,true);document.removeEventListener('keydown',key,true)}},[open,onOpenChange]);
|
||||
const probe=React.cloneElement(trigger,{ref:anchor,'aria-expanded':!!open,'aria-haspopup':role,'aria-controls':open?id:undefined,onClick:e=>{trigger.props.onClick?.(e);if(!disabled)onOpenChange?.(!open)},onKeyDown:e=>{trigger.props.onKeyDown?.(e);if(['ArrowDown','ArrowUp'].includes(e.key)&&!disabled){e.preventDefault();onOpenChange?.(true);requestAnimationFrame(()=>floating.current?.querySelector('[role=menuitem]')?.focus())}}});
|
||||
return JSX.jsxs(JSX.Fragment,{children:[probe,present&&PortalHost.createPortal(JSX.jsx('div',{ref:floating,id,role,'aria-label':ariaLabel,className:'wb-popover wb-popover--placement-bottom wb-popover--expand-'+phase+' '+className,style:{...style,...position,position:'fixed',zIndex:100010,transformOrigin:'top left'},onClick:e=>e.stopPropagation(),onMouseEnter:onFloatingMouseEnter,onMouseLeave:onFloatingMouseLeave,children}),document.body)]});
|
||||
}
|
||||
function createBindings(hostRef){
|
||||
const host=()=>hostRef.current;
|
||||
function useLocale$1(){return host().locale==='en'?'en':'zh'}
|
||||
function useExpertI18n(){const locale=useLocale$1();return React.useCallback((key,values)=>{let text=dictionaries[locale]?.[key]??dictionaries.zh[key]??key;for(const[k,v]of Object.entries(values??{}))text=text.replaceAll('{'+k+'}',String(v));return text},[locale])}
|
||||
const J=({message,type})=>host().notify?.(message,type),Ua={warning:message=>J({message,type:'warning'})};
|
||||
const unavailable=()=>Promise.resolve({success:false,errorMessage:'云端专家服务暂不可用'});
|
||||
const resetSecurityState=()=>{},resolveSecurityDialog=()=>{};
|
||||
const useShareExpert=()=>({sharing:false,securityBlocked:false,shareExpert:unavailable,securityDialog:null,resetSecurityState,resolveSecurityDialog});
|
||||
const ic=()=>false,oc=()=>false;
|
||||
const al=()=>({adapter:{},reporter:{reportEvent:()=>{},Events:{}},wb:{platform:{type:'desktop'},shell:{openPath:async value=>{if(!host().openPath)throw Error('打开文件夹暂不可用');return host().openPath(value)}}}});
|
||||
const resolveExpertAssetUrl=value=>host().resolveAssetUrl?host().resolveAssetUrl(value):safeAsset(value);
|
||||
const useExpertAvatar=value=>({finalAvatarUrl:value});
|
||||
const Tooltip=({content,children})=>React.cloneElement(children,content?{title:typeof content==='string'?content:undefined}:{});
|
||||
const Popover=HostPopover,Az=160,yn={sm:14,md:16,lg:20,xl:24},hn=()=>null;
|
||||
const VirtualDropdownMenuList=()=>null,DropdownFooter=()=>null,ML=()=>null,L_=()=>null,Fs=()=>null,uc=()=>null;
|
||||
const ExpertCloudSyncBadge=()=>null,ExpertShareModal=()=>null,CreatedExpertCloudUploadConfirmModal=()=>null;
|
||||
const openExpertCloudUploadFlow=unavailable,shouldSkipExpertCloudUploadConfirm=()=>false,setSkipExpertCloudUploadConfirm=()=>{};
|
||||
const getCreateUploadSlotCount=()=>0,getQuotaExceededMessage=()=>'',createInitialUploadItemResults=()=>[],getBatchUploadItemResults=()=>[];
|
||||
const Uu={default:(...values)=>values.flatMap(value=>typeof value==='string'?[value]:value&&typeof value==='object'?Object.keys(value).filter(key=>value[key]):[]).filter(Boolean).join(' ')};
|
||||
const Z={'action.close':'关闭'};
|
||||
const RR=JSX;
|
||||
const ZF=JSX;
|
||||
const nxt=React;
|
||||
const JI=JSX;
|
||||
const LR=React;
|
||||
const zR=React;
|
||||
const BR=JSX;
|
||||
const Xz=React;
|
||||
const Zz=JSX;
|
||||
const Oz=React;
|
||||
const kz=JSX;
|
||||
const Mz=React;
|
||||
const _n=React;
|
||||
const vn=JSX;
|
||||
const KS=React;
|
||||
const JS=JSX;
|
||||
const Wu=React;
|
||||
const Gu=JSX;
|
||||
const Lm=JSX;
|
||||
const Ou=React;
|
||||
const ku=JSX;
|
||||
const vF=React;
|
||||
const yF=JSX;
|
||||
const Ml=React;
|
||||
const Nl=JSX;
|
||||
const Sd=React;
|
||||
const Cd=JSX;
|
||||
const Yl=React;
|
||||
const Xl=JSX;
|
||||
const dv=React;
|
||||
const fv=JSX;
|
||||
const qS=PortalHost;
|
||||
function resolveCardI18nText(e,t){return e?typeof e==`string`?e:e[t]||e.zh||e.en||``:``}
|
||||
function resolveShareExpertName(e,t){return resolveCardI18nText(e.profession,t)||resolveCardI18nText(e.displayName,t)||resolveCardI18nText(e.name,t)||e.id}
|
||||
function resolveLocalDeleteContentKey(e){return e===`local_ahead`?`myExperts.created.deleteLocalAheadConfirmContent`:e===`cloud_ahead`?`myExperts.created.deleteCloudAheadConfirmContent`:e===`both`?`myExperts.created.deleteBothConfirmContent`:`myExperts.created.deleteConfirmContent`}
|
||||
const MoreDotsIcon$1=()=>(0,RR.jsxs)(`svg`,{width:`16`,height:`16`,viewBox:`0 0 24 24`,fill:`currentColor`,"aria-hidden":`true`,children:[(0,RR.jsx)(`circle`,{cx:`5`,cy:`12`,r:`1.5`}),(0,RR.jsx)(`circle`,{cx:`12`,cy:`12`,r:`1.5`}),(0,RR.jsx)(`circle`,{cx:`19`,cy:`12`,r:`1.5`})]});
|
||||
const CloudUploadIcon=({size:e=16,...t})=>(0,ZF.jsx)(`svg`,{width:e,height:e,viewBox:`0 0 16 16`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,"aria-hidden":`true`,...t,children:(0,ZF.jsx)(`path`,{fill:`currentColor`,fillRule:`evenodd`,transform:`translate(0.399994 0.399963)`,d:`M4.6 13.2Q2.6947 13.2 1.3474 11.8526Q0 10.5053 0 8.6Q0 7.0261 0.9642 5.7822Q1.7842 4.724 3.0098 4.2817Q3.1149 2.5799 4.3474 1.3474Q5.6947 0 7.6 0Q9.5053 0 10.8526 1.3474Q12.0861 2.5808 12.1904 4.2817Q13.4159 4.7241 14.2359 5.7823Q15.2 7.0261 15.2 8.6Q15.2 10.5053 13.8526 11.8526Q12.5053 13.2 10.6 13.2L10.6 12Q12.0082 12 13.0041 11.0041Q14 10.0082 14 8.6Q14 7.4367 13.2874 6.5173Q12.5749 5.5979 11.4483 5.3075Q11.2415 5.2583 11.1174 5.0857Q10.987 4.9178 10.9984 4.7056Q10.9993 4.6812 10.9995 4.6569L10.9998 4.6287L11 4.6Q11 3.1918 10.0041 2.1959Q9.0082 1.2 7.6 1.2Q6.1918 1.2 5.1959 2.1959Q4.2 3.1918 4.2 4.5971L4.2001 4.6252L4.2004 4.6562Q4.2007 4.6811 4.2016 4.7056Q4.213 4.9178 4.0826 5.0857Q3.9585 5.2583 3.7517 5.3075Q2.6251 5.5979 1.9126 6.5174Q1.2 7.4367 1.2 8.6Q1.2 10.0082 2.1959 11.0041Q3.1918 12 4.6 12L4.6 13.2ZM10.1758 9.5243L8.2 7.5485L8.2 14.6L7 14.6L7 7.5485L5.0243 9.5243L4.1758 8.6758L6.4686 6.3828L6.4783 6.3732Q6.7924 6.059 6.9327 5.9525Q7.2556 5.7071 7.6 5.7071Q7.9444 5.7071 8.2673 5.9525Q8.4076 6.059 8.7217 6.3732L8.7314 6.3828L11.0243 8.6758L10.1758 9.5243Z`})});
|
||||
function CreatedExpertDeleteDialog(e){let{visible:t,cloudSync:n,canDeleteCloud:r=!1,deleteCloudChecked:i,deleting:a=!1,t:o,onDeleteCloudCheckedChange:s,onCancel:c,onConfirm:l}=e,u=n?.presence===`cloud_only`,d=r&&!u,f=(0,nxt.useMemo)(()=>u?`myExperts.cloud.deleteConfirmContent`:resolveLocalDeleteContentKey(n?.presence),[n?.presence,u]);return(0,JI.jsx)(Ie,{visible:t,showCloseButton:!0,title:o(u?`myExperts.cloud.deleteConfirmTitle`:`myExperts.created.deleteConfirmTitle`),content:(0,JI.jsxs)(`div`,{className:`ec-delete-confirm-content`,children:[(0,JI.jsx)(`div`,{children:o(f)}),d&&(0,JI.jsxs)(`label`,{className:`ec-delete-confirm-cloud-option`,children:[(0,JI.jsx)(`input`,{type:`checkbox`,checked:i,disabled:a,onChange:e=>s(e.currentTarget.checked)}),(0,JI.jsx)(`span`,{children:o(`myExperts.created.deleteCloudOption`)})]})]}),confirmText:o(a?`myExperts.cloud.deleting`:u?`myExperts.cloud.delete`:`myExperts.created.deleteConfirmBtn`),cancelText:o(`common.cancel`),confirmVariant:`danger`,confirmLoading:a,confirmDisabled:a,disableOverlayClose:a,onClose:c,onConfirm:l})}
|
||||
const CreatedExpertCard=({expert:e,onUse:t,onDelete:n,onEdit:r,onShare:i,onCloudUpload:a,onCloudDownload:o,onCloudDelete:s,categories:c,isMenuOpen:l,onToggleMenu:u,highlightKeyword:d})=>{let f=useExpertI18n(),p=useLocale$1(),{adapter:m,reporter:h,wb:g}=al(),{reportEvent:v,Events:y}=h,b=ic(),{sharing:x,securityBlocked:S,shareExpert:C,securityDialog:w,resolveSecurityDialog:D,resetSecurityState:O}=useShareExpert(m),[k,A]=(0,LR.useState)(!1),[j,M]=(0,LR.useState)(!1),[N,P]=(0,LR.useState)(!1),[F,I]=(0,LR.useState)(!1),[L,R]=(0,LR.useState)(!1),[z,B]=(0,LR.useState)(!1),[V,H]=(0,LR.useState)(),[U,W]=(0,LR.useState)(!1),[ee,te]=(0,LR.useState)(!1),{finalAvatarUrl:ne}=useExpertAvatar((0,LR.useMemo)(()=>resolveExpertAssetUrl(e.avatar||``),[e.avatar])),re=e,ie=e.expertType===`team`,ae=oc(),G=ie||ae!==!0?void 0:e.cloudSync,oe=G?.presence===`cloud_only`,se=!!a&&!!e.expertRootDir&&G?.cloudStatus!==`checking`&&(G?.presence===`local_only`||G?.presence===`local_ahead`),ce=(oe||G?.presence===`cloud_ahead`)&&G?.cloudStatus===`ready`&&!!G.cloudUid&&!!o,le=(G?.presence===`both`||G?.presence===`local_ahead`||G?.presence===`cloud_ahead`)&&!!G?.cloudUid&&!!s,K=oe&&!!G?.cloudUid&&!!s,ue=!oe&&!!e.expertRootDir,de=ue&&g.platform.type===`desktop`,fe=K||ue&&!!n,pe=(0,LR.useMemo)(()=>{let t=e.expertType===`team`?{...re,operationalTag:void 0}:re;if(resolveCardI18nText(t.profession,p))return t;let n={[p]:resolveCardI18nText(t.displayName,p)||t.name};return{...t,profession:n}},[e.expertType,re,p]);(0,LR.useEffect)(()=>{O(),H(void 0),B(!1)},[e.id,e.expertRootDir,e.updatedAt,O]);let me=(0,LR.useCallback)(t=>{v(y.ExpertUploadSuccess,{id:e.id,name:t,expertType:e.expertType===`team`?`team`:`custom`,triggerSource:`my_experts_share`,version:typeof e.version==`string`?e.version:``})},[y,e.expertType,e.id,e.version,v]),he=(0,LR.useCallback)(async e=>{let t=await C(e);return t.success&&t.shareCode&&me(e.expertName),t},[me,C]),ge=(0,LR.useCallback)(()=>{r?.(e)},[e,r]),_e=(0,LR.useCallback)(async()=>{if(i?.(e),!e.expertRootDir){J({message:f(`share.copyFailed`),type:`plain`});return}O(),H(void 0),B(!1),R(!0);let t=await he({expertRootDir:e.expertRootDir,expertId:e.id,expertName:resolveShareExpertName(e,p)});if(t.success&&t.shareCode){H(t.shareCode),B(!0),D(!1);return}if(!t.errorKey?.startsWith(`security_`)){J({message:t.errorMessage||f(`share.copyFailed`),type:`plain`}),R(!1);return}(t.errorKey===`security_risk_cancelled`||t.errorKey===`security_check_cancelled`)&&R(!1)},[e,he,p,i,O,D,f]),ve=(0,LR.useCallback)(async()=>{if(!se||U||!a)return;let t=G?.syncReason===`conflict_local_wins`;if(!t||typeof window>`u`||window.confirm(f(`myExperts.cloud.conflictConfirm`))){W(!0);try{await openExpertCloudUploadFlow({expert:e,t:f,skipOrdinaryConfirm:shouldSkipExpertCloudUploadConfirm(),onOrdinaryConfirm:e=>setSkipExpertCloudUploadConfirm(void 0,e),upload:n=>Promise.resolve(a(e.id,t,n,G?.presence===`local_ahead`?`update`:`create`)).then(e=>e??{success:!0})})}finally{W(!1)}}},[se,G?.syncReason,U,e,a,f]),ye=(0,LR.useCallback)(async()=>{await ve()},[ve]),be=(0,LR.useCallback)(async(e={})=>{if(ee)return!1;if(!G?.cloudUid||!o)return J({message:f(`myExperts.cloud.downloadFailed`),type:`plain`}),!1;if(G.cloudStatus!==`ready`)return J({message:G.checkError||f(`myExperts.cloud.downloadFailed`),type:`plain`}),!1;if(!ce)return!1;te(!0);try{return await o(G.cloudUid,G.cloud,e)!==!1}finally{te(!1)}},[ce,ee,G?.checkError,G?.cloud,G?.cloudStatus,G?.cloudUid,o,f]),xe=(0,LR.useCallback)(async(e={})=>ie||!oe?!0:be(e),[be,oe,ie]),Se=(0,LR.useCallback)(async()=>{await be({silent:!1})},[be]),Ce=U||ee,we=se||ce,Te=(0,LR.useCallback)(async e=>{if(e.stopPropagation(),!Ce){if(G?.presence===`local_only`||G?.presence===`local_ahead`){await ve();return}(G?.presence===`cloud_only`||G?.presence===`cloud_ahead`)&&await be({silent:!1})}},[Ce,G?.presence,be,ve]),Ee=(0,LR.useCallback)(()=>{P(oe),M(!0)},[oe]),De=(0,LR.useCallback)(async()=>{if(e.expertRootDir&&g.platform.type===`desktop`)try{let t=await g.shell.openPath(e.expertRootDir);if(t)throw Error(t)}catch(e){console.error(`[CreatedExpertCard] openFolder failed:`,e),J({message:f(`myExperts.created.openFolderFailed`),type:`warning`})}},[e.expertRootDir,f,g]),Oe=(0,LR.useCallback)(async()=>{await xe({silent:!0})&&A(!0)},[xe]),ke=(0,LR.useCallback)(async()=>{await xe({silent:!0})&&A(!0)},[xe]),Ae=(0,LR.useCallback)(async()=>{if(F)return;let t=G?.cloudUid,r=!!t&&!!s&&(oe||N);I(!0);try{if(!oe){if(!n||await n(e.id)===!1)return;M(!1),P(!1)}if(r&&s&&t){let e=await s(t);if(oe&&e===!1)return}oe&&(M(!1),P(!1))}finally{I(!1)}},[G?.cloudUid,N,F,e.id,oe,s,n]),je=(0,LR.useCallback)(()=>{F||(M(!1),P(!1))},[F]),Me=(0,LR.useCallback)(e=>{let t=w?.type;D(e),(!e||t===`blocked`||t===`timeout`)&&R(!1)},[D,w?.type]),Ne=(0,LR.useCallback)(()=>{x||(O(),H(void 0),B(!1),R(!1))},[O,x]),Pe=(0,LR.useCallback)((e,n,r)=>(t(e,r),!0),[t]),Fe=(0,LR.useCallback)(e=>resolveExpertAssetUrl(e),[]),Ie=(0,LR.useMemo)(()=>{let e=[{key:`view`,label:f(`myExperts.created.ctxView`),icon:Line truncated
|
||||
const CreatedExpertsPanel=({createdExperts:e,onUse:t,activeTab:n,onTabChange:r,onDelete:i,onEdit:a,onCloudUpload:o,onCloudDownload:s,onCloudDelete:c,onCloudCheckUploadQuota:l,onCloudUploadMany:u,categories:d,searchKeyword:f=``,toolbarExtra:p})=>{let m=useExpertI18n(),h=useLocale$1(),[g,v]=(0,zR.useState)(null),[y,b]=(0,zR.useState)(!1),[x,S]=(0,zR.useState)(!1),[C,w]=(0,zR.useState)(`confirm`),[D,O]=(0,zR.useState)([]),[k,A]=(0,zR.useState)([]),[j,M]=(0,zR.useState)(null),[N,P]=(0,zR.useState)(null),[F,I]=(0,zR.useState)(!1),L=(0,zR.useMemo)(()=>e.filter(e=>e.expertType!==`team`),[e]),R=(0,zR.useMemo)(()=>e.filter(e=>e.expertType===`team`),[e]),z=n===`team`?R:L,B=(0,zR.useMemo)(()=>filterExpertsByKeyword(z,f,h),[z,h,f]),V=n===`team`,H=f.trim().length>0,U=H&&B.length===0,W=!H&&z.length===0,ee=(0,zR.useMemo)(()=>V?[]:z.filter(e=>e.expertRootDir&&e.cloudSync?.cloudStatus!==`checking`&&!e.cloudSync?.invalidLocal&&(e.cloudSync?.presence===`local_only`||e.cloudSync?.presence===`local_ahead`)),[z,V]),te=(0,zR.useMemo)(()=>ee.map(e=>e.id),[ee]),ne=(0,zR.useMemo)(()=>ee.filter(e=>e.cloudSync?.syncReason===`conflict_local_wins`).length,[ee]),re=(0,zR.useMemo)(()=>D.filter(e=>e.cloudSync?.syncReason===`conflict_local_wins`).length,[D]),ie=!!u&&te.length>0,ae=ie&&!y,G=(0,zR.useCallback)((e,t)=>{v(n=>t===!0?e:t===!1?n===e?null:n:n===e?null:e)},[]),oe=(0,zR.useCallback)(e=>{r(e),v(null)},[r]);(0,zR.useEffect)(()=>{v(null)},[f]);let se=(0,zR.useCallback)(()=>{S(!1),w(`confirm`),O([]),A([]),M(null),P(null),I(!1)},[]),ce=(0,zR.useCallback)(()=>{if(!ae)return;let e=getCreateUploadSlotCount(ee);(async()=>{if(e>0){if(!l){Ua.warning(m(`myExperts.cloudUpload.modal.quotaUnavailable`));return}let t;try{t=await l(e)}catch{Ua.warning(m(`myExperts.cloudUpload.modal.quotaUnavailable`));return}if(t.status===`unavailable`){Ua.warning(m(`myExperts.cloudUpload.modal.quotaUnavailable`));return}if(t.status===`exceeded`){Ua.warning(getQuotaExceededMessage(m,{required:e,quota:t.quota}));return}}O(ee),A(createInitialUploadItemResults(ee,h)),M(null),I(!1),w(ne>0?`conflict`:`confirm`),S(!0)})().catch(()=>Ua.warning(m(`myExperts.cloudUpload.modal.quotaUnavailable`)))},[ae,ne,h,l,m,ee]),le=(0,zR.useCallback)(async()=>{if(!u||y||D.length===0)return;let e,t;if(C===`credential-risk`){let n=k.filter(e=>e.status===`confirmation_required`);if(n.length===0)return;e=n.map(e=>e.expertId),t=Object.fromEntries(n.map(e=>[e.expertId,e.confirmationTypes?.length?e.confirmationTypes:[`credential`]]))}else if(C===`error`){if(e=k.filter(e=>e.status===`failed`).map(e=>e.expertId),e.length===0)return}else e=D.map(e=>e.id);let n=F||re>0;I(n),w(`uploading`),M({completed:0,total:e.length,success:0,failed:0}),b(!0);try{let r=await u(e,n,t);P(r),M({completed:e.length,total:e.length,success:r.succeeded.length,failed:r.failed.length});let i=getBatchUploadItemResults(D.filter(t=>e.includes(t.id)),r,h,m),a=new Map(i.map(e=>[e.expertId,e])),o=k.map(e=>a.get(e.expertId)??e);A(o);let s=o.some(e=>e.status===`quota_blocked`)||!!r.quotaAdmission,c=o.some(e=>e.status===`confirmation_required`),l=new Set(o.filter(e=>e.status!==`success`).map(e=>e.expertId));if(l.size>0){O(e=>e.filter(e=>l.has(e.id))),w(s?`quota-blocked`:c?`credential-risk`:`error`);return}se()}catch(t){let n=t instanceof Error?t.message:String(t);M({completed:e.length,total:e.length,success:0,failed:e.length});let r=new Set(e);P(null);let i=k.map(e=>r.has(e.expertId)?{...e,status:`failed`,message:n}:e);A(i);let a=new Set(i.filter(e=>e.status!==`success`).map(e=>e.expertId));O(e=>e.filter(e=>a.has(e.id))),w(`error`)}finally{b(!1)}},[se,h,u,F,re,D,k,C,y]);return(0,BR.jsxs)(BR.Fragment,{children:[(0,BR.jsxs)(`div`,{className:`ec-my-created-toolbar`,children:[(0,BR.jsxs)(`div`,{className:`${EI.tabs} ec-my-created-tabs`,role:`tablist`,children:[(0,BR.jsxs)(`button`,{type:`button`,role:`tab`,"aria-selected":n===`expert`,className:n===`expert`?`${EI.tab} ${EI.active}`:EI.tab,onClick:()=>oe(`expert`),children:[(0,BR.jsx)(`span`,{className:`ec-my-created-tab-label`,children:m(`expertSection`)}),(0,BR.jsx)(`span`,{className:`ec-my-created-tab-count`,children:L.length})]}),(0,BR.jsxs)(`button`,{type:`button`,role:`tab`,"aria-selected":n===`team`,className:n===`team`?`${EI.tab} ${EI.active}`:EI.tab,onClick:()=>oe(`team`),children:[(0,BR.jsx)(`span`,{className:`ec-my-created-tab-label`,children:m(`teamSection`)}),(0,BR.jsx)(`span`,{className:`ec-my-created-tab-count`,children:R.length})]})]}),(ie||p)&&(0,BR.jsxs)(`div`,{className:`ec-my-toolbar-right`,children:[ie&&(0,BR.jsxs)(`button`,{type:`button`,className:`ec-my-cloud-upload-all`,onClick:ce,disabled:!ae,"aria-busy":y,"aria-label":m(`myExperts.cloud.uploadAll`),children:[(0,BR.jsx)(CloudUploadIcon,{className:`ec-my-cloud-upload-all__icon`}),(0,BR.jsx)(`span`,{children:m(y?`myExperts.cloud.uploading`:`myExperts.cloud.uploadAll`)})]}),p]})]}),U&&(0,BR.jsx)(ExpertListEmptyState,{message:m(V?`noTeamSearchResult`:`noSearchResult`,{keyword:f.trim()}),showIcon:!0}),W&&(0,BR.jsxs)(`Line truncated
|
||||
function Dropdown({trigger:r,items:s,onSelect:c,placement:l=`bottom-start`,triggerMode:u=`click`,contextMenuAnchor:p,offsetDistance:m=4,closeOnMouseLeave:h=!1,mouseLeaveCloseDelay:g=120,open:_,onOpenChange:v,disabled:y,portalRoot:x,className:S,ariaLabel:C,layer:w,card:T=!1,searchable:E=!1,searchValue:D,defaultSearchValue:O=``,onSearchChange:k,searchPlaceholder:A,footer:j,width:M,submenuWidth:N=240,submenuOffsetDistance:P=4,maxHeight:F,virtualized:I=!1,selectionMode:L=`default`,itemTooltipPlacement:R=`right`,footerItems:z,highlightMatch:B=!1,anchorRef:V}){let[H,U]=(0,Xz.useState)(!1),W=_!==void 0,G=W?!!_:H,[K,q]=(0,Xz.useState)(O),ee=D!==void 0,J=ee?D:K,Y=(0,Xz.useCallback)(r=>{W||U(r),!r&&!ee&&K&&(q(``),k?.(``)),v?.(r)},[W,v,ee,K,k]),{trigger:te,onFloatingMouseEnter:ne,onFloatingMouseLeave:re}=useDropdownHoverClose({enabled:h,trigger:r,onRequestClose:()=>Y(!1),delay:g,open:G}),ie=(0,Xz.useCallback)(r=>{ee||q(r),k?.(r)},[ee,k]),ae=(0,Xz.useCallback)(r=>{r.disabled||r.empty||c?.(r.key,r)!==!1&&Y(!1)},[c,Y]),oe=[`wb-dropdown`,T?`wb-dropdown--card`:``,L===`single`?`wb-dropdown--single`:``,F===void 0?``:`wb-dropdown--scrollable`,S].filter(Boolean).join(` `),[se,ce]=(0,Xz.useState)(!1),le=(0,Xz.useRef)(null),ue=(0,Xz.useCallback)(()=>{ce(!0),le.current!==null&&window.clearTimeout(le.current),le.current=window.setTimeout(()=>{le.current=null,ce(!1)},150)},[]);(0,Xz.useEffect)(()=>()=>{le.current!==null&&window.clearTimeout(le.current)},[]);let de=j!==void 0||z!==void 0&&z.length>0,fe=I&&F!==void 0&&s.length>40&&s.every(r=>!r.children?.length&&!r.divider),pe={items:s,onSelectLeaf:ae,submenuWidth:N,submenuOffsetDistance:P,selectionMode:L,itemTooltipPlacement:R,highlightKeyword:E&&B?J:void 0,suppressItemTooltips:se,onHoverRegionEnter:ne,onHoverRegionLeave:re};return(0,Zz.jsxs)(Popover,{anchorRef:V,trigger:te,triggerMode:u,contextMenuAnchor:p,placement:l,offsetDistance:m,open:G,onOpenChange:Y,disabled:y,role:`menu`,ariaLabel:C,layer:w,className:oe,onFloatingMouseEnter:ne,onFloatingMouseLeave:re,portalRoot:x??`inline`,stopClickPropagation:!0,style:M===void 0?void 0:{width:M},transitionPreset:`expand`,children:[(0,Zz.jsxs)(`div`,{className:[`wb-dropdown__scroll`,de?`wb-dropdown__scroll--has-footer`:``,fe?`wb-dropdown__scroll--virtual`:``].filter(Boolean).join(` `),style:F===void 0?void 0:{maxHeight:F,overflowY:fe?`hidden`:`auto`},onScroll:ue,children:[E&&(0,Zz.jsx)(`div`,{className:`wb-dropdown__search`,children:(0,Zz.jsx)(ML,{size:`small`,variant:`filled`,prefix:(0,Zz.jsx)(L_,{width:14,height:14}),allowClear:!0,placeholder:A,value:J,onChange:r=>ie(r.target.value),autoFocus:!0,className:`wb-dropdown__input`})}),fe?(0,Zz.jsx)(VirtualDropdownMenuList,{...pe,maxHeight:F,searchKeyword:J,onScroll:ue}):(0,Zz.jsx)(DropdownMenuList,{...pe})]}),de&&(0,Zz.jsx)(DropdownFooter,{items:z,footer:j,menuProps:pe})]})}
|
||||
function DropdownMenuList({items:r,onSelectLeaf:s,submenuWidth:c,submenuOffsetDistance:l,selectionMode:u=`default`,itemTooltipPlacement:p=`right`,highlightKeyword:m,suppressItemTooltips:h=!1,onHoverRegionEnter:g,onHoverRegionLeave:_,virtualStartIndex:v,virtualTotal:y}){let x=u===`single`,[S,C]=(0,Oz.useState)(null),w=(0,Oz.useRef)(null),cancelSubmenuClose=()=>{w.current!==null&&(clearTimeout(w.current),w.current=null)},scheduleSubmenuClose=r=>{cancelSubmenuClose(),w.current=setTimeout(()=>{C(s=>s===r?null:s),w.current=null},Az)};return(0,Oz.useEffect)(()=>()=>{w.current!==null&&clearTimeout(w.current)},[]),(0,kz.jsx)(`ul`,{className:`wb-dropdown__list`,role:`none`,children:r.map((r,w)=>{let T=r.trackId?{"data-track-id":r.trackId,...r.trackName?{"data-track-name":r.trackName}:{},...r.trackProps?{"data-track-props":JSON.stringify(r.trackProps)}:{}}:{},E=!!r.children&&r.children.length>0,D=r.disabled||r.empty||E&&!r.selectableWithChildren,O=r.disabled?r.disabledTooltip:void 0,k=h?void 0:O??r.tooltip,A=O?`top`:p,enterSubmenuRegion=r=>{cancelSubmenuClose(),g?.(r)},leaveSubmenuRegion=s=>{let c=s?.currentTarget,l=s&&typeof document.elementFromPoint==`function`?document.elementFromPoint(s.clientX,s.clientY):null;l instanceof Node&&(c?.contains(l)??!1)||(scheduleSubmenuClose(r.key),_?.(s))},j=(0,kz.jsxs)(`li`,{className:[`wb-dropdown__item`,r.disabled?`wb-dropdown__item--disabled`:``,r.selected?`wb-dropdown__item--selected`:``,r.danger?`wb-dropdown__item--danger`:``,r.empty?`wb-dropdown__item--empty`:``,r.className].filter(Boolean).join(` `),role:r.empty?`presentation`:x?`menuitemradio`:`menuitem`,"data-virtual-index":v===void 0?void 0:v+w,"aria-posinset":v===void 0?void 0:v+w+1,"aria-setsize":y,"aria-disabled":r.disabled||r.empty||void 0,"aria-checked":x&&!r.empty?!!r.selected:r.selected||void 0,"aria-haspopup":E||void 0,tabIndex:r.disabled||r.empty?-1:0,onMouseDown:D?r=>r.preventDefault():void 0,onMouseEnter:()=>{cancelSubmenuClose(),g?.(),C(E&&!r.disabled?r.key:null)},onMouseOver:()=>{cancelSubmenuClose(),g?.()},onMouseLeave:()=>{E&&!r.disabled&&scheduleSubmenuClose(r.key)},onClick:()=>{D||s(r)},onKeyDown:c=>{(c.key===`Enter`||c.key===` `)&&!D&&(c.preventDefault(),s(r))},...T,children:[r.icon&&(0,kz.jsx)(`span`,{className:`wb-dropdown__icon`,children:r.icon}),(0,kz.jsx)(DropdownItemLabel,{label:r.label,highlightKeyword:m}),r.selected&&!r.hideSelectedCheck&&(0,kz.jsx)(`span`,{className:`wb-dropdown__check`,"aria-hidden":`true`,children:(0,kz.jsx)(Fs,{size:16})}),E&&(0,kz.jsx)(`span`,{className:`wb-dropdown__arrow`,"aria-hidden":`true`,children:(0,kz.jsx)(uc,{size:14})}),k&&(0,kz.jsx)(Tooltip,{content:k,placement:A,children:(0,kz.jsx)(`span`,{className:`wb-dropdown__item-reason-hit`,"aria-hidden":`true`})})]});return(0,kz.jsxs)(Oz.Fragment,{children:[r.divider&&(0,kz.jsx)(`li`,{className:`wb-dropdown__divider`,role:`separator`}),E?(0,kz.jsx)(Popover,{trigger:j,triggerMode:`manual`,open:S===r.key,onOpenChange:s=>{C(c=>s?r.key:c===r.key?null:c)},placement:`right-start`,offsetDistance:{mainAxis:l,crossAxis:-8},disabled:r.disabled,role:`menu`,className:`wb-dropdown`,portalRoot:`inline`,onFloatingMouseEnter:enterSubmenuRegion,onFloatingMouseLeave:leaveSubmenuRegion,stopClickPropagation:!0,style:{width:c},transitionPreset:`expand`,children:(0,kz.jsx)(DropdownMenuList,{items:r.children,onSelectLeaf:s,submenuWidth:c,submenuOffsetDistance:l,selectionMode:u,itemTooltipPlacement:p,highlightKeyword:m,suppressItemTooltips:h,onHoverRegionEnter:enterSubmenuRegion,onHoverRegionLeave:leaveSubmenuRegion})}):j]},r.key)})})}
|
||||
function DropdownItemLabel({label:r,highlightKeyword:s}){let c=(0,Oz.useRef)(null),[l,u]=(0,Oz.useState)(!1),p=typeof r==`string`;if((0,Oz.useEffect)(()=>{if(!p){u(!1);return}let r=c.current;if(!r)return;let measure=()=>{u(r.scrollWidth>r.clientWidth)};if(measure(),typeof ResizeObserver>`u`)return;let s=new ResizeObserver(measure);return s.observe(r),()=>s.disconnect()},[p,r]),!p)return(0,kz.jsx)(`span`,{className:`wb-dropdown__label`,children:r});let m=s?.trim(),h=m?highlightLabelText(r,m):r;return(0,kz.jsx)(Tooltip,{content:l?r:void 0,placement:`top`,children:(0,kz.jsx)(`span`,{ref:c,className:`wb-dropdown__label wb-dropdown__label--ellipsis`,children:h})})}
|
||||
function highlightLabelText(r,s){let c=r.toLowerCase(),l=s.toLowerCase(),u=[],p=0,m=c.indexOf(l,p);if(m<0)return r;for(;m>=0;)m>p&&u.push(r.slice(p,m)),u.push((0,kz.jsx)(`mark`,{className:`wb-dropdown__highlight`,children:r.slice(m,m+s.length)},`hit-${m}`)),p=m+s.length,m=c.indexOf(l,p);return p<r.length&&u.push(r.slice(p)),u}
|
||||
function useDropdownHoverClose({enabled:r,trigger:s,onRequestClose:c,delay:l,open:u}){let p=(0,Mz.useRef)(null),m=(0,Mz.useRef)(null),h=s===void 0?void 0:s.ref,g=(0,Mz.useRef)(c);g.current=c;let _=(0,Mz.useCallback)(r=>{m.current=r,typeof h==`function`?h(r):h!=null&&(h.current=r)},[h]),v=(0,Mz.useCallback)(()=>{p.current!=null&&(window.clearTimeout(p.current),p.current=null)},[]),y=(0,Mz.useCallback)(()=>{let r=m.current;if(r?.matches(`:hover`))return!0;let s=r?.getAttribute(`aria-controls`);return(s?document.getElementById(s):null)?.matches(`:hover`)??!1},[]),x=(0,Mz.useCallback)(s=>{if(!r)return;let c=s?.currentTarget,u=s&&typeof document.elementFromPoint==`function`?document.elementFromPoint(s.clientX,s.clientY):null;u instanceof Node&&(c?.contains(u)??!1)||(v(),p.current=window.setTimeout(()=>{p.current=null,!y()&&g.current()},l))},[v,l,r,y]);return(0,Mz.useEffect)(()=>v,[v]),(0,Mz.useEffect)(()=>{u||v()},[v,u]),!r||s===void 0?{trigger:s}:{trigger:Mz.cloneElement(s,{ref:_,onMouseEnter:r=>{m.current=r.currentTarget,s.props.onMouseEnter?.(r),v()},onMouseLeave:r=>{m.current=r.currentTarget,s.props.onMouseLeave?.(r),x()}}),onFloatingMouseEnter:v,onFloatingMouseLeave:x}}
|
||||
function createIcon(r,s){let c=(0,_n.forwardRef)(function(c,l){let u=c.active&&s?.activeAsset?s.activeAsset:r;return(0,vn.jsx)(bn,{ref:l,component:u,size:c.size??s?.size,strokeWidth:c.strokeWidth??s?.strokeWidth,color:c.color??s?.color,...c})});return _n.memo(c)}
|
||||
function resolveSize(r){if(r!=null)return typeof r==`number`?r:typeof r==`string`&&r in yn?yn[r]:r}
|
||||
const bn=(0,_n.forwardRef)(function(r,s){let{component:c,size:l=`md`,width:u,height:p,color:m,spin:h,rotate:g,strokeWidth:_,className:v,style:y,children:x,active:S,...C}=r,w=resolveSize(u??l),T=resolveSize(p??l),E={...m?{color:m}:null,...g&&!h?{transform:`rotate(${g}deg)`}:null,...y},D=[`wb-icon`,h?`wb-icon--spin`:``,v].filter(Boolean).join(` `),O=E;if(c&&typeof c==`object`&&`url`in c){let r=c;if(r.inline)return(0,vn.jsx)(hn,{ref:s,asset:r,width:w,height:T,className:D,style:O,rest:C});let l=[D,r.themable?`wb-icon--themable`:``].filter(Boolean).join(` `);return(0,vn.jsx)(`img`,{...C,ref:s,src:r.url,alt:r.alt??``,"aria-hidden":!r.alt||void 0,width:w,height:T,className:l,style:O,draggable:!1})}if(c){let r=c,l={width:w,height:T,className:D,style:E,"aria-hidden":!0,..._===void 0?null:{strokeWidth:_},...C};return(0,vn.jsx)(r,{ref:s,...l,children:x})}return(0,vn.jsx)(`svg`,{ref:s,width:w,height:T,className:D,style:O,"aria-hidden":!0,xmlns:`http://www.w3.org/2000/svg`,...C,children:x})});
|
||||
function ConfirmDialog({visible:t,title:s,content:u,detailContent:p,confirmText:h=`\u786E\u8BA4`,cancelText:_,confirmVariant:T=`primary`,confirmButtonColor:E,showDontAskAgain:O=!1,dontAskAgainText:A=`\u4E0D\u518D\u63D0\u793A`,footerLeftSlot:j,footerButtonsAlign:M=`end`,buttonOrder:N=`cancel-confirm`,confirmLoading:L=!1,footerButtonWidth:R,confirmDisabled:B=!1,showCloseButton:H=!1,onClose:W,onConfirm:ee,theme:G,appearance:ne=`default`,confirmTrack:re,cancelTrack:ie,overlayClassName:ae,portalContainer:oe,disableOverlayClose:de=!1}){let[fe,pe]=(0,KS.useState)(!1),me=(0,KS.useRef)(null),he=typeof R==`number`?`${R}px`:R,ge=he?{width:he}:void 0;(0,KS.useEffect)(()=>{if(!t)return;let handleKeyDown=t=>{t.key===`Escape`&&W()};return document.addEventListener(`keydown`,handleKeyDown),()=>document.removeEventListener(`keydown`,handleKeyDown)},[t,W]),(0,KS.useEffect)(()=>{if(!oe)return;let t=oe,s=t.style.position;return(!s||s===`static`)&&(t.style.position=`relative`),()=>{t.style.position=s}},[oe]);let ve=(0,KS.useCallback)(t=>{t.stopPropagation(),!de&&(me.current?.contains(t.target)||W())},[W,de]),we=(0,KS.useCallback)(()=>{ee(O?fe:void 0)},[ee,O,fe]),De=(0,KS.useCallback)(t=>{pe(t.target.checked)},[]),buildTrackAttrs=t=>t?{"data-track-id":t.elementId,"data-track-name":t.elementName,...t.props?{"data-track-props":JSON.stringify(t.props)}:{}}:{},Oe=buildTrackAttrs(re),je=buildTrackAttrs(ie);if(!t)return null;let Ie=!!oe,Re=[WS.dialogOverlay,Ie?WS.dialogOverlayContained:``,ae].filter(Boolean).join(` `);return(0,qS.createPortal)((0,JS.jsx)(`div`,{className:Re,onClick:ve,"data-theme":G,"data-appearance":ne,children:(0,JS.jsxs)(`div`,{ref:me,className:[WS.dialogContainer,ne===`workbuddy`?WS.dialogContainerWorkbuddy:``].filter(Boolean).join(` `),children:[H&&(0,JS.jsx)(Ku,{className:WS.dialogClose,onClick:W,variant:`ghost`,size:`small`,iconOnly:!0,"aria-label":Z[`action.close`],children:(0,JS.jsx)(CloseIcon$2,{})}),(0,JS.jsx)(`div`,{className:WS.dialogHeader,children:s}),u&&(0,JS.jsx)(`div`,{className:WS.dialogContent,children:u}),p&&(0,JS.jsx)(`div`,{className:WS.dialogDetailContent,children:p}),(0,JS.jsxs)(`div`,{className:[WS.dialogFooter,M===`start`?WS.dialogFooterAlignStart:``,M===`center`?WS.dialogFooterAlignCenter:``].filter(Boolean).join(` `),children:[O&&(0,JS.jsxs)(`label`,{className:WS.dontAskAgain,children:[(0,JS.jsx)(`input`,{type:`checkbox`,checked:fe,onChange:De,className:WS.checkbox}),(0,JS.jsx)(`span`,{className:WS.checkboxLabel,children:A})]}),j&&(0,JS.jsx)(`div`,{className:WS.footerLeftSlot,children:j}),(0,JS.jsx)(`div`,{className:[WS.footerButtons,M===`start`?WS.footerButtonsAlignStart:``,M===`center`?WS.footerButtonsAlignCenter:``].filter(Boolean).join(` `),children:N===`confirm-cancel`?(0,JS.jsxs)(JS.Fragment,{children:[h&&(0,JS.jsx)(Ku,{variant:T,onClick:we,disabled:B,style:{...ge,...E?{backgroundColor:E}:{}},...Oe,children:L?`...`:h}),_&&(0,JS.jsx)(Ku,{variant:`secondary`,className:WS.cancelButtonStatic,onClick:W,style:ge,...je,children:_})]}):(0,JS.jsxs)(JS.Fragment,{children:[_&&(0,JS.jsx)(Ku,{variant:`secondary`,className:WS.cancelButtonStatic,onClick:W,style:ge,...je,children:_}),h&&(0,JS.jsx)(Ku,{variant:T,onClick:we,disabled:B,style:{...ge,...E?{backgroundColor:E}:{}},...Oe,children:L?`...`:h})]})})]})]})}),oe??document.body)}
|
||||
const Ku=(0,Wu.forwardRef)(({variant:t=`secondary`,size:s=`medium`,leftIcon:u,rightIcon:p,loading:h=!1,iconOnly:_=!1,fullWidth:T=!1,className:E,children:O,disabled:A,...j},M)=>{let N=(0,Uu.default)(`cb-button`,`cb-button--${t}`,`cb-button--${s}`,{"cb-button--disabled":A,"cb-button--loading":h,"cb-button--icon-only":_,"cb-button--full-width":T},E),renderContent=()=>_?u||p||O:(0,Gu.jsxs)(`span`,{className:`cb-button__content`,children:[u&&(0,Gu.jsx)(`span`,{className:`cb-button__icon`,children:u}),O,p&&(0,Gu.jsx)(`span`,{className:`cb-button__icon`,children:p})]});return(0,Gu.jsxs)(`button`,{ref:M,className:N,disabled:A||h,...j,children:[renderContent(),h&&(0,Gu.jsx)(`div`,{className:`cb-button__loading`})]})});
|
||||
const CloseIcon$2=({className:t,style:s,width:u=12,height:p=12,...h})=>(0,Lm.jsxs)(`svg`,{className:t,style:s,...h,width:u,height:p,viewBox:`0 0 12 12`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children:[(0,Lm.jsx)(`path`,{d:`M9.375 2.625L2.625 9.375`,stroke:`currentColor`,strokeWidth:`0.975`,strokeLinecap:`round`,strokeLinejoin:`round`}),(0,Lm.jsx)(`path`,{d:`M9.375 9.375L2.625 2.625`,stroke:`currentColor`,strokeWidth:`0.975`,strokeLinecap:`round`,strokeLinejoin:`round`})]});
|
||||
const Au=(0,Ou.forwardRef)((r,s)=>(0,ku.jsx)(`svg`,{ref:s,viewBox:`0 0 16 16`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`,...r,children:(0,ku.jsx)(`path`,{transform:`matrix(1 0 0 1 0.333008 1.57364)`,fillRule:`evenodd`,d:`M15.334 6.4263Q15.3339 7.466 14.7056 8.6204Q14.1008 9.7315 13.0674 10.6698Q10.6634 12.8527 7.667 12.8527Q4.6709 12.8527 2.2665 10.6689Q1.2332 9.7304 0.6284 8.6194Q0 7.4652 0 6.4263Q0 5.3875 0.6284 4.2332Q1.2332 3.1222 2.2665 2.1838Q4.6709 0 7.667 0Q10.6634 0 13.0674 2.1828Q14.1008 3.1211 14.7056 4.2322Q15.3339 5.3866 15.334 6.4263ZM14.1339 6.4263Q14.1339 5.6921 13.6516 4.8059Q13.1438 3.8731 12.2607 3.0713Q10.1999 1.2 7.667 1.2Q5.1345 1.2 3.0733 3.0721Q2.1902 3.8742 1.6823 4.807Q1.2 5.6929 1.2 6.4263Q1.2 7.1597 1.6823 8.0457Q2.1902 8.9785 3.0733 9.7805Q5.1345 11.6527 7.667 11.6526Q10.1999 11.6527 12.2607 9.7814Q13.1438 8.9796 13.6516 8.0467Q14.1339 7.1606 14.1339 6.4263ZM10.6226 6.4263Q10.6226 7.6506 9.7569 8.5163Q8.8913 9.382 7.667 9.382Q6.4427 9.382 5.577 8.5163Q4.7113 7.6506 4.7113 6.4263Q4.7113 5.2021 5.577 4.3364Q6.4427 3.4707 7.667 3.4707Q8.8913 3.4707 9.7569 4.3364Q10.6226 5.2021 10.6226 6.4263ZM9.4227 6.4263Q9.4226 5.6991 8.9084 5.1849Q8.3942 4.6707 7.667 4.6707Q6.9398 4.6707 6.4256 5.1849Q5.9113 5.6991 5.9113 6.4263Q5.9113 7.1535 6.4256 7.6678Q6.9398 8.182 7.667 8.182Q8.3942 8.182 8.9084 7.6678Q9.4226 7.1535 9.4227 6.4263Z`})}));
|
||||
const bF=(0,vF.forwardRef)((r,s)=>(0,yF.jsx)(`svg`,{ref:s,viewBox:`0 0 16 16`,xmlns:`http://www.w3.org/2000/svg`,fill:`none`,...r,children:(0,yF.jsx)(`path`,{fill:`currentColor`,fillRule:`evenodd`,transform:`matrix(1 0 0 1 0.999868 1.49989)`,d:`M0.3011 12.4122Q0.6022 12.7102 1.0178 12.6286L1.9012 12.4553Q4.4733 11.9508 6.3267 10.0973L11.9021 4.5219Q12.678 3.7461 12.678 2.6489Q12.678 1.5517 11.9021 0.7758Q11.1263 0 10.0291 0Q8.9319 0 8.1561 0.7758L2.6125 6.3194Q0.7248 8.2071 0.2379 10.8318L0.0773 11.6977Q0 12.1142 0.3011 12.4122ZM11.0536 3.6734L10.7156 4.0114L8.6671 1.9618L9.0046 1.6244Q9.429 1.2 10.0291 1.2Q10.6292 1.2 11.0536 1.6244Q11.478 2.0487 11.478 2.6489Q11.478 3.249 11.0536 3.6734ZM1.3645 11.3378L1.6702 11.2778Q3.8834 10.8436 5.4782 9.2488L9.8671 4.86L7.8186 2.8103L3.461 7.1679Q1.8368 8.7922 1.4178 11.0507L1.3645 11.3378ZM6.0001 11.3L13.0001 11.3L13.0001 12.5L6.0001 12.5L6.0001 11.3Z`})}));
|
||||
const Pl=(0,Ml.forwardRef)((r,s)=>(0,Nl.jsx)(`svg`,{ref:s,viewBox:`0 0 16 16`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`,...r,children:(0,Nl.jsx)(`path`,{transform:`matrix(1 0 0 1 1.03834 1.20291)`,fillRule:`evenodd`,d:`M4.6312 0L9.6312 0L9.6312 1.2L4.6312 1.2L4.6312 0ZM0 2.9061L13.9633 2.9061L13.9633 4.1061L2.7311 4.1061L2.7311 8.2934Q2.7311 10.2897 2.8934 10.7529Q3.3549 12.0696 4.6717 12.5311Q5.1348 12.6934 7.1311 12.6934Q9.1274 12.6934 9.5906 12.5311Q10.9074 12.0696 11.3688 10.7529Q11.5311 10.2897 11.5311 8.2934L11.5311 5.0392L12.7311 5.0392L12.7311 8.2934Q12.7311 10.4939 12.5013 11.1497Q11.8489 13.0112 9.9875 13.6636Q9.3316 13.8934 7.1311 13.8934Q4.9306 13.8934 4.2748 13.6636Q2.4133 13.0112 1.7609 11.1497Q1.5311 10.4939 1.5311 8.2934L1.5311 4.1061L0 4.1061L0 2.9061ZM7.7312 11.2934L7.7312 5.2934L6.5312 5.2934L6.5312 11.2934L7.7312 11.2934Z`})}));
|
||||
const wd=(0,Sd.forwardRef)((r,s)=>(0,Cd.jsx)(`svg`,{ref:s,viewBox:`0 0 16 16`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`,...r,children:(0,Cd.jsx)(`path`,{transform:`matrix(1 0 0 1 0.900391 0.90039)`,d:`M4.4893 0C4.8371 0 5.0591 -0.0008 5.2754 0.0244C6.3094 0.1455 7.0485 0.6989 7.6641 1.3145L8.6494 2.2998L6.9521 2.2998L6.8154 2.1631C6.2812 1.6289 5.7808 1.2913 5.1367 1.2158C5.0059 1.2005 4.8651 1.1992 4.4893 1.1992C3.3369 1.1992 2.5394 1.4806 2.0293 1.9717C1.5234 2.4588 1.1992 3.2499 1.1992 4.4893L1.1992 8.0996C1.1992 9.0452 1.1999 9.7144 1.2471 10.2353C1.2935 10.7481 1.382 11.0583 1.5215 11.2998C1.8929 11.9429 2.4455 12.2436 3.1484 12.3867C3.519 12.4622 3.9095 12.4906 4.3184 12.5L9.2217 12.5C9.8441 12.4975 10.462 12.477 11.0312 12.3564C11.7703 12.1999 12.3358 11.8919 12.6777 11.2998C12.8172 11.0583 12.9057 10.7481 12.9521 10.2353C12.9914 9.8018 12.9958 9.2656 12.9971 8.5527L12.9961 8.1064C12.9832 6.8588 12.9095 6.3058 12.6777 5.9043C12.3358 5.3123 11.7703 5.0042 11.0312 4.8477C10.462 4.7271 9.8441 4.7066 9.2217 4.7041L2.5996 4.7041L2.5996 3.5049L8.1572 3.5049L8.1514 3.499L9.8486 3.499L9.8652 3.5156C10.3318 3.533 10.8171 3.5747 11.2803 3.6729C12.2112 3.8701 13.1449 4.3124 13.7178 5.3047C14.1305 6.02 14.1839 6.8921 14.1963 8.0938L14.1963 8.0996L14.1992 8.0996C14.1992 8.2186 14.1974 8.3345 14.1973 8.4473L14.1992 9.1025L14.1982 9.1035L14.1992 9.1045L14.1934 9.1045C14.1885 9.5777 14.1788 9.9874 14.1465 10.3437C14.0925 10.9395 13.9801 11.4449 13.7178 11.8994C13.1449 12.8917 12.2112 13.334 11.2803 13.5312C10.3688 13.7243 9.3719 13.6992 8.5996 13.6992L5.5996 13.6992C4.8004 13.6992 3.8041 13.7447 2.9092 13.5625C1.9757 13.3724 1.0723 12.9228 0.4814 11.8994C0.2191 11.4449 0.1067 10.9395 0.0527 10.3437C-0.0005 9.7561 0 9.0232 0 8.0996L0 4.4893C0 3.0495 0.3791 1.8952 1.1973 1.1074C2.0113 0.3238 3.1594 0 4.4893 0Z`})}));
|
||||
const Zl=(0,Yl.forwardRef)((r,s)=>(0,Xl.jsx)(`svg`,{ref:s,viewBox:`0 0 14 14`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,...r,children:(0,Xl.jsx)(`path`,{fill:`currentColor`,fillRule:`evenodd`,transform:`matrix(0 -1 1 0 1.63754 12.3219)`,d:`M3.072 10.6849Q3.5599 10.725 4.9563 10.725L4.9563 9.75Q3.5999 9.75 3.1519 9.7132Q2.5469 9.6635 2.2227 9.4965Q1.5665 9.1585 1.2285 8.5023Q1.0615 8.1781 1.0118 7.5731Q0.975 7.1252 0.975 5.7688L0.975 4.9563Q0.975 3.5999 1.0118 3.1519Q1.0615 2.5469 1.2285 2.2227Q1.5665 1.5665 2.2227 1.2285Q2.5469 1.0615 3.1519 1.0118Q3.5999 0.975 4.9563 0.975L4.9563 0Q3.5599 0 3.072 0.0401Q2.2733 0.1057 1.7763 0.3617Q0.8426 0.8426 0.3617 1.7763Q0.1057 2.2733 0.0401 3.072Q0 3.5599 0 4.9563L0 5.7688Q0 7.1652 0.0401 7.653Q0.1057 8.4517 0.3617 8.9487Q0.8426 9.8824 1.7763 10.3633Q2.2733 10.6193 3.072 10.6849ZM5.3207 5.8501L7.3323 7.8616L6.6429 8.5511L4.4557 6.3639L4.4467 6.3549Q4.1584 6.0666 4.0623 5.9401Q3.8486 5.6589 3.8486 5.3626Q3.8486 5.0662 4.0623 4.7851Q4.1584 4.6586 4.4467 4.3702L4.4557 4.3613L6.6429 2.1741L7.3323 2.8636L5.3207 4.8751L10.6438 4.8751L10.6438 5.8501L5.3207 5.8501Z`})}));
|
||||
const ShareIcon=r=>{let s=dv.useId();return(0,fv.jsxs)(`svg`,{width:`16`,height:`16`,viewBox:`0 0 16 16`,fill:`currentColor`,...r,children:[(0,fv.jsx)(`clipPath`,{id:s,children:(0,fv.jsx)(`rect`,{width:`16`,height:`16`})}),(0,fv.jsx)(`g`,{clipPath:`url(#${s})`,children:(0,fv.jsx)(`path`,{transform:`matrix(1 0 0 1 0.900042 -0.0114454)`,d:`M0 10.5114Q0 7.4426 2.1309 5.2343Q4.0501 3.2452 6.9999 2.9501L6.9999 2.2226Q6.9999 2.1316 6.9987 1.9596Q6.9942 1.3235 7.0379 1.072Q7.1435 0.4641 7.6102 0.232Q8.0769 0 8.6253 0.2827Q8.8522 0.3996 9.3567 0.7872Q9.493 0.8918 9.5656 0.9468L12.8559 3.4374Q12.9206 3.4864 13.0449 3.58Q13.8036 4.1518 14.0511 4.4109Q14.5433 4.9263 14.5434 5.5101Q14.5435 6.0938 14.0513 6.6094Q13.8039 6.8685 13.0454 7.4405Q12.9213 7.534 12.8565 7.5831L9.5659 10.0752Q9.4934 10.1301 9.357 10.2349Q8.8525 10.6227 8.6255 10.7397Q8.0771 11.0226 7.6103 10.7906Q7.1435 10.5586 7.0379 9.9506Q7.0064 9.7693 7 9.3883L7 7.0732Q7.5596 6.8823 7.8937 7.1438Q8.2014 7.3847 8.2 7.9895Q8.1999 8.0211 8.2 8.0372L8.2 8.7997Q8.1999 8.8951 8.1987 9.0712Q8.1963 9.4111 8.2059 9.5925Q8.3563 9.4906 8.6257 9.2835Q8.7655 9.1761 8.8414 9.1186L12.132 6.6265Q12.1978 6.5767 12.3229 6.4823Q13.3434 5.7129 13.3434 5.5102Q13.3433 5.3075 12.3227 4.5383Q12.1977 4.4442 12.1317 4.3942L8.8414 1.9036Q8.7653 1.846 8.6257 1.7388Q8.3563 1.5319 8.2059 1.43Q8.1963 1.6114 8.1987 1.9511Q8.1999 2.1272 8.2 2.2226L8.2 3.1241Q8.1999 3.6404 7.9749 3.8734Q7.7665 4.0891 7.2781 4.1297L7.2633 4.131Q4.653 4.3486 2.9944 6.0675Q1.2 7.9272 1.2 10.5114Q1.2 11.8336 1.7239 13.0475Q1.8534 13.3475 2.0094 13.6285Q2.1167 11.4689 3.444 9.7197Q4.4404 8.4065 5.8 7.629L5.8 9.0582Q5.0187 9.6295 4.3999 10.4451Q3.2 12.0264 3.2 14.0114Q3.2 14.3771 3.2452 14.7403Q3.3192 15.3349 2.7865 15.6064Q2.2041 15.9032 1.7834 15.4046Q1.0684 14.5571 0.6222 13.5231Q0 12.0816 0 10.5114Z`,fillRule:`evenodd`})})]})};
|
||||
const $te=createIcon(Au),lo=createIcon(bF),bo=createIcon(Pl),uo=createIcon(wd),ote=createIcon(Zl,{size:'sm'}),Oa=ShareIcon,Wa=Dropdown;
|
||||
function Ie(props){const[theme,setTheme]=React.useState(()=>document.documentElement.dataset.theme==='dark'?'dark':'light');React.useEffect(()=>{const o=new MutationObserver(()=>setTheme(document.documentElement.dataset.theme==='dark'?'dark':'light'));o.observe(document.documentElement,{attributes:true,attributeFilter:['data-theme']});return()=>o.disconnect()},[]);return JSX.jsx(ConfirmDialog,{...props,theme})}
|
||||
function TeamDetail({expert,onClose,onSummon}){const locale=useLocale$1();React.useEffect(()=>{const key=e=>{if(e.key==='Escape'&&!e.isComposing){e.stopPropagation();onClose()}};document.addEventListener('keydown',key,true);return()=>document.removeEventListener('keydown',key,true)},[onClose]);const content=host().renderPreview?.({expert,onClose,onSummon})??PortalHost.createPortal(JSX.jsx('div',{className:WS.dialogOverlay,onClick:onClose,children:JSX.jsxs('section',{className:WS.dialogContainer,role:'dialog','aria-modal':true,'aria-label':resolveShareExpertName(expert,locale),onClick:e=>e.stopPropagation(),children:[JSX.jsx('div',{className:WS.dialogHeader,children:resolveShareExpertName(expert,locale)}),JSX.jsx('div',{className:WS.dialogContent,children:resolveCardI18nText(expert.description,locale)}),JSX.jsx(Ku,{onClick:onClose,children:'关闭'})]})}),document.body);return JSX.jsx('div',{style:{display:'contents'},onClick:e=>e.stopPropagation(),onKeyDown:e=>e.stopPropagation(),children:content})}
|
||||
return {Card:CreatedExpertCard,Panel:CreatedExpertsPanel};
|
||||
}
|
||||
const defaultBindings=createBindings(defaultRef),Context=React.createContext({ref:defaultRef,bindings:defaultBindings});
|
||||
export function CreatedExpertSourceProvider({host,children}){const ref=React.useRef(host??{});ref.current=host??{};const bindings=React.useMemo(()=>createBindings(ref),[]);const value=React.useMemo(()=>({ref,bindings}),[bindings]);return JSX.jsx(Context.Provider,{value,children:JSX.jsx('div',{className:'yc-created-expert-source',style:{display:'contents'},children})})}
|
||||
function useLocalCallbacks(props,ref){const fail=React.useCallback(()=>ref.current.notify?.('操作失败,请重试','warning'),[ref]);const onDelete=React.useCallback(async id=>{try{return await props.onDelete?.(id)}catch{fail();return false}},[props.onDelete,fail]);const onEdit=React.useCallback(expert=>{try{Promise.resolve(props.onEdit?.(expert)).catch(fail)}catch{fail()}},[props.onEdit,fail]);const onUse=React.useCallback((id,prompt)=>{try{Promise.resolve(props.onUse?.(id,prompt)).catch(fail)}catch{fail()}},[props.onUse,fail]);return{onUse,onDelete:props.onDelete?onDelete:undefined,onEdit:props.onEdit?onEdit:undefined}}
|
||||
export function CreatedExpertCard(props){const{ref,bindings}=React.useContext(Context);const callbacks=useLocalCallbacks(props,ref);return JSX.jsx(bindings.Card,{expert:props.expert,...callbacks,isMenuOpen:props.isMenuOpen,onToggleMenu:props.onToggleMenu,highlightKeyword:props.highlightKeyword})}
|
||||
export function CreatedExpertsPanel(props){const{ref,bindings}=React.useContext(Context);const callbacks=useLocalCallbacks(props,ref);return JSX.jsx(bindings.Panel,{createdExperts:props.createdExperts,activeTab:props.activeTab,onTabChange:props.onTabChange,...callbacks,searchKeyword:props.searchKeyword,toolbarExtra:props.toolbarExtra})}
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none">
|
||||
<path fill="#DEDEDE" fill-rule="evenodd" transform="translate(3.24072 1.71484)" d="M40.7627 6.0145C40.6641 5.6626 40.6147 5.2879 40.6147 4.8904L40.6147 2.9278C40.6147 2.2947 40.751 1.7059 41.0229 1.1614C41.0605 1.0862 41.1006 1.0127 41.1426 0.9407C37.9795 0.1622 34.1631 -0.133 29.6943 0.0547Q14.1333 0.7084 5.9067 6.0848C4.0728 7.2833 2.6079 8.6004 1.5132 10.0367C0.8955 10.8462 0.4727 11.5704 0.2437 12.2093L0 12.8895L0.1567 13.595L0.1567 13.5953C0.3691 14.5503 0.7676 15.6055 1.3516 16.761Q3.1138 20.2455 6.0728 22.2491C8.8462 24.1268 12.1445 24.8421 15.9678 24.3948C20.3662 23.8799 25.3647 21.7974 30.9624 18.1465L28.231 13.9586C23.2891 17.1814 19.0078 19.0049 15.3867 19.4285C12.7939 19.732 10.6235 19.292 8.876 18.1089C7.6191 17.2579 6.5981 16.0564 5.814 14.5049C5.6001 14.0818 5.4258 13.6932 5.292 13.3387C5.353 13.251 5.4185 13.1609 5.4888 13.0684C6.2368 12.0879 7.2876 11.1553 8.6416 10.2703C10.6592 8.952 13.1743 7.8689 16.186 7.0215C20.0723 5.9283 24.645 5.2713 29.9043 5.0503C34.3306 4.8643 37.9502 5.1856 40.7627 6.0145ZM53.6965 12.6924H44.4979C44.2361 12.6924 44.024 12.618 43.8617 12.4693C43.6853 12.3077 43.5972 12.0902 43.5972 11.817V9.7521Q43.5972 9.3116 43.9359 8.9383L43.964 8.9074L48.3864 5.4228H44.5153C44.2486 5.4228 44.0325 5.3417 43.8671 5.1797C43.6987 5.0148 43.6146 4.7983 43.6146 4.5303V2.5678C43.6146 2.2945 43.7028 2.0771 43.8792 1.9155C44.0414 1.7667 44.2535 1.6924 44.5153 1.6924H53.6442C53.9109 1.6924 54.1241 1.7706 54.2837 1.9269C54.4462 2.0861 54.5275 2.2997 54.5275 2.5678V4.6327C54.5275 4.9264 54.4145 5.1976 54.1887 5.4465L54.1606 5.4774L49.7382 8.962H53.6965C53.9632 8.962 54.1793 9.043 54.3447 9.2051C54.513 9.3699 54.5972 9.5864 54.5972 9.8545V11.817C54.5972 12.0902 54.509 12.3077 54.3326 12.4693C54.1703 12.618 53.9583 12.6924 53.6965 12.6924ZM44.024 21.6924H38.1703C38.0037 21.6924 37.8688 21.6451 37.7655 21.5504C37.6533 21.4476 37.5972 21.3092 37.5972 21.1353V19.8213C37.5972 19.6344 37.669 19.4618 37.8127 19.3034L37.8306 19.2837L40.6449 17.0662H38.1814C38.0117 17.0663 37.8742 17.0147 37.7689 16.9116C37.6618 16.8067 37.6083 16.6689 37.6082 16.4983V15.2495C37.6083 15.0756 37.6644 14.9372 37.7766 14.8343C37.8799 14.7397 38.0148 14.6924 38.1814 14.6924H43.9907C44.1605 14.6924 44.2961 14.7421 44.3977 14.8416C44.5011 14.9429 44.5528 15.0789 44.5528 15.2495V16.5635C44.5528 16.7504 44.4809 16.923 44.3372 17.0814L44.3194 17.101L41.5051 19.3185H44.024C44.1937 19.3185 44.3312 19.3701 44.4365 19.4732C44.5436 19.5781 44.5972 19.7159 44.5972 19.8864V21.1353C44.5972 21.3092 44.5411 21.4476 44.4288 21.5504C44.3255 21.6451 44.1906 21.6924 44.024 21.6924ZM49.6431 16.0525C49.8457 16.8597 50.0249 17.7229 50.1812 18.6424C50.501 20.5276 50.8198 23.5677 51.1377 27.7623C50.1338 27.9717 49.0942 28.169 48.0972 28.3255C46.5605 28.5665 44.4507 28.9151 42.8306 29.1468C42.4292 28.3077 41.8672 27.544 41.1372 26.9056C36.3418 22.71 8.6353 25.3323 4.9053 30.5767C1.6909 35.0965 4.8086 42.3431 9.8242 43.5879C9.9263 44.0696 10.0581 44.594 10.2192 45.1614C10.8345 47.3257 11.7041 49.3248 12.8296 51.1587C14.4365 53.7771 16.4565 55.8672 18.8896 57.4288Q23.4285 60.3412 29.5112 60.5511Q35.3713 60.7532 40.3774 58.9793C43.7046 57.8006 46.5874 56.0269 49.0264 53.6585C51.4746 51.2813 53.3452 48.431 54.6382 45.1072C55.9443 41.75 56.5972 38.065 56.5972 34.0523V33.9564L56.5898 33.8607C56.5522 33.3702 56.5073 32.762 56.4551 32.0359Q56.5979 32.0096 56.7246 31.9861C57.2261 31.8936 57.5605 31.4239 57.5146 30.9161L57.2056 27.5191C57.1528 26.9385 56.6255 26.5315 56.0635 26.6087C55.752 22.648 55.4346 19.7137 55.1104 17.8057C55.0005 17.1582 54.8794 16.5313 54.7466 15.9249C54.416 16.0098 54.0659 16.0525 53.6963 16.0525H49.6431ZM49.5972 33.3255C50.2397 33.2022 50.8828 33.0799 51.5088 32.9615C51.5415 33.409 51.5708 33.804 51.5972 34.1463C51.5874 37.4964 51.0474 40.5457 49.978 43.2945C48.9487 45.9412 47.4702 48.2002 45.543 50.0716C43.6265 51.9324 41.3481 53.3309 38.7075 54.2664C35.9663 55.2378 32.958 55.667 29.6831 55.554C24.0371 55.3594 19.8398 53.0225 17.0913 48.5438C16.2061 47.1009 15.5186 45.5179 15.0288 43.795C14.9609 43.5552 14.8999 43.3292 14.8462 43.1163C21.9502 40.9144 20.019 35.6553 23.8711 35.1343C25.6899 34.8885 26.7324 36.2 27.933 37.7104L27.9331 37.7105C29.5361 39.7266 31.4209 42.097 35.8091 41.5901C40.1113 41.0933 42.7715 37.858 43.4341 34.3712C44.8784 34.1541 47.1812 33.7884 49.5972 33.3255ZM28.0971 44.6923H23.5971C23.0382 44.6762 22.5668 44.8715 22.1829 45.2781C21.7763 45.662 21.581 46.1334 21.5971 46.6923C21.581 47.2513 21.7763 47.7227 22.1829 48.1065C22.5668 48.5131 23.0382 48.7084 23.5971 48.6923H28.0971C28.6561 48.7084 29.1275 48.5131 29.5113 48.1065C29.9179 47.7227 30.1132 47.2513 30.0971 46.6923C30.1132 46.1334 29.9179 45.662 29.5113 45.2781C29.1275 44.8715 28.6561 44.6762 28.0971 44.6923Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"tz": {
|
||||
"page": "_page_1yu3o_7",
|
||||
"inactive": "_inactive_1yu3o_17",
|
||||
"layout": "_layout_1yu3o_20",
|
||||
"main": "_main_1yu3o_26",
|
||||
"topbar": "_topbar_1yu3o_35",
|
||||
"topbarLeft": "_topbarLeft_1yu3o_43",
|
||||
"topbarNavDivider": "_topbarNavDivider_1yu3o_50",
|
||||
"topbarRight": "_topbarRight_1yu3o_57",
|
||||
"topbarActionBtn": "_topbarActionBtn_1yu3o_69",
|
||||
"topbarRightMyExperts": "_topbarRightMyExperts_1yu3o_72",
|
||||
"topbarSubpage": "_topbarSubpage_1yu3o_75",
|
||||
"subpageNav": "_subpageNav_1yu3o_75",
|
||||
"subpageBack": "_subpageBack_1yu3o_91",
|
||||
"subpageTitle": "_subpageTitle_1yu3o_99",
|
||||
"mainScroll": "_mainScroll_1yu3o_110",
|
||||
"myExpertsScroll": "_myExpertsScroll_1yu3o_131",
|
||||
"mainScrollSplit": "_mainScrollSplit_1yu3o_134",
|
||||
"categoryTabsPlaceholder": "_categoryTabsPlaceholder_1yu3o_151",
|
||||
"expertScroll": "_expertScroll_1yu3o_156",
|
||||
"pageError": "_pageError_1yu3o_159",
|
||||
"pageErrorMsg": "_pageErrorMsg_1yu3o_169",
|
||||
"listError": "_listError_1yu3o_174",
|
||||
"listErrorMsg": "_listErrorMsg_1yu3o_183",
|
||||
"loadMoreSpinner": "_loadMoreSpinner_1yu3o_188",
|
||||
"loadMoreSentinel": "_loadMoreSentinel_1yu3o_194"
|
||||
},
|
||||
"EI": {
|
||||
"row": "_row_ki2oo_1",
|
||||
"tabs": "_tabs_ki2oo_8",
|
||||
"actions": "_actions_ki2oo_14",
|
||||
"tab": "_tab_ki2oo_8",
|
||||
"active": "_active_ki2oo_42",
|
||||
"sortGroup": "_sortGroup_ki2oo_45",
|
||||
"sortBtn": "_sortBtn_ki2oo_57"
|
||||
},
|
||||
"UF": {
|
||||
"wrap": "_wrap_zzafx_1",
|
||||
"maskedStart": "_maskedStart_zzafx_27",
|
||||
"maskedEnd": "_maskedEnd_zzafx_28",
|
||||
"tabs": "_tabs_zzafx_35",
|
||||
"dragging": "_dragging_zzafx_54",
|
||||
"next": "_next_zzafx_58",
|
||||
"nextIcon": "_nextIcon_zzafx_82",
|
||||
"prev": "_prev_zzafx_89",
|
||||
"prevIcon": "_prevIcon_zzafx_93",
|
||||
"tab": "_tab_zzafx_35",
|
||||
"active": "_active_zzafx_117"
|
||||
},
|
||||
"lI": {
|
||||
"content": "_content_1tpa4_1",
|
||||
"animated": "_animated_1tpa4_4",
|
||||
"grid-content-in": "_grid-content-in_1tpa4_1",
|
||||
"grid": "_grid_1tpa4_18",
|
||||
"skeleton": "_skeleton_1tpa4_61",
|
||||
"expandWrap": "_expandWrap_1tpa4_64",
|
||||
"expandBtn": "_expandBtn_1tpa4_69",
|
||||
"expandIcon": "_expandIcon_1tpa4_92",
|
||||
"expanded": "_expanded_1tpa4_99",
|
||||
"searchHint": "_searchHint_1tpa4_102",
|
||||
"keyword": "_keyword_1tpa4_107",
|
||||
"count": "_count_1tpa4_111"
|
||||
},
|
||||
"tI": {
|
||||
"card": "_card_1hdcm_2",
|
||||
"fade-in-up": "_fade-in-up_1hdcm_1",
|
||||
"skeleton": "_skeleton_1hdcm_33",
|
||||
"selectable": "_selectable_1hdcm_41",
|
||||
"selected": "_selected_1hdcm_45",
|
||||
"disabled": "_disabled_1hdcm_55",
|
||||
"centerCard": "_centerCard_1hdcm_60",
|
||||
"main": "_main_1hdcm_91",
|
||||
"head": "_head_1hdcm_97",
|
||||
"body": "_body_1hdcm_101",
|
||||
"titleRow": "_titleRow_1hdcm_104",
|
||||
"role": "_role_1hdcm_109",
|
||||
"tags": "_tags_1hdcm_118",
|
||||
"tag": "_tag_1hdcm_73",
|
||||
"tagPaid": "_tagPaid_1hdcm_74",
|
||||
"topActions": "_topActions_1hdcm_144",
|
||||
"metadata": "_metadata_1hdcm_148",
|
||||
"centerCardWithoutMetadata": "_centerCardWithoutMetadata_1hdcm_151",
|
||||
"subtitle": "_subtitle_1hdcm_225",
|
||||
"desc": "_desc_1hdcm_226",
|
||||
"roleWrap": "_roleWrap_1hdcm_239",
|
||||
"importSecurityBadge": "_importSecurityBadge_1hdcm_258",
|
||||
"safe": "_safe_1hdcm_268",
|
||||
"low": "_low_1hdcm_272",
|
||||
"medium": "_medium_1hdcm_276",
|
||||
"importSecurityIcon": "_importSecurityIcon_1hdcm_280",
|
||||
"operationalTag": "_operationalTag_1hdcm_284",
|
||||
"operationalTagIconBackground": "_operationalTagIconBackground_1hdcm_306",
|
||||
"operationalTagIcon": "_operationalTagIcon_1hdcm_306",
|
||||
"operationalTagText": "_operationalTagText_1hdcm_322",
|
||||
"centerDescription": "_centerDescription_1hdcm_341",
|
||||
"usage": "_usage_1hdcm_362",
|
||||
"usageIcon": "_usageIcon_1hdcm_368",
|
||||
"footer": "_footer_1hdcm_376",
|
||||
"titleActions": "_titleActions_1hdcm_384",
|
||||
"summonBtn": "_summonBtn_1hdcm_408",
|
||||
"summonBtnTop": "_summonBtnTop_1hdcm_441",
|
||||
"selectedBadge": "_selectedBadge_1hdcm_450"
|
||||
},
|
||||
"_L": {
|
||||
"scrollShell": "_scrollShell_944zj_1",
|
||||
"body": "_body_944zj_8",
|
||||
"scrollFade": "_scrollFade_944zj_23",
|
||||
"scrollFadeTop": "_scrollFadeTop_944zj_33",
|
||||
"scrollFadeBottom": "_scrollFadeBottom_944zj_37",
|
||||
"hasTopFade": "_hasTopFade_944zj_41",
|
||||
"hasBottomFade": "_hasBottomFade_944zj_42",
|
||||
"top": "_top_944zj_45",
|
||||
"avatar": "_avatar_944zj_53",
|
||||
"avatarImagePending": "_avatarImagePending_944zj_72",
|
||||
"avatarFallback": "_avatarFallback_944zj_143",
|
||||
"info": "_info_944zj_155",
|
||||
"titleRow": "_titleRow_944zj_159",
|
||||
"actions": "_actions_944zj_167",
|
||||
"role": "_role_944zj_174",
|
||||
"rolePrimary": "_rolePrimary_944zj_188",
|
||||
"roleSecondary": "_roleSecondary_944zj_189",
|
||||
"roleSeparator": "_roleSeparator_944zj_196",
|
||||
"cloudBadge": "_cloudBadge_944zj_205",
|
||||
"headerSummonBtn": "_headerSummonBtn_944zj_125",
|
||||
"sectionTitle": "_sectionTitle_944zj_237",
|
||||
"content": "_content_944zj_240",
|
||||
"desc": "_desc_944zj_246",
|
||||
"tagsList": "_tagsList_944zj_252",
|
||||
"tagChip": "_tagChip_944zj_80",
|
||||
"metadataRow": "_metadataRow_944zj_269",
|
||||
"metadataItem": "_metadataItem_944zj_286",
|
||||
"metadataValue": "_metadataValue_944zj_283",
|
||||
"metadataValueWithIcon": "_metadataValueWithIcon_944zj_309",
|
||||
"metadataUsageIcon": "_metadataUsageIcon_944zj_315",
|
||||
"metadataItemClickable": "_metadataItemClickable_944zj_331",
|
||||
"metadataValueText": "_metadataValueText_944zj_283",
|
||||
"metadataDeveloperJumpIcon": "_metadataDeveloperJumpIcon_944zj_343",
|
||||
"metadataLabel": "_metadataLabel_944zj_352",
|
||||
"quickPrompts": "_quickPrompts_944zj_359",
|
||||
"sectionTitleWithIcon": "_sectionTitleWithIcon_944zj_365",
|
||||
"sectionIcon": "_sectionIcon_944zj_377",
|
||||
"teamMembersIcon": "_teamMembersIcon_944zj_383",
|
||||
"team": "_team_944zj_383",
|
||||
"more": "_more_944zj_394",
|
||||
"moreTitle": "_moreTitle_944zj_400",
|
||||
"moreTitleIcon": "_moreTitleIcon_944zj_410",
|
||||
"moreBar": "_moreBar_944zj_419",
|
||||
"moreLine": "_moreLine_944zj_428",
|
||||
"close": "_close_944zj_442"
|
||||
},
|
||||
"KF": {
|
||||
"root": "_root_pmhio_1",
|
||||
"icon": "_icon_pmhio_9",
|
||||
"message": "_message_pmhio_15"
|
||||
},
|
||||
"Vvt": {
|
||||
"root": "_root_mbn4e_1"
|
||||
},
|
||||
"NR": {
|
||||
"overlay": "_overlay_p0cpn_1",
|
||||
"overlay-in": "_overlay-in_p0cpn_1",
|
||||
"card": "_card_p0cpn_20",
|
||||
"modal-in": "_modal-in_p0cpn_1",
|
||||
"expertView": "_expertView_p0cpn_40",
|
||||
"caseView": "_caseView_p0cpn_49"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type {ComponentType,SVGProps} from 'react';
|
||||
export const UnifiedMarketHeader:ComponentType<{activeTab:'experts'|'skills'|'connectors';onTabChange(tab:'experts'|'skills'|'connectors'):void}>;
|
||||
export const ExpertSearchBar:ComponentType<{value:string;onChange(value:string):void;placeholder?:string;variant?:string}>;
|
||||
export const ExpertListEmptyState:ComponentType<{message:string;showIcon?:boolean}>;
|
||||
export const ExpertCard:ComponentType<{expert:Record<string,unknown>;avatarUrl?:string;cardPosition:number;onSummon():void;onExpertClick(expert:Record<string,unknown>):void;appearance?:string;highlightKeyword?:string;hideTopSummonButton?:boolean;priorityAvatarLoad?:boolean;isFirst?:boolean}>;
|
||||
export const ExpertIcon:ComponentType<SVGProps<SVGSVGElement>>;export const SkillIcon:typeof ExpertIcon;export const ConnectorIcon:typeof ExpertIcon;
|
||||
export function setExpertSearchPrimitive(value:ComponentType<any>):void;
|
||||
export function filterExpertsByKeyword<T>(items:T[],keyword:string,locale:string):T[];
|
||||
export function formatExpertUseCount(value:number,locale:string):string;
|
||||
export function formatExpertVersion(value:string):string;
|
||||
export const BackIcon:typeof ExpertIcon;export const AddIcon:typeof ExpertIcon;export const MyExpertIcon:typeof ExpertIcon;
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as React from 'react';
|
||||
import * as JSX from 'react/jsx-runtime';
|
||||
const FD=JSX,mI=React,Uvt=JSX,qF=JSX,nI=React,rI=JSX,YF=React,XF=JSX,JF=React,p_t=JSX,ivt='1200px',avt=4,xMe=8;
|
||||
const Vvt={"root":"_root_mbn4e_1"},KF={"root":"_root_pmhio_1","icon":"_icon_pmhio_9","message":"_message_pmhio_15"},Wvt=300;
|
||||
const tI={"card":"_card_1hdcm_2","fade-in-up":"_fade-in-up_1hdcm_1","skeleton":"_skeleton_1hdcm_33","selectable":"_selectable_1hdcm_41","selected":"_selected_1hdcm_45","disabled":"_disabled_1hdcm_55","centerCard":"_centerCard_1hdcm_60","main":"_main_1hdcm_91","head":"_head_1hdcm_97","body":"_body_1hdcm_101","titleRow":"_titleRow_1hdcm_104","role":"_role_1hdcm_109","tags":"_tags_1hdcm_118","tag":"_tag_1hdcm_73","tagPaid":"_tagPaid_1hdcm_74","topActions":"_topActions_1hdcm_144","metadata":"_metadata_1hdcm_148","centerCardWithoutMetadata":"_centerCardWithoutMetadata_1hdcm_151","subtitle":"_subtitle_1hdcm_225","desc":"_desc_1hdcm_226","roleWrap":"_roleWrap_1hdcm_239","importSecurityBadge":"_importSecurityBadge_1hdcm_258","safe":"_safe_1hdcm_268","low":"_low_1hdcm_272","medium":"_medium_1hdcm_276","importSecurityIcon":"_importSecurityIcon_1hdcm_280","operationalTag":"_operationalTag_1hdcm_284","operationalTagIconBackground":"_operationalTagIconBackground_1hdcm_306","operationalTagIcon":"_operationalTagIcon_1hdcm_306","operationalTagText":"_operationalTagText_1hdcm_322","centerDescription":"_centerDescription_1hdcm_341","usage":"_usage_1hdcm_362","usageIcon":"_usageIcon_1hdcm_368","footer":"_footer_1hdcm_376","titleActions":"_titleActions_1hdcm_384","summonBtn":"_summonBtn_1hdcm_408","summonBtnTop":"_summonBtnTop_1hdcm_441","selectedBadge":"_selectedBadge_1hdcm_450"};
|
||||
function useExpertI18n(){return (key,values)=>({searchPlaceholder:'搜索专家',clearSearch:'清除搜索',summonShort:'召唤',summonExpertWithName:'查看专家 '+(values?.name??''),summonTeamWithName:'查看专家团 '+(values?.name??''),'card.paidTag':'付费'}[key]??key)}
|
||||
function useDebouncedValue$1(value,delay){const [v,set]=React.useState(value);React.useEffect(()=>{const t=setTimeout(()=>set(value),delay);return()=>clearTimeout(t)},[value,delay]);return v}
|
||||
let SearchPrimitive;export function setExpertSearchPrimitive(value){SearchPrimitive=value}
|
||||
const ka={Search:props=>JSX.jsx(SearchPrimitive,props)};
|
||||
const _o=({content,children})=>React.cloneElement(children,{title:content});
|
||||
const Ao=({iconUrl,fallback,size})=>iconUrl?JSX.jsx('img',{src:iconUrl,width:size,height:size,alt:''}):fallback;
|
||||
const ID=20;
|
||||
const Nu=React,Pu=JSX;
|
||||
const Fu=(0,Nu.forwardRef)((r,s)=>(0,Pu.jsx)(`svg`,{ref:s,viewBox:`0 0 16 16`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`,...r,children:(0,Pu.jsx)(`path`,{transform:`matrix(1 0 0 1 1.36963 0.768944)`,fillRule:`evenodd`,d:`M7.1035 0C8.6643 0 10.7053 0.0099 12.0742 1.5205C13.2979 2.8714 13.3086 4.7621 13.3086 6.1885L13.3086 8.1777C13.3086 10.1172 12.4294 11.8523 11.0498 13.004C8.8995 14.7989 5.3968 15.0938 3.2227 13.0078C2.3428 12.1634 1.8536 11.0199 1.7246 9.7939C0.8046 9.5348 0.112 8.7337 0.0127 7.7568L0 7.5146C0.0001 6.2068 1.0603 5.1465 2.3682 5.1465L2.6104 5.1592C3.6191 5.2617 4.4383 5.9978 4.6689 6.9629L5.6562 6.9629C5.9052 5.9216 6.8394 5.1465 7.957 5.1465L8.1992 5.1592C9.3932 5.2805 10.3251 6.2887 10.3252 7.5146L10.3125 7.7568C10.1912 8.9509 9.1831 9.8828 7.957 9.8828L7.7148 9.8701C6.7393 9.7711 5.939 9.0803 5.6787 8.1621L4.6445 8.1621C4.4136 8.9759 3.7594 9.6106 2.9346 9.8134C3.0599 10.7423 3.4425 11.555 4.0537 12.1416C5.6974 13.7188 8.498 13.5697 10.2803 12.0821C11.3984 11.1486 12.1084 9.7464 12.1084 8.1777L12.1084 6.1885C12.1084 4.677 12.0504 3.281 11.1856 2.3262C10.2269 1.2683 8.7608 1.1992 7.1035 1.1992C5.2192 1.1992 3.708 1.2459 2.6699 1.292C2.179 1.3138 1.7933 1.3357 1.5244 1.3525C1.4552 1.8424 1.5142 2.1892 1.625 2.4326C1.7619 2.733 2.0131 2.9569 2.3867 3.1113C3.1734 3.4364 4.3684 3.392 5.4512 3.1201L5.7441 4.2842C4.5613 4.5812 3.0644 4.6898 1.9287 4.2207C1.3415 3.978 0.8226 3.567 0.5322 2.9297C0.2457 2.2999 0.2194 1.5309 0.4434 0.6396L0.4736 0.5488C0.5603 0.3473 0.7528 0.2058 0.9766 0.1875L0.9785 0.1875C0.9785 0.1875 0.9822 0.1867 0.9844 0.1865C0.9892 0.1862 0.997 0.1862 1.0059 0.1855C1.0243 0.1841 1.0517 0.1813 1.0869 0.1787C1.1586 0.1735 1.265 0.166 1.4033 0.1572C1.6808 0.1396 2.0904 0.1162 2.6172 0.0928C3.6711 0.046 5.1999 0 7.1035 0ZM2.3682 6.3467C1.7231 6.3467 1.2003 6.8696 1.2002 7.5146C1.2002 8.1598 1.723 8.6826 2.3682 8.6826C3.0133 8.6826 3.5361 8.1598 3.5361 7.5146C3.5361 6.8696 3.0132 6.3467 2.3682 6.3467ZM7.957 6.3467C7.312 6.3467 6.7892 6.8696 6.7891 7.5146C6.7891 8.1598 7.3119 8.6826 7.957 8.6826C8.6021 8.6825 9.125 8.1598 9.125 7.5146C9.1249 6.8697 8.602 6.3468 7.957 6.3467Z`})}));
|
||||
const UF=React,WF=JSX;
|
||||
const GF=(0,UF.forwardRef)((r,s)=>(0,WF.jsx)(`svg`,{ref:s,viewBox:`0 0 16 16`,xmlns:`http://www.w3.org/2000/svg`,fill:`none`,...r,children:(0,WF.jsx)(`path`,{fill:`currentColor`,fillRule:`evenodd`,transform:`matrix(1 0 0 1 0.485779 0.117911)`,d:`M4.6063 4.6987Q4.7494 4.7505 4.8255 4.7783Q5.7193 5.1048 6.5463 5.5015L0.59 11.4578Q0 12.0478 0 12.8821Q0 13.7164 0.59 14.3064Q1.1799 14.8963 2.0142 14.8963Q2.8485 14.8963 3.4385 14.3064L8.8444 8.9005L7.9958 8.052L2.59 13.4578Q2.3515 13.6963 2.0142 13.6963Q1.677 13.6963 1.4385 13.4578Q1.2 13.2193 1.2 12.8821Q1.2 12.5448 1.4385 12.3064L7.6601 6.0847Q8.8609 6.7696 9.9038 7.6204Q12.3371 9.6056 13.2462 12.0051Q13.3286 12.2429 13.5609 12.3395Q13.7872 12.4495 14.0199 12.3536Q14.194 12.2902 14.2984 12.1372Q14.4068 11.987 14.4072 11.8017Q14.459 8.4358 12.2057 5.3174Q9.969 2.2218 6.2641 0.4907Q5.6636 0.2101 5.415 0.1409Q4.9087 0 4.4826 0.1859Q4.1015 0.3522 3.8452 0.7962Q3.7204 1.0124 3.4651 1.6298L3.4638 1.633L3.4222 1.7337Q3.4095 1.7643 3.3849 1.8238Q3.1054 2.4987 3.0381 2.7676Q2.8998 3.3203 3.1034 3.7425Q3.2831 4.1152 3.7555 4.3571Q3.9811 4.4726 4.6063 4.6987ZM10.6624 6.6906Q8.315 4.7755 5.2372 3.6511Q5.1593 3.6227 5.0145 3.5703Q4.2127 3.2802 4.1843 3.2213Q4.1416 3.1328 4.4936 2.2829Q4.5183 2.2232 4.5311 2.1922L4.5728 2.0915L4.5741 2.0883Q4.8934 1.3159 4.9625 1.2858Q5.0496 1.2478 5.7561 1.5779Q9.1787 3.1771 11.233 6.0201Q12.1806 7.3316 12.6737 8.6809Q11.8234 7.6378 10.6624 6.6906ZM10.0312 10.5647C9.9585 10.3032 9.5796 10.3284 9.4539 10.6029L8.8284 11.9696C8.7889 12.0557 8.7157 12.1262 8.6286 12.163L7.2451 12.7441C6.9676 12.861 6.9301 13.2405 7.1879 13.3222L8.473 13.7281C8.5541 13.7537 8.6138 13.8161 8.6366 13.8983L8.998 15.1995C9.0706 15.461 9.4496 15.4358 9.5753 15.1612L10.2008 13.7945C10.2404 13.7084 10.3135 13.6379 10.4006 13.6012L11.7831 13.0201C12.0609 12.9032 12.0984 12.5235 11.8403 12.442L10.5562 12.0361C10.4752 12.0105 10.4155 11.9488 10.3926 11.8669L10.0312 10.5647Z`})}));
|
||||
const yl=React,bl=JSX;
|
||||
const xl=(0,yl.forwardRef)((r,s)=>(0,bl.jsx)(`svg`,{ref:s,viewBox:`0 0 16 16`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,...r,children:(0,bl.jsx)(`path`,{fill:`currentColor`,transform:`matrix(1 0 0 1 0.435199 0.715962)`,d:`M7.5656 4.2827C8.2868 4.6991 8.7999 5.2128 9.1214 5.8068C9.4424 6.4 9.5528 7.0359 9.5284 7.6644C9.5192 7.8997 9.4911 8.1358 9.4474 8.3709L8.3226 7.7215C8.325 7.6869 8.3279 7.6525 8.3293 7.6183C8.3469 7.1636 8.2661 6.7484 8.0655 6.3778C7.8653 6.0082 7.5257 5.6444 6.966 5.3213C6.4063 4.9981 5.9214 4.8859 5.5013 4.8973C5.08 4.9089 4.68 5.0465 4.2951 5.2892C3.5056 5.787 2.8232 6.6961 2.254 7.6819C1.6848 8.6678 1.2387 9.7133 1.2024 10.6459C1.1847 11.1006 1.2655 11.5158 1.4661 11.8864C1.6663 12.256 2.0059 12.6198 2.5656 12.9429C3.1254 13.2661 3.6102 13.3783 4.0304 13.3669C4.4516 13.3553 4.8516 13.2177 5.2365 12.975C5.7924 12.6245 6.2937 12.0689 6.7412 11.4273L7.784 12.0294C7.2672 12.7855 6.6365 13.511 5.876 13.9904C5.3439 14.3258 4.738 14.5482 4.0638 14.5668C3.3886 14.5853 2.6872 14.3978 1.966 13.9815C1.2448 13.5651 0.7317 13.0514 0.4102 12.4574C0.0892 11.8642 -0.0211 11.2283 0.0033 10.5998C0.0512 9.3663 0.6211 8.1118 1.2154 7.0823C1.8098 6.0528 2.6114 4.932 3.6556 4.2738C4.1877 3.9384 4.7936 3.716 5.4678 3.6974C6.143 3.6789 6.8445 3.8663 7.5656 4.2827ZM13.1637 0.5865C13.8848 1.0029 14.3979 1.5167 14.7195 2.1106C15.0405 2.7038 15.1508 3.3397 15.1264 3.9683C15.0785 5.2017 14.5086 6.4563 13.9142 7.4858C13.3199 8.5153 12.5183 9.636 11.4741 10.2943C10.942 10.6296 10.3361 10.8521 9.6618 10.8706C8.9867 10.8892 8.2852 10.7017 7.5641 10.2853C6.8429 9.869 6.3298 9.3552 6.0083 8.7613C5.6873 8.1681 5.5769 7.5322 5.6013 6.9037C5.6105 6.6684 5.6386 6.4323 5.6823 6.1971L6.8071 6.8465C6.8046 6.8811 6.8018 6.9155 6.8004 6.9498C6.7828 7.4045 6.8636 7.8197 7.0642 8.1903C7.2643 8.5599 7.6039 8.9236 8.1637 9.2468C8.7234 9.5699 9.2083 9.6822 9.6284 9.6707C10.0497 9.6591 10.4496 9.5215 10.8346 9.2789C11.6241 8.7811 12.3065 7.872 12.8757 6.8862C13.4449 5.9003 13.8909 4.8548 13.9273 3.9221C13.945 3.4674 13.8642 3.0523 13.6636 2.6816C13.4634 2.3121 13.1238 1.9483 12.5641 1.6251C12.0043 1.3019 11.5195 1.1897 11.0993 1.2012C10.6781 1.2128 10.2781 1.3504 9.8932 1.593C9.3373 1.9435 8.836 2.4991 8.3884 3.1407L7.3456 2.5387C7.8624 1.7825 8.4932 1.057 9.2537 0.5776C9.7858 0.2422 10.3917 0.0198 11.0659 0.0013C11.7411 -0.0173 12.4425 0.1702 13.1637 0.5865Z`})}));
|
||||
const wM=React,TM=JSX;
|
||||
const EM=(0,wM.forwardRef)((r,s)=>(0,TM.jsx)(`svg`,{ref:s,viewBox:`0 0 16 16`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`,...r,children:(0,TM.jsx)(`path`,{transform:`translate(1.31421, 2.07574)`,fillRule:`evenodd`,d:`M5.2615 11.8485L1.1758 7.7627L1.1564 7.7434Q0.5569 7.1439 0.3684 6.8959Q0 6.411 0 5.9243Q0 5.4375 0.3684 4.9526Q0.5569 4.7046 1.1564 4.1051L1.1758 4.0858L5.2615 0L6.11 0.8485L2.0243 4.9343L2.0049 4.9536Q1.7986 5.1599 1.6452 5.3242L12.6858 5.3242L12.6858 6.5243L1.6452 6.5243Q1.7986 6.6886 2.0049 6.8949L2.0146 6.9045L2.0243 6.9142L6.11 11L5.2615 11.8485Z`})}));
|
||||
const Rn=React,zn=JSX;
|
||||
const Bn=(0,Rn.forwardRef)((r,s)=>(0,zn.jsx)(`svg`,{ref:s,viewBox:`0 0 16 16`,fill:`none`,...r,children:(0,zn.jsx)(`path`,{d:`M8 3V13M3 8H13`,stroke:`currentColor`,strokeWidth:`1.5`,strokeLinecap:`round`})}));
|
||||
const qm=React,Jm=JSX;
|
||||
const Ym=(0,qm.forwardRef)((r,s)=>(0,Jm.jsx)(`svg`,{ref:s,viewBox:`0 0 16 16`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,...r,children:(0,Jm.jsx)(`path`,{fill:`currentColor`,transform:`matrix(1 0 0 1 1.46815 0.938622)`,d:`M1.4068 3.9801L1.4069 6.0292L1.4073 9.4832Q1.4073 9.8118 1.4116 9.9148Q1.4911 11.8282 2.8489 13.1841Q4.206 14.5394 6.1197 14.6185Q6.2226 14.6227 6.5501 14.6227Q6.9953 14.6227 7.1318 14.6171Q9.6669 14.5122 11.4642 12.7149Q13.2616 10.9176 13.3665 8.3824Q13.3721 8.246 13.3721 7.8008L13.3721 6.262Q13.3722 4.3022 13.2222 3.7094Q12.8911 2.4003 11.9315 1.4407Q10.9719 0.4811 9.6629 0.15Q9.07 0 7.1102 0L0.5985 0L0.4147 0.3676Q0.2823 0.6323 0.1899 1.0022Q0 1.7617 0.1649 2.4214Q0.4235 3.4555 1.4068 3.9801ZM2.7368 4.4082Q3.3886 4.5203 4.1998 4.5203L4.1998 3.1903Q1.7281 3.1903 1.4552 2.0988Q1.3602 1.7187 1.4634 1.33L7.1102 1.33Q8.9044 1.33 9.3367 1.4394Q10.2906 1.6806 10.9911 2.3811Q11.6916 3.0816 11.9328 4.0355Q12.0422 4.4678 12.0422 6.262L12.0421 7.8008L12.0421 7.8008Q12.0421 8.2185 12.0376 8.3275Q11.9542 10.3441 10.5238 11.7745Q9.0934 13.2048 7.0768 13.2882Q6.9678 13.2927 6.5501 13.2928Q6.2501 13.2927 6.1746 13.2896Q4.7789 13.232 3.7887 12.243Q2.7984 11.2541 2.7405 9.8596Q2.7373 9.7841 2.7373 9.4831L2.7369 6.0291L2.7368 4.4082ZM5.4619 8.1614L5.4619 5.7614L4.1318 5.7614L4.1318 8.1614L5.4619 8.1614ZM8.2969 8.1614L8.2969 5.7614L6.9668 5.7614L6.9668 8.1614L8.2969 8.1614ZM8.9025 11.099Q10.0968 10.1701 10.0968 8.7616L8.7667 8.7616Q8.7668 9.5197 8.086 10.0491Q7.3822 10.5966 6.4318 10.5966L6.4318 11.9266Q7.8385 11.9266 8.9025 11.099Z`,fillRule:`evenodd`})}));
|
||||
const useLocale$1=()=> 'zh';const al=()=>({reporter:{reportEvent:()=>{},Events:{}}});
|
||||
const resolveExpertListAvatarUrl=value=>value;
|
||||
const ImportSecurityRiskIcon=()=>null,ExpertCloudSyncBadge=()=>null;
|
||||
function useVisibleMarketTabs(){return [{key:'experts',label:'专家',tooltip:'发现和管理专家',Icon:Fu},{key:'skills',label:'技能',tooltip:'发现和管理技能',Icon:GF},{key:'connectors',label:'连接器',tooltip:'连接工具与服务',Icon:xl}]}
|
||||
const MarketTabIcon=({iconUrl:e,Icon:t})=>(0,FD.jsx)(Ao,{iconUrl:e,fallback:(0,FD.jsx)(t,{width:ID,height:ID}),size:ID});
|
||||
const UnifiedMarketHeader=({activeTab:e,onTabChange:t})=>{let n=useVisibleMarketTabs();return(0,FD.jsx)(`div`,{className:`um-header__inner`,children:(0,FD.jsx)(`div`,{className:`um-header__tabs`,role:`tablist`,children:n.map(({key:n,label:r,tooltip:i,Icon:a,iconUrl:o})=>{let s=e===n;return(0,FD.jsx)(_o,{content:i,placement:`bottom`,children:(0,FD.jsxs)(`button`,{type:`button`,role:`tab`,"aria-selected":s,className:`um-tab${s?` um-tab--active`:``}`,"data-track-id":`expert_top_tab_switch`,"data-track-name":`\u4E13\u5BB6/\u6280\u80FD/\u8FDE\u63A5\u5668`,"data-track-props":JSON.stringify({tab:n}),onClick:()=>t(n),children:[(0,FD.jsx)(MarketTabIcon,{iconUrl:o,Icon:a}),(0,FD.jsx)(`span`,{children:r})]})},n)})})})};
|
||||
const ExpertSearchBar=({value:e,onChange:t,placeholder:n,variant:r})=>{let i=useExpertI18n(),[a,o]=(0,mI.useState)(e),s=(0,mI.useRef)(!1),c=useDebouncedValue$1(a,Wvt);return(0,mI.useEffect)(()=>{o(e)},[e]),(0,mI.useEffect)(()=>{!s.current&&c!==e&&t(c)},[c]),(0,Uvt.jsx)(`div`,{className:Vvt.root,children:(0,Uvt.jsx)(ka.Search,{value:a,onChange:e=>o(e.target.value),onCompositionStart:()=>{s.current=!0},onCompositionEnd:n=>{let r=n.currentTarget.value;s.current=!1,o(r),r!==e&&t(r)},placeholder:n??i(`searchPlaceholder`),allowClear:!0,clearAriaLabel:i(`clearSearch`)||`Clear search`,variant:r})})};
|
||||
const ExpertListEmptyState=({message:e,showIcon:t})=>(0,qF.jsxs)(`div`,{className:KF.root,"data-market-perf":`empty`,children:[t&&(0,qF.jsx)(GraduationCapIcon,{}),(0,qF.jsx)(`p`,{className:KF.message,children:e})]});
|
||||
const GraduationCapIcon=()=>(0,qF.jsxs)(`svg`,{className:KF.icon,viewBox:`0 0 64 64`,fill:`none`,"aria-hidden":`true`,children:[(0,qF.jsx)(`path`,{d:`M32 8L4 22L32 36L60 22L32 8Z`,stroke:`currentColor`,strokeWidth:`2`,strokeLinejoin:`round`}),(0,qF.jsx)(`path`,{d:`M14 28V42C14 42 20 50 32 50C44 50 50 42 50 42V28`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`}),(0,qF.jsx)(`path`,{d:`M54 24V40`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`}),(0,qF.jsx)(`circle`,{cx:`54`,cy:`44`,r:`3`,stroke:`currentColor`,strokeWidth:`2`})]});
|
||||
function formatExpertUseCount(e,t){return typeof e!=`number`||!Number.isFinite(e)||e<=0?``:e>=1e4?`${Number.parseFloat((e/1e4).toFixed(2)).toString()}w`:e.toLocaleString(t===`zh`?`zh-CN`:`en-US`)}
|
||||
function formatExpertVersion(e){if(typeof e!=`string`)return``;let t=e.trim();return t?t.toLowerCase().startsWith(`v`)?t:`v${t}`:``}
|
||||
function resolveExpertCardTitle(e,t,n){for(let r of[e,t,n]){if(typeof r!=`string`)continue;let e=r.trim();if(e)return e}return``}
|
||||
function resolveExpertCardAuthorName(e,t){let n=e;if(e&&typeof e==`object`&&!Array.isArray(e)){let r=e;n=r[t]??r.zh??r.en}if(typeof n==`string`){let e=n.trim();if(e)return e}return``}
|
||||
function textMatchesKeyword(e,t){let n=t.trim().toLowerCase();if(!n||e.includes(n))return!0;if(n.includes(` `)){let t=n.replace(/\s+/g,``);if(t&&e.includes(t))return!0}return!1}
|
||||
function getStrictLocaleSearchText(e,t){if(typeof e==`string`)return t===`en`?e.toLowerCase():``;if(!e||typeof e!=`object`||Array.isArray(e))return``;let n=e[t];return typeof n==`string`?n.toLowerCase():``}
|
||||
function filterExpertsByKeyword(e,t,n){let r=t.trim().toLowerCase();return r?e.filter(e=>[getStrictLocaleSearchText(e.name,n),getStrictLocaleSearchText(e.displayName,n),getStrictLocaleSearchText(e.profession,n),getStrictLocaleSearchText(e.description,n),getStrictLocaleSearchText(e.displayDescription,n)].some(e=>textMatchesKeyword(e,r))):e}
|
||||
function getLocalizedText$1(e,t,n=``){return e?typeof e==`string`?e||n:e[t]??e.zh??e.en??n:n}
|
||||
function getLocalizedTag(e,t){return typeof e==`string`?e:getLocalizedText$1(e,t)}
|
||||
function getLocalizedTagTexts(e,t){return Array.isArray(e)?e.map(e=>getLocalizedTag(e,t).trim()).filter(Boolean):[]}
|
||||
function getAvatarFallbackBackground(e){return`var(--ec-avatar-fallback-bg-${(e?.charCodeAt(0)||0)%xMe+1})`}
|
||||
function resolveExpertPaidFlags(e){let t=e.isPaid??!1,n=e.isPurchased??!1;return{isPaid:t,isPurchased:n,paidNotPurchased:t&&!n}}
|
||||
function buildExpertCardVisibleTags(e){let t=getLocalizedTagTexts(e.tags,e.locale),n=e.isPaid?4:3;return e.isPaid?[e.paidTagText,...t].slice(0,n):t.slice(0,n)}
|
||||
function getImportSecurityRiskText(e,t){return e===`safe`?t===`zh`?`\u5DF2\u68C0\u6D4B\uFF1A\u672A\u53D1\u73B0\u98CE\u9669`:`Checked: no risk found`:e===`medium`?t===`zh`?`\u4E2D\u98CE\u9669\uFF1A\u4E13\u5BB6\u5305\u5B89\u5168\u68C0\u6D4B\u53D1\u73B0\u4E2D\u98CE\u9669`:`Medium risk detected in this expert package`:t===`zh`?`\u4F4E\u98CE\u9669\uFF1A\u4E13\u5BB6\u5305\u5B89\u5168\u68C0\u6D4B\u53D1\u73B0\u4F4E\u98CE\u9669`:`Low risk detected in this expert package`}
|
||||
function useDeferredImageLoad(e){let[t,n]=(0,nI.useState)(null),[r,i]=(0,nI.useState)(e);return(0,nI.useEffect)(()=>{e&&i(!0)},[e]),(0,nI.useEffect)(()=>{if(r||!t)return;if(typeof IntersectionObserver>`u`){i(!0);return}let e=new IntersectionObserver(t=>{t[0]?.isIntersecting&&(i(!0),e.disconnect())},{rootMargin:ivt});return e.observe(t),()=>e.disconnect()},[r,t]),{imageContainerRef:(0,nI.useCallback)(e=>{n(e)},[]),shouldLoadImage:r}}
|
||||
function useRevealOnLoad(e){let t=e||``,[n,r]=(0,JF.useState)(()=>({src:t,status:t?`pending`:`error`})),i=n.src===t?n.status:t?`pending`:`error`,a=(0,JF.useCallback)(()=>{r({src:t,status:`loaded`})},[t]),o=(0,JF.useCallback)(()=>{r({src:t,status:`error`})},[t]);return{shouldMount:!!t&&i!==`error`,revealed:!!t&&i===`loaded`,onLoad:a,onError:o}}
|
||||
function getFetchPriorityProps(e){return{fetchpriority:e}}
|
||||
function ExpertCardAvatar({avatarUrl:e,expertName:t,fallbackLabel:n,shouldLoadImage:r,priorityAvatarLoad:i,size:a=`default`}){let o=resolveExpertListAvatarUrl(e,a===`large`?48:36),[s,c]=(0,YF.useState)(``),l=s===o?e:o,u=useRevealOnLoad(r?l:void 0),d=(0,YF.useRef)(null),[f,p]=(0,YF.useState)(``),m=!e||!!e&&f===e,h=(0,YF.useCallback)(()=>{u.onLoad(),p(t=>t===e?``:t)},[e,u.onLoad]),g=(0,YF.useCallback)(()=>{if(l!==e){c(o);return}u.onError(),p(e||``)},[e,l,o,u.onError]);return(0,YF.useLayoutEffect)(()=>{let e=d.current;e?.complete&&e.naturalWidth>0&&h()},[l,h,r]),(0,XF.jsxs)(`div`,{className:a===`large`?`ec-card-avatar-sq ec-card-avatar-sq--large`:`ec-card-avatar-sq`,children:[m?(0,XF.jsx)(`div`,{className:`ec-avatar-sq-fallback`,style:{background:getAvatarFallbackBackground(n)},children:n.charAt(0)}):null,u.shouldMount?(0,XF.jsx)(`img`,{ref:d,className:u.revealed?void 0:`is-pending`,src:l,alt:t,loading:`eager`,decoding:`async`,...getFetchPriorityProps(i?`high`:`auto`),onLoad:h,onError:g,draggable:!1},l):null]})}
|
||||
function escapeRegExp$1(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}
|
||||
function highlightText(e,t){if(!t||!t.trim())return e;let n=t.trim(),r=escapeRegExp$1(n),i=r;if(n.includes(` `)){let e=n.replace(/\s+/g,``);e&&(i=`${r}|${escapeRegExp$1(e)}`)}let a=RegExp(`(${i})`,`gi`),o=e.split(a);if(o.length===1)return e;let s=RegExp(`^(?:${i})$`,`i`);return o.filter(e=>e!==``).map((e,t)=>s.test(e)?(0,p_t.jsx)(`span`,{className:`ec-search-highlight`,children:e},t):e)}
|
||||
const ExpertCard$1=({expert:e,avatarUrl:t,cardPosition:n,onSummon:r,appearance:i=`default`,highlightKeyword:a,onTeamClick:o,onExpertClick:s,priorityAvatarLoad:c=!1,selectable:l=!1,selected:u=!1,disabled:d=!1,onSelect:f,renderFooter:p,extraContent:m,className:h,wrapperRef:g,hideTopSummonButton:v=!1,topActions:y,topActionsPlacement:b=`overlay`,hideSubtitle:x=!1,isFirst:S=!1,cloudSyncBadgeVariant:C,cloudSyncBadgeLoading:w=!1,onCloudSyncBadgeClick:D,listTelemetrySource:O})=>{let k=useExpertI18n(),A=useLocale$1(),{reportEvent:j,Events:M}=al().reporter,N=(0,nI.useRef)(null),P=(0,nI.useRef)(null),{imageContainerRef:F,shouldLoadImage:I}=useDeferredImageLoad(c),L=(0,nI.useMemo)(()=>getLocalizedText$1(e.name,A,e.id),[e.id,e.name,A]),R=(0,nI.useMemo)(()=>getLocalizedText$1(e.name,A).trim(),[e.name,A]),z=(0,nI.useMemo)(()=>getLocalizedText$1(e.displayName,A),[e.displayName,A]),B=(0,nI.useMemo)(()=>resolveExpertCardAuthorName(e.author,A),[e.author,A]),V=(0,nI.useMemo)(()=>getLocalizedText$1(e.profession,A),[e.profession,A]),H=(0,nI.useMemo)(()=>resolveExpertCardTitle(V,z,L),[z,L,V]),U=(0,nI.useMemo)(()=>getLocalizedText$1(e.displayDescription||e.description,A),[e.displayDescription,e.description,A]),W=(0,nI.useMemo)(()=>getLocalizedText$1(e.operationalTag,A).trim(),[e.operationalTag,A]),ee=A===`zh`&&W===`\u7279\u9080\u4E13\u5BB6`?`\u7279\u9080`:W,te=(0,nI.useMemo)(()=>formatExpertUseCount(e.useCount,A),[e.useCount,A]),ne=e.importSecurityRisk,re=ne?.riskLevel===`low`||ne?.riskLevel===`medium`?ne.riskLevel:void 0,ie=(0,nI.useMemo)(()=>getImportSecurityRiskText(re,A),[re,A]),ae=e.expertType===`team`,G=i===`center`,oe=(0,nI.useMemo)(()=>{let e=U.trim();return(ae?[B,z,R]:[z,R]).map(e=>e.trim()).find(t=>t&&t!==H&&t!==e)??``},[B,H,U,z,R,ae]),{isPaid:se,paidNotPurchased:ce}=resolveExpertPaidFlags(e),le=(0,nI.useRef)(null);(0,nI.useEffect)(()=>{O&&se&&le.current!==e.id&&(le.current=e.id,j(M.AssetListShow,{assetId:e.id,type:`expert`,...e.pricing?{payMode:e.pricing.type===`monthly`?`month`:`once`}:{},source:O}))},[M,e.id,e.pricing,se,O,j]);let K=(0,nI.useCallback)(()=>{O&&se&&j(M.AssetListClick,{assetId:e.id,type:`expert`,...e.pricing?{payMode:e.pricing.type===`monthly`?`month`:`once`}:{},source:O})},[M,e.id,e.pricing,se,O,j]),ue=(0,nI.useMemo)(()=>buildExpertCardVisibleTags({isPaid:se,tags:e.tags,locale:A,paidTagText:k(`card.paidTag`)}),[se,e.tags,A,k]),[de,fe]=(0,nI.useState)(ue.length);(0,nI.useLayoutEffect)(()=>{let e=N.current,t=P.current;if(!e||!t||ue.length===0||typeof window>`u`){fe(ue.length);return}let calculateVisibleTagCount=()=>{let n=e.clientWidth;if(n<=0){fe(ue.length);return}let r=window.getComputedStyle(t),i=r.columnGap||r.gap||``,a=Number.parseFloat(i)||avt,o=Array.from(t.children),s=0,c=0;o.forEach(e=>{let t=Math.ceil(e.getBoundingClientRect().width),r=c===0?t:s+a+t;r<=n&&(s=r,c+=1)}),fe(e=>e===c?e:c)};calculateVisibleTagCount();let n=window.requestAnimationFrame(calculateVisibleTagCount);if(typeof ResizeObserver>`u`)return()=>window.cancelAnimationFrame(n);let r=new ResizeObserver(calculateVisibleTagCount);return r.observe(e),()=>{window.cancelAnimationFrame(n),r.disconnect()}},[ue]);let pe=(0,nI.useCallback)(()=>{if(l){!d&&f&&f(e.id);return}if(ae&&o){o(e);return}if(s){K(),s(e);return}r(e.id,n,void 0,e.marketExpertId,e.source)},[l,d,f,ae,o,s,e,r,n,K]),me=(0,nI.useCallback)(e=>{(e.key===`Enter`||e.key===` `)&&(e.preventDefault(),pe())},[pe]),he=(0,nI.useCallback)(t=>{if(t.preventDefault(),t.stopPropagation(),ce&&s){K(),s(e);return}r(e.id,n,void 0,e.marketExpertId,e.source)},[r,e.id,e.marketExpertId,e.source,n,ce,s,e,K]),ge=k(ae?`summonTeamWithName`:`summonExpertWithName`,{name:L}),_e=H,ve=(0,nI.useMemo)(()=>({"--delay":`${n*.02}s`}),[n]),ye=(0,nI.useCallback)(e=>{F(e),typeof g==`function`?g(e):g&&`current`in g&&(g.current=e)},[F,g]),be=(0,nI.useMemo)(()=>{let e=[tI.card];return h&&e.push(h),l&&e.push(tI.selectable),u&&e.push(tI.selected),d&&e.push(tI.disabled),G&&e.push(tI.centerCard),G&&!te&&e.push(tI.centerCardWithoutMetadata),e.join(` `)},[h,te,l,u,d,G]),xe=ae?`expert_team_summon`:void 0,Se=ae?`\u53EC\u5524\u4E13\u5BB6\u56E2`:void 0,Ce=ae?JSON.stringify({source:e.id,type:`team`,version:e.version??``}):void 0,we=ae?`expert_team_summon`:`expert_summon_click`,Te=ae?`\u53EC\u5524\u4E13\u5BB6\u56E2`:`\u7ACB\u5373\u53EC\u5524`,Ee=(0,nI.useMemo)(()=>JSON.stringify({source:e.id,type:ae?`team`:e.categoryId||`expert`,version:e.version??``}),[e.categoryId,e.id,e.version,ae]),De=(0,nI.useMemo)(()=>l||v?null:(0,rI.jsx)(`button`,{type:`button`,className:`${tI.summonBtn} ${tI.summonBtnTop}`,"data-track-id":we,"data-track-name":Te,"data-track-props":Ee,onClick:he,"aria-label":ge,children:k(`summonShort`)}),[he,v,l,ge,we,Te,Ee,k]);return(0,rI.jsxs)(`article`,{className:be,ref:ye,style:ve,"data-market-perf":S?`first-item`:void 0,"data-track-id":xe,"data-track-name":Se,"data-track-props":Ce,onClick:pe,onKeyDown:me,role:`button`,tabIndex:0,children:[!l&&y&&b===`overlay`&&(0,rI.jsxs)(`div`,{className:tI.topActions,children:[y,DeLine truncated
|
||||
export {UnifiedMarketHeader,ExpertSearchBar,ExpertListEmptyState,ExpertCard$1 as ExpertCard,Fu as ExpertIcon,GF as SkillIcon,xl as ConnectorIcon,EM as BackIcon,Bn as AddIcon,Ym as MyExpertIcon,filterExpertsByKeyword,formatExpertUseCount,formatExpertVersion};
|
||||
@@ -0,0 +1,356 @@
|
||||
/* REA recovered declarations; generated by recover-capability-center.mjs. */
|
||||
.yc-capability-center{--wb-palette-black-10:#0000001a;--wb-palette-black-20:#0003;--wb-palette-black-30:#0000004d;--wb-palette-black-40:#0006;--wb-palette-black-50:#00000080;--wb-palette-black-60:#0009;--wb-palette-black-70:#000000b3;--wb-palette-black-75:#000000bf;--wb-palette-black-90:#000000e6;--wb-palette-black-100:#000;--wb-palette-white-10:#ffffff1a;--wb-palette-white-20:#fff3;--wb-palette-white-30:#ffffff4d;--wb-palette-white-40:#fff6;--wb-palette-white-50:#ffffff80;--wb-palette-white-60:#fff9;--wb-palette-white-70:#ffffffb3;--wb-palette-white-75:#ffffffbf;--wb-palette-white-80:#fffc;--wb-palette-white-90:#ffffffe6;--wb-palette-white-100:#fff;--wb-palette-brand-1:#dff7f2;--wb-palette-brand-2:#bff0e6;--wb-palette-brand-3:#9fe8d9;--wb-palette-brand-4:#80e1cd;--wb-palette-brand-5:#60d9c0;--wb-palette-brand-7:#40d1b3;--wb-palette-brand-8:#00c29a;--wb-palette-brand-9:#009273;--wb-palette-brand-10:#00614d;--wb-palette-gray-1:#fafafa;--wb-palette-gray-2:#f7f7f7;--wb-palette-gray-3:#f2f2f2;--wb-palette-gray-4:#ebebeb;--wb-palette-gray-5:#e6e6e6;--wb-palette-green-1:#e7f8f0;--wb-palette-green-2:#c2efd6;--wb-palette-green-3:#a3e7c2;--wb-palette-green-4:#85dfad;--wb-palette-green-5:#66d799;--wb-palette-green-6:#47cf84;--wb-palette-green-7:#29c770;--wb-palette-green-8:#0cbf5b;--wb-palette-green-9:#088f50;--wb-palette-orange-1:#ffeddf;--wb-palette-orange-2:#ffdcbf;--wb-palette-orange-3:#ffca9f;--wb-palette-orange-4:#ffb980;--wb-palette-orange-5:#ffa760;--wb-palette-orange-6:#ff9540;--wb-palette-orange-7:#ff8420;--wb-palette-orange-8:#ff7800;--wb-palette-orange-9:#c04100;--wb-palette-orange-10:#800f00;--wb-palette-red-1:#fce8e8;--wb-palette-red-2:#f9d1d1;--wb-palette-red-3:#f5b9b9;--wb-palette-red-4:#f2a2a2;--wb-palette-red-5:#ef8b8b;--wb-palette-red-6:#ec7474;--wb-palette-red-7:#e85c5c;--wb-palette-red-8:#f64041;--wb-palette-red-9:#b42c3f;--wb-palette-red-10:#821238;--wb-palette-blue-1:#eaf2ff;--wb-palette-blue-2:#bfdbfe;--wb-palette-blue-3:#93c5fd;--wb-palette-blue-4:#60a5fa;--wb-palette-blue-5:#4f86ff;--wb-palette-blue-6:#4f7df3;--wb-palette-blue-7:#3370ff;--wb-palette-blue-8:#2563eb;--wb-palette-blue-9:#1565c0;--wb-palette-blue-10:#0550ae;--wb-palette-brand-blue:#0052d9;--wb-palette-purple-1:#f5f2ff;--wb-palette-purple-2:#f1eeff;--wb-palette-purple-3:#e0d4ff;--wb-palette-purple-4:#c8a2ff;--wb-palette-purple-5:#b0a0c8;--wb-palette-purple-6:#7c5cfc;--wb-palette-purple-7:#7b61ff;--wb-palette-purple-8:#7c3aed;--wb-palette-purple-9:#8250df;--wb-palette-purple-10:#6c4dff;--wb-palette-cyan-1:#e8f7ff;--wb-palette-cyan-2:#a6d5da;--wb-palette-cyan-3:#6fc3df;--wb-palette-cyan-4:#22d3ee;--wb-palette-cyan-5:#06b6d4;--wb-palette-cyan-6:#14b8a6;--wb-palette-cyan-7:#00796b;--wb-color-bg-primary-default:var(--wb-palette-gray-3);--wb-color-bg-primary-hover:var(--wb-palette-gray-4);--wb-color-bg-primary-active:var(--wb-palette-gray-5);--wb-color-bg-primary-hover-strong:var(--wb-palette-gray-5);--wb-color-bg-secondary-default:var(--wb-palette-white-100);--wb-color-bg-secondary-hover-active:var(--wb-palette-gray-2);--wb-color-border-primary:var(--wb-palette-gray-5);--wb-color-border-secondary:var(--wb-palette-gray-4);--wb-color-border-tertiary:var(--wb-palette-gray-3);--wb-color-border-focus:var(--wb-palette-black-75);--wb-color-border-solid:#dedede;--wb-color-mask-color:var(--wb-palette-black-60);--wb-color-text-primary:var(--wb-palette-black-100);--wb-color-text-secondary:var(--wb-palette-black-70);--wb-color-text-secondary-solid:#5d5d5d;--wb-color-text-tertiary:var(--wb-palette-black-50);--wb-color-text-placeholder:#8a9099;--wb-color-text-disabled:var(--wb-palette-black-30);--wb-color-text-hint:var(--wb-palette-black-30);--wb-color-text-solid:var(--wb-palette-black-100);--wb-color-text-brand-default:var(--wb-palette-brand-8);--wb-color-text-brand-hover:var(--wb-palette-brand-7);--wb-color-text-brand-focus:var(--wb-palette-brand-5);--wb-color-text-brand-active:var(--wb-palette-brand-9);--wb-color-text-brand-disabled:var(--wb-palette-brand-3);--wb-color-accent-brand:var(--wb-palette-brand-8);--wb-color-text-link-default:#1470b4;--wb-color-text-link-hover:#0e58a0;--wb-color-text-error-default:var(--wb-palette-red-8);--wb-color-text-error-hover:var(--wb-palette-red-7);--wb-color-text-error-focus:var(--wb-palette-red-5);--wb-color-text-error-active:var(--wb-palette-red-9);--wb-color-text-error-disabled:var(--wb-palette-red-3);--wb-color-text-success-default:var(--wb-palette-green-8);--wb-color-text-success-hover:var(--wb-palette-green-7);--wb-color-text-success-focus:var(--wb-palette-green-5);--wb-color-text-success-active:var(--wb-palette-green-9);--wb-color-text-success-disabled:var(--wb-palette-green-3);--wb-color-text-warning-default:var(--wb-palette-orange-8);--wb-color-text-warning-hover:var(--wb-palette-orange-7);--wb-brand-primary:var(--wb-palette-brand-8);--wb-text-strong:var(--wb-color-text-primary);--wb-text-medium:var(--wb-color-text-secondary);--wb-text-weak:var(--wb-color-text-tertiary);--wb-text-disable:var(--wb-coloLine truncated
|
||||
.yc-capability-center .expert-center-dark{--wb-palette-black-10:#0000001a;--wb-palette-black-20:#0003;--wb-palette-black-30:#0000004d;--wb-palette-black-40:#0006;--wb-palette-black-50:#00000080;--wb-palette-black-60:#0009;--wb-palette-black-70:#000000b3;--wb-palette-black-75:#000000bf;--wb-palette-black-90:#000000e6;--wb-palette-black-100:#000;--wb-palette-white-10:#ffffff1a;--wb-palette-white-20:#fff3;--wb-palette-white-30:#ffffff4d;--wb-palette-white-40:#fff6;--wb-palette-white-50:#ffffff80;--wb-palette-white-60:#fff9;--wb-palette-white-70:#ffffffb3;--wb-palette-white-75:#ffffffbf;--wb-palette-white-80:#fffc;--wb-palette-white-90:#ffffffe6;--wb-palette-white-100:#fff;--wb-palette-brand-1:#0e2520;--wb-palette-brand-2:#163932;--wb-palette-brand-3:#185f51;--wb-palette-brand-4:#168770;--wb-palette-brand-5:#1ca98c;--wb-palette-brand-7:#22cba8;--wb-palette-brand-8:#4cf0ce;--wb-palette-brand-9:#7cf0d8;--wb-palette-brand-10:#a8f5e5;--wb-palette-gray-1:#141414;--wb-palette-gray-2:#292929;--wb-palette-gray-3:#1f1f1f;--wb-palette-gray-4:#242424;--wb-palette-gray-5:#3a3a3a;--wb-palette-green-1:#223b30;--wb-palette-green-2:#153825;--wb-palette-green-3:#175d37;--wb-palette-green-4:#188247;--wb-palette-green-5:#1ea35a;--wb-palette-green-6:#24c46c;--wb-palette-green-7:#2ee07e;--wb-palette-green-8:#43f08f;--wb-palette-green-9:#b3e8d1;--wb-palette-orange-1:#25180e;--wb-palette-orange-2:#2e1f12;--wb-palette-orange-3:#462e1a;--wb-palette-orange-4:#663c19;--wb-palette-orange-5:#8b4914;--wb-palette-orange-6:#a75818;--wb-palette-orange-7:#c3671c;--wb-palette-orange-8:#ec7913;--wb-palette-orange-9:#f1854d;--wb-palette-orange-10:#f2988c;--wb-palette-red-1:#250e0e;--wb-palette-red-2:#260e0e;--wb-palette-red-3:#3a1616;--wb-palette-red-4:#551515;--wb-palette-red-5:#6a1a1a;--wb-palette-red-6:#8a1414;--wb-palette-red-7:#a31717;--wb-palette-red-8:#af191a;--wb-palette-red-9:#dc4258;--wb-palette-red-10:#f07aa2;--wb-palette-blue-1:#1b2737;--wb-palette-blue-2:#1e2b43;--wb-palette-blue-3:#4270a9;--wb-palette-blue-4:#4d80ef;--wb-palette-blue-5:#5387f9;--wb-palette-blue-6:#5789ff;--wb-palette-blue-7:#6b9cff;--wb-palette-blue-8:#6baaf8;--wb-palette-blue-9:#93c5fd;--wb-palette-blue-10:#79c0ff;--wb-palette-brand-blue:#4d80ef;--wb-palette-purple-1:#211b37;--wb-palette-purple-2:#211c3b;--wb-palette-purple-3:#2a2145;--wb-palette-purple-4:#2c243d;--wb-palette-purple-5:#2e224f;--wb-palette-purple-6:#8d74b4;--wb-palette-purple-7:#8f61d1;--wb-palette-purple-8:#9b6bf7;--wb-palette-purple-9:#a78bfa;--wb-palette-purple-10:#d2a8ff;--wb-palette-cyan-1:#0c2a26;--wb-palette-cyan-2:#1a3530;--wb-palette-cyan-3:#5cc4b0;--wb-palette-cyan-4:#2dd4bf;--wb-palette-cyan-5:#54a8c4;--wb-palette-cyan-6:#7aafb6;--wb-palette-cyan-7:#22d3ee;--wb-color-bg-primary-default:var(--wb-palette-gray-3);--wb-color-bg-primary-hover:var(--wb-palette-gray-4);--wb-color-bg-primary-active:var(--wb-palette-gray-5);--wb-color-bg-primary-hover-strong:var(--wb-palette-gray-5);--wb-color-bg-secondary-default:var(--wb-palette-gray-2);--wb-color-bg-secondary-hover-active:var(--wb-palette-gray-2);--wb-color-border-primary:#262626;--wb-color-border-secondary:var(--wb-palette-gray-5);--wb-color-border-tertiary:var(--wb-palette-gray-4);--wb-color-border-focus:var(--wb-palette-white-70);--wb-color-border-solid:#3d3d3d;--wb-color-mask-color:#000c;--wb-color-text-primary:#fff;--wb-color-text-secondary:#ffffffa6;--wb-color-text-secondary-solid:#a6a6a6;--wb-color-text-tertiary:#ffffff80;--wb-color-text-placeholder:#727882;--wb-color-text-disabled:#ffffff59;--wb-color-text-hint:var(--wb-palette-white-40);--wb-color-text-solid:var(--wb-palette-white-100);--wb-color-text-brand-default:var(--wb-palette-brand-7);--wb-color-text-brand-hover:var(--wb-palette-brand-8);--wb-color-text-brand-focus:var(--wb-palette-brand-9);--wb-color-text-brand-active:var(--wb-palette-brand-5);--wb-color-text-brand-disabled:#3dd9b659;--wb-color-accent-brand:var(--wb-palette-brand-7);--wb-color-text-link-default:#46a8f0;--wb-color-text-link-hover:#5ea9f2;--wb-color-text-error-default:var(--wb-palette-red-7);--wb-color-text-error-hover:var(--wb-palette-red-8);--wb-color-text-error-focus:var(--wb-palette-red-9);--wb-color-text-error-active:var(--wb-palette-red-6);--wb-color-text-error-disabled:#f4737459;--wb-color-text-success-default:var(--wb-palette-green-6);--wb-color-text-success-hover:var(--wb-palette-green-7);--wb-color-text-success-focus:var(--wb-palette-green-8);--wb-color-text-success-active:#77f0b8;--wb-color-text-success-disabled:#4ed88a59;--wb-color-text-warning-default:var(--wb-palette-orange-7);--wb-color-text-warning-hover:var(--wb-palette-orange-8);--wb-brand-primary:var(--wb-palette-brand-7);--wb-text-strong:var(--wb-color-text-primary);--wb-text-medium:var(--wb-color-text-secondary);--wb-text-weak:var(--wb-color-text-tertiary);--wb-text-disable:var(--wb-color-text-disabled);--wb-text-white:var(--wb-palette-white-100);--wb-text-muted:var(--wb-color-text-disabled);--wb-text-link:var(--wb-color-text-brand-default);--Line truncated
|
||||
.yc-capability-center{--wb-todo-menu-bg-hover:var(--wb-palette-gray-4);--wb-todo-menu-bg-active:var(--wb-palette-gray-5);--wb-todo-menu-icon-default:var(--wb-palette-black-70);--wb-todo-menu-text-default:var(--wb-palette-black-90);--wb-todo-menu-text-heading:var(--wb-palette-black-30);--wb-todo-menu-text-view-more:var(--wb-palette-black-50);--wb-menu-item-danger-fg:#f14c4c;--wb-icon-strong:color-mix(in srgb, var(--wb-palette-black-100) 96%, transparent);--wb-icon-primary:var(--wb-palette-black-90);--wb-icon-secondary:var(--wb-palette-black-70);--wb-icon-medium:var(--wb-palette-black-70);--wb-icon-tertiary:var(--wb-palette-black-50);--wb-icon-muted:var(--wb-palette-black-30);--wb-status-success:var(--wb-palette-green-8);--wb-status-warning:var(--wb-palette-orange-8);--wb-status-error:var(--wb-palette-red-8);--wb-status-info:var(--wb-palette-brand-8);--wb-status-warning-soft:var(--wb-palette-orange-1);--wb-status-error-soft:var(--wb-palette-red-1);--wb-sidebar-bg:var(--wb-palette-gray-2);--wb-sidebar-border:var(--wb-color-bg-primary-hover);--wb-sidebar-mask-end:var(--wb-sidebar-bg);--wb-text-version-badge:var(--wb-palette-black-20);--wb-text-enterprise:var(--wb-color-text-warning-default);--wb-scrollbar-thumb:var(--wb-palette-black-20);--wb-scrollbar-thumb-hover:var(--wb-palette-black-30);--wb-scrollbar-thumb-bg:#0000001f;--wb-badge-warning-bg:var(--wb-status-warning-soft);--wb-badge-warning-fg:var(--wb-color-text-warning-default);--wb-badge-success-bg:var(--wb-palette-green-1);--wb-badge-success-fg:var(--wb-color-text-success-active);--wb-accent-unread:var(--wb-palette-brand-8);--wb-bg-pill-active:#000000bf;--wb-bg-pill-active-hover:#000000d1;--wb-bg-pill-hover:#fafafa;--wb-task-starter-trigger-bg-hover:#ebebeb;--wb-quick-action-item-bg-hover:#ebebeb;--wb-quick-action-item-border-hover:#e5e5e5;--wb-home-composer-chip-bg-hover:#f2f2f2;--wb-mode-selector-shadow:0 8px 24px #0f172a1a, 0 2px 6px #0f172a0a;--wb-text-connector:#fff;--wb-connector-blue:#4f7df3;--wb-connector-blue-bg:#eaf2ff;--wb-connector-green:#31a354;--wb-connector-green-bg:#ecfdf3;--wb-connector-amber:#f3b434;--wb-connector-amber-bg:#fff7e0;--wb-connector-red:#e05243;--wb-connector-red-bg:#fef0ee;--wb-connector-purple:#7b61ff;--wb-connector-purple-bg:#f1eeff;--wb-connector-orange:#f97316;--wb-connector-orange-bg:#fff0e6;--wb-connector-slate:#64748b;--wb-connector-slate-bg:#f1f5f9;--wb-connector-sky:#0ea5e9;--wb-connector-sky-bg:#e8f7ff;--wb-bg-connector-google-sheets:var(--wb-connector-blue);--wb-bg-connector-google-drive:var(--wb-connector-amber);--wb-bg-connector-notion:var(--wb-connector-purple);--wb-bg-connector-google-docs:#6b7280;--wb-bg-connector-linear:#6366f1;--wb-bg-connector-github:var(--wb-connector-green);--wb-bg-connector-gmail:var(--wb-connector-red);--wb-bg-connector-slack:var(--wb-connector-orange);--wb-bg-connector-figma:#f05a3a;--wb-border-drag-indicator:var(--wb-palette-brand-8);--wb-sash-hover-gradient-vertical:linear-gradient(180deg, #00c29a00 0%, #00c29a 50%, #00c29a00 100%);--wb-sash-hover-gradient-horizontal:linear-gradient(90deg, #00c29a00 0%, #00c29a 50%, #00c29a00 100%);--wb-connector-detail-try-loading-bg:#ccc;--wb-connector-detail-surface-bg:#fafafa;--wb-connector-detail-flow-dot-bg:#e5e5e5;--wb-connector-token-input-placeholder:#c6c6c6;--wb-toast-info-icon:#0052d9;--wb-brand-wechat-green:#0bc160;--wb-brand-wechat-green-hover:#07a152;--wb-brand-wechat-green-connected:#07c160;--wb-bg-overlay-elevated:#3c3c40c7;--wb-bg-scrim-light:#fffc;--wb-color-text-on-scrim-light:#0000006b;--wb-bg-warning-soft-strong:#f59e0b29;--wb-border-input-soft-dark:#ffffff61;--wb-control-checkbox-selected-bg:var(--wb-palette-black-90);--wb-control-checkbox-selected-bg-hover:color-mix(in srgb, var(--wb-palette-black-100) 96%, transparent);--wb-control-checkbox-selected-fg:var(--wb-palette-white-100);--wb-checkbox-bg-default:transparent;--wb-checkbox-border-default:var(--wb-palette-black-20);--wb-checkbox-border-hover:var(--wb-palette-black-50);--wb-checkbox-bg-selected:var(--wb-palette-black-75);--wb-checkbox-border-selected:var(--wb-palette-black-75);--wb-checkbox-icon-selected:var(--wb-palette-gray-1);--wb-bg-input-slot-gradient:linear-gradient(180deg, #ebebeb 0%, #f5f5f5 100%);--wb-bg-surface-overlay-soft:#0000000a;--wb-bg-surface-overlay-strong:#00000014;--wb-text-faded:#00000059;--wb-border-overlay:#00000061;--wb-accent-link-soft:#1a79ff24;--wb-accent-link-soft-hover:#4080ff1f;--wb-icon-folder:#e89a2d;--wb-status-info-blue:#60a5fa;--wb-status-success-muted:#10b981;--wb-status-warning-muted:#f59e0b;--wb-status-success-soft-bg:#10b9811a;--wb-status-warning-soft-bg:#f59e0b1a;--wb-status-error-soft-bg:#ff4d4f1a;--wb-alert-success-bg:#d1fae5;--wb-alert-success-text:#047857;--wb-alert-info-bg:#dbeafe;--wb-alert-info-text:var(--wb-palette-blue-8);--wb-alert-warning-bg:#fef3c7;--wb-alert-warning-text:#d97706;--wb-alert-warning-bg-fc:#f9f1e8;--wb-alert-warning-text-fc:#c04100;--wb-alert-error-bg:#fef2f2;--wb-alert-error-text:#dc2626;--wb-badge-goLine truncated
|
||||
.yc-capability-center .expert-center-dark{--wb-todo-menu-bg-hover:color-mix(in srgb, var(--wb-palette-white-100) 8%, transparent);--wb-todo-menu-bg-active:color-mix(in srgb, var(--wb-palette-white-100) 12%, transparent);--wb-todo-menu-icon-default:var(--wb-palette-white-70);--wb-todo-menu-text-default:var(--wb-palette-white-90);--wb-todo-menu-text-heading:var(--wb-palette-white-40);--wb-todo-menu-text-view-more:var(--wb-palette-white-50);--wb-menu-item-danger-fg:#ff6b6b;--wb-icon-strong:color-mix(in srgb, var(--wb-palette-black-100) 96%, transparent);--wb-icon-primary:var(--wb-palette-white-90);--wb-icon-secondary:var(--wb-palette-white-70);--wb-icon-medium:var(--wb-palette-white-70);--wb-icon-tertiary:var(--wb-palette-white-50);--wb-icon-muted:var(--wb-palette-white-30);--wb-status-success:var(--wb-color-text-success-default);--wb-status-warning:var(--wb-color-text-warning-default);--wb-status-error:var(--wb-color-text-error-default);--wb-status-info:var(--wb-color-text-brand-default);--wb-status-warning-soft:color-mix(in srgb, var(--wb-color-text-warning-default) 18%, transparent);--wb-status-error-soft:color-mix(in srgb, var(--wb-color-text-error-default) 18%, transparent);--wb-sidebar-bg:var(--wb-palette-gray-3);--wb-sidebar-border:var(--wb-palette-gray-5);--wb-sidebar-mask-end:var(--wb-sidebar-bg);--wb-text-version-badge:var(--wb-palette-white-20);--wb-text-enterprise:var(--wb-color-text-warning-hover);--wb-scrollbar-thumb:var(--wb-palette-white-20);--wb-scrollbar-thumb-hover:var(--wb-palette-white-30);--wb-scrollbar-thumb-bg:#ffffff1f;--wb-badge-warning-bg:var(--wb-status-warning-soft);--wb-badge-warning-fg:var(--wb-color-text-warning-hover);--wb-badge-success-bg:color-mix(in srgb, var(--wb-color-text-success-default) 18%, transparent);--wb-badge-success-fg:var(--wb-color-text-success-hover);--wb-accent-unread:var(--wb-color-text-brand-default);--wb-bg-pill-active:#ffffff2e;--wb-bg-pill-active-hover:#ffffff38;--wb-bg-pill-hover:#ffffff0f;--wb-task-starter-trigger-bg-hover:#ffffff0f;--wb-quick-action-item-bg-hover:#ffffff24;--wb-quick-action-item-border-hover:#ffffff14;--wb-home-composer-chip-bg-hover:#2e2e2e;--wb-mode-selector-shadow:0 8px 24px #0006, 0 2px 6px #00000040;--wb-text-connector:#fff;--wb-connector-blue:#4f7df3;--wb-connector-blue-bg:#eaf2ff;--wb-connector-green:#31a354;--wb-connector-green-bg:#ecfdf3;--wb-connector-amber:#f3b434;--wb-connector-amber-bg:#fff7e0;--wb-connector-red:#e05243;--wb-connector-red-bg:#fef0ee;--wb-connector-purple:#7b61ff;--wb-connector-purple-bg:#f1eeff;--wb-connector-orange:#f97316;--wb-connector-orange-bg:#fff0e6;--wb-connector-slate:#64748b;--wb-connector-slate-bg:#f1f5f9;--wb-connector-sky:#0ea5e9;--wb-connector-sky-bg:#e8f7ff;--wb-bg-connector-google-sheets:var(--wb-connector-blue);--wb-bg-connector-google-drive:var(--wb-connector-amber);--wb-bg-connector-notion:var(--wb-connector-purple);--wb-bg-connector-google-docs:#6b7280;--wb-bg-connector-linear:#6366f1;--wb-bg-connector-github:var(--wb-connector-green);--wb-bg-connector-gmail:var(--wb-connector-red);--wb-bg-connector-slack:var(--wb-connector-orange);--wb-bg-connector-figma:#f05a3a;--wb-border-drag-indicator:var(--wb-color-text-brand-default);--wb-sash-hover-gradient-vertical:linear-gradient(180deg, #00c29a00 0%, #00c29a 50%, #00c29a00 100%);--wb-sash-hover-gradient-horizontal:linear-gradient(90deg, #00c29a00 0%, #00c29a 50%, #00c29a00 100%);--wb-connector-detail-try-loading-bg:#1e1e1e;--wb-connector-detail-surface-bg:#1e1e1e;--wb-connector-detail-flow-dot-bg:#3d3d3d;--wb-connector-token-input-placeholder:#c6c6c6;--wb-toast-info-icon:#0052d9;--wb-brand-wechat-green:#0bc160;--wb-brand-wechat-green-hover:#07a152;--wb-brand-wechat-green-connected:#07c160;--wb-bg-overlay-elevated:#28282ce0;--wb-bg-scrim-light:#fffc;--wb-color-text-on-scrim-light:#0000006b;--wb-bg-warning-soft-strong:#f59e0b29;--wb-border-input-soft-dark:#ffffff61;--wb-control-checkbox-selected-bg:var(--wb-palette-white-100);--wb-control-checkbox-selected-bg-hover:color-mix(in srgb, var(--wb-palette-white-100) 88%, transparent);--wb-control-checkbox-selected-fg:var(--wb-palette-black-90);--wb-checkbox-bg-default:transparent;--wb-checkbox-border-default:#fff3;--wb-checkbox-border-hover:var(--wb-palette-white-50);--wb-checkbox-bg-selected:#ffffffbf;--wb-checkbox-border-selected:#ffffffbf;--wb-checkbox-icon-selected:var(--wb-palette-gray-3);--wb-bg-input-slot-gradient:linear-gradient(180deg, var(--wb-palette-gray-3) 0%, var(--wb-palette-gray-4) 100%);--wb-bg-surface-overlay-soft:#ffffff0d;--wb-bg-surface-overlay-strong:#ffffff1a;--wb-text-faded:#ffffff59;--wb-border-overlay:#ffffff61;--wb-accent-link-soft:#1a79ff24;--wb-accent-link-soft-hover:#4080ff1f;--wb-icon-folder:#e89a2d;--wb-status-info-blue:#60a5fa;--wb-status-success-muted:#10b981;--wb-status-warning-muted:#f59e0b;--wb-status-success-soft-bg:#10b98124;--wb-status-warning-soft-bg:#f59e0b24;--wb-status-error-soft-bg:#ff4d4f24;--wb-alert-success-bg:#1d3528;--wb-alert-success-text:#34d399;Line truncated
|
||||
.yc-capability-center{--wb-spacing-0:0;--wb-spacing-1:2px;--wb-spacing-2:4px;--wb-spacing-2-5:5px;--wb-spacing-3:8px;--wb-spacing-4:12px;--wb-spacing-5:16px;--wb-spacing-6:20px;--wb-spacing-7:24px;--wb-spacing-8:32px;--wb-spacing-9:40px;--wb-spacing-10:48px;--wb-spacing-12:64px;--wb-radius-none:0;--wb-radius-xs:2px;--wb-radius-sm:4px;--wb-radius-md:6px;--wb-radius-lg:8px;--wb-radius-xl:12px;--wb-radius-2xl:16px;--wb-radius-3xl:20px;--wb-radius-4xl:24px;--wb-radius-full:9999px;--wb-font-code-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;--wb-font-mono:var(--wb-font-code-family);--wb-font-size-1:8px;--wb-font-size-2:10px;--wb-font-size-3:12px;--wb-font-size-4:13px;--wb-font-size-5:14px;--wb-font-size-6:16px;--wb-font-size-7:18px;--wb-font-size-8:20px;--wb-font-size-9:24px;--wb-font-size-10:28px;--wb-font-size-11:32px;--wb-font-line-height-4:16px;--wb-font-line-height-4-2:18px;--wb-font-line-height-5:20px;--wb-font-line-height-5-2:22px;--wb-font-line-height-6:24px;--wb-font-line-height-7:28px;--wb-font-line-height-8:32px;--wb-font-line-height-10:40px;--wb-font-weight-400:400;--wb-font-weight-500:500;--wb-font-weight-600:600;--wb-font-weight-700:700;--wb-font-display-size:var(--wb-font-size-11);--wb-font-display-line-height:var(--wb-font-line-height-10);--wb-font-display-weight:var(--wb-font-weight-700);--wb-font-h1-size:var(--wb-font-size-9);--wb-font-h1-line-height:var(--wb-font-line-height-8);--wb-font-h1-weight:var(--wb-font-weight-600);--wb-font-h2-size:var(--wb-font-size-8);--wb-font-h2-line-height:var(--wb-font-line-height-7);--wb-font-h2-weight:var(--wb-font-weight-600);--wb-font-h3-size:var(--wb-font-size-6);--wb-font-h3-line-height:var(--wb-font-line-height-6);--wb-font-h3-weight:var(--wb-font-weight-600);--wb-font-body-size:var(--wb-font-size-5);--wb-font-body-line-height:var(--wb-font-line-height-5-2);--wb-font-body-weight:var(--wb-font-weight-400);--wb-font-body-strong-size:var(--wb-font-size-5);--wb-font-body-strong-line-height:var(--wb-font-line-height-5-2);--wb-font-body-strong-weight:var(--wb-font-weight-600);--wb-font-caption-size:var(--wb-font-size-3);--wb-font-caption-line-height:var(--wb-font-line-height-4-2);--wb-font-caption-weight:var(--wb-font-weight-400);--wb-font-code-size:var(--wb-font-size-4);--wb-font-code-line-height:var(--wb-font-line-height-5);--wb-font-code-weight:var(--wb-font-weight-400);--wb-font-spec-h1-size:var(--wb-font-size-11);--wb-font-spec-h1-line-height:var(--wb-font-line-height-10);--wb-font-spec-h2-size:var(--wb-font-size-10);--wb-font-spec-h2-line-height:var(--wb-font-line-height-9);--wb-font-spec-h3-size:var(--wb-font-size-9);--wb-font-spec-h3-line-height:var(--wb-font-line-height-8);--wb-font-spec-h4-size:var(--wb-font-size-8);--wb-font-spec-h4-line-height:var(--wb-font-line-height-7);--wb-font-spec-h5-size:var(--wb-font-size-7);--wb-font-spec-h5-line-height:var(--wb-font-line-height-6-2);--wb-font-spec-h6-size:var(--wb-font-size-6);--wb-font-spec-h6-line-height:var(--wb-font-line-height-6);--wb-font-spec-h7-size:var(--wb-font-size-5);--wb-font-spec-h7-line-height:var(--wb-font-line-height-5-2);--wb-font-spec-h8-size:var(--wb-font-size-3);--wb-font-spec-h8-line-height:var(--wb-font-line-height-5);--wb-font-spec-body-sm-size:var(--wb-font-size-3);--wb-font-spec-body-sm-line-height:var(--wb-font-line-height-5);--wb-font-spec-body-md-size:var(--wb-font-size-5);--wb-font-spec-body-md-line-height:var(--wb-font-line-height-5-2);--wb-font-spec-body-lg-size:var(--wb-font-size-6);--wb-font-spec-body-lg-line-height:var(--wb-font-line-height-6);--wb-font-spec-body-xl-size:var(--wb-font-size-7);--wb-font-spec-body-xl-line-height:var(--wb-font-line-height-6-2);--wb-font-chatflow-h1-size:21px;--wb-font-chatflow-h1-line-height:37px;--wb-font-chatflow-h2-size:19px;--wb-font-chatflow-h2-line-height:33px;--wb-font-chatflow-h3-size:17px;--wb-font-chatflow-h3-line-height:30px;--wb-font-chatflow-h4-size:15px;--wb-font-chatflow-h4-line-height:var(--wb-font-line-height-6-2);--wb-font-chatflow-p1-highlight-size:15px;--wb-font-chatflow-p1-highlight-line-height:var(--wb-font-line-height-6-2);--wb-font-chatflow-p1-size:15px;--wb-font-chatflow-p1-line-height:var(--wb-font-line-height-6-2);--wb-font-chatflow-p2-size:var(--wb-font-size-4);--wb-font-chatflow-p2-line-height:23px;--wb-font-chatflow-p2-code-size:var(--wb-font-size-4);--wb-font-chatflow-p2-code-line-height:23px;--wb-font-chatflow-p3-size:11px;--wb-font-chatflow-p3-line-height:19px;--wb-shadow-none:none;--wb-shadow-sm:0 1px 2px color-mix(in srgb, var(--wb-palette-black-100) 8%, transparent);--wb-shadow-md:0 2px 8px color-mix(in srgb, var(--wb-palette-black-100) 12%, transparent);--wb-shadow-lg:0 4px 16px color-mix(in srgb, var(--wb-palette-black-100) 16%, transparent);--wb-shadow-xl:0 8px 32px color-mix(in srgb, var(--wb-palette-black-100) 20%, transparent);--wb-shadow-input:0 16px 32px -8px color-mix(in srgb, var(--wb-palette-black-100) 3%, transparent), 0 2px 4px -4px color-mix(in srgb, var(--wb-palettLine truncated
|
||||
.yc-capability-center .expert-center-dark{--wb-shadow-selection-quote-popup:0px 4px 12px 0px #0003, 0px 3px 6px 0px #00000029;--wb-shadow-medium:0 12px 32px #0000004d;--wb-shadow-heavy:0 20px 60px #0006;--wb-shadow-popup-medium:0px 6px 24px -4px color-mix(in srgb, var(--wb-palette-black-100) 20%, transparent), 0px 4px 6px -2px color-mix(in srgb, var(--wb-palette-black-100) 30%, transparent);--wb-shadow-popup-large:0px 24px 48px -8px color-mix(in srgb, var(--wb-palette-black-100) 40%, transparent), 0px 6px 12px -4px color-mix(in srgb, var(--wb-palette-black-100) 30%, transparent);--wb-shadow-input-box:0px 16px 32px -4px color-mix(in srgb, var(--wb-palette-black-100) 20%, transparent), 0px 2px 4px -2px color-mix(in srgb, var(--wb-palette-black-100) 40%, transparent);--wb-shadow-focus-ring:0 0 0 2px #4b91ff26;--wb-shadow-card-hover:0 8px 20px #00000080;--wb-shadow-input-card:0 6px 12px -8px #0006, 0 2px 4px -4px #0000004d;--wb-shadow-message-toolbar:0px 6px 24px 0px #00000052, 0px 4px 6px 0px #00000052;--wb-shadow-elevated:0 8px 24px #0000004d;--wb-shadow-focus-blue:0 0 0 2px #4a90d94d;--wb-shadow-error:0 4px 12px #f4433673;--wb-shadow-drawer:-4px -2px 16px #0006, 0 0 0 1px #ffffff14;--wb-shadow-drawer-fullscreen:0 -2px 16px #0006}
|
||||
.yc-capability-center{--wb-motion-duration-instant:.1s;--wb-motion-duration-fast:.15s;--wb-motion-duration-medium:.2s;--wb-motion-duration-gentle:.3s;--wb-motion-duration-slow:.4s;--wb-motion-duration-sm:.15s;--wb-motion-duration-base:.2s;--wb-motion-easing-decelerate:cubic-bezier(0, 0, .2, 1);--wb-motion-easing-accelerate:cubic-bezier(.4, 0, 1, 1);--wb-motion-easing-ease-in-out:cubic-bezier(.4, 0, .2, 1);--wb-motion-easing-standard:cubic-bezier(.4, 0, .2, 1);--wb-motion-easing-emphasized:cubic-bezier(.2, 0, 0, 1);--wb-motion-easing-linear:linear;--wb-size-3:12px;--wb-size-3-2:14px;--wb-size-4:16px;--wb-size-5:20px;--wb-size-6:24px;--wb-size-7:28px;--wb-size-8:32px;--wb-size-9:36px;--wb-size-10:40px;--wb-size-11:44px;--wb-size-12:48px;--wb-size-13:52px;--wb-size-14:56px;--wb-size-17:68px;--wb-size-control-sm:var(--wb-size-6);--wb-size-control-ms:var(--wb-size-7);--wb-size-control-md:var(--wb-size-8);--wb-size-control-lg:var(--wb-size-10);--wb-size-control-xl:var(--wb-size-12);--wb-size-control-2xl:var(--wb-size-14);--wb-icon-size-sm:var(--wb-size-3);--wb-icon-size-ms:var(--wb-size-3-2);--wb-icon-size-md:var(--wb-size-4);--wb-icon-size-lg:var(--wb-size-5);--wb-icon-size-xl:var(--wb-size-6);--wb-sash-hit-size:4px;--wb-sash-line-width:1.5px;--wb-titlebar-height:56px;--wb-titlebar-height-compact:40px}
|
||||
.yc-capability-center[data-electron-desktop=true]{--wb-titlebar-height:64px}
|
||||
.yc-capability-center[data-electron-desktop=true][data-platform=mac]{--wb-mac-traffic-light-safe-area:79px}
|
||||
.yc-capability-center{--wb-colleagues-layer-chat-float:900;--wb-colleagues-layer-chat-menu:2;--wb-colleagues-layer-promo:1400;--wb-colleagues-layer-promo-loading:1410;--wb-colleagues-layer-confirm:1500;--wb-colleagues-layer-confirm-top:1600}
|
||||
.yc-capability-center{--wb-alert-gap:var(--wb-spacing-3);--wb-alert-padding-y:var(--wb-spacing-4);--wb-alert-padding-x:var(--wb-spacing-5);--wb-alert-border-width:0;--wb-alert-border-radius:var(--wb-radius-lg);--wb-alert-attached-overlap:48px;--wb-alert-attached-border-radius:var(--wb-radius-4xl);--wb-alert-attached-padding-bottom:calc(var(--wb-alert-padding-y) + var(--wb-alert-attached-overlap));--wb-alert-font-size:var(--wb-font-body-size);--wb-alert-line-height:var(--wb-font-body-line-height);--wb-alert-font-weight:var(--wb-font-body-weight);--wb-alert-icon-size:var(--wb-icon-size-md);--wb-alert-label-margin-top:2px;--wb-alert-label-padding-y:1px;--wb-alert-label-padding-x:var(--wb-spacing-2);--wb-alert-label-border-radius:var(--wb-radius-sm);--wb-alert-label-font-size:var(--wb-font-size-2);--wb-alert-label-line-height:1.8;--wb-alert-label-font-weight:var(--wb-font-weight-500);--wb-alert-title-margin-bottom:var(--wb-spacing-1);--wb-alert-title-font-size:var(--wb-font-size-5);--wb-alert-title-line-height:var(--wb-font-line-height-5-2);--wb-alert-title-font-weight:var(--wb-font-weight-500);--wb-alert-content-color-text:var(--wb-color-text-tertiary);--wb-alert-actions-gap:var(--wb-spacing-3);--wb-alert-close-margin-top:1px;--wb-alert-close-icon-size:var(--wb-icon-size-md);--wb-alert-close-color-bg:transparent;--wb-alert-close-color-text:var(--wb-color-text-secondary);--wb-alert-close-color-text-hover:var(--wb-color-text-primary);--wb-alert-motion-duration:var(--wb-motion-duration-instant);--wb-alert-motion-easing:var(--wb-motion-easing-standard);--wb-alert-color-text:var(--wb-palette-black-90);--wb-alert-default-color-bg:var(--wb-color-bg-primary-default);--wb-alert-default-color-text:var(--wb-alert-color-text);--wb-alert-default-color-icon:var(--wb-color-text-secondary);--wb-alert-info-color-bg:#e7edfb;--wb-alert-info-color-text:var(--wb-alert-color-text);--wb-alert-info-color-icon:#0034b5;--wb-alert-success-color-bg:var(--wb-palette-green-1);--wb-alert-success-color-text:var(--wb-alert-color-text);--wb-alert-success-color-icon:var(--wb-palette-green-9);--wb-alert-warning-color-bg:#fff3e6;--wb-alert-warning-color-text:var(--wb-alert-color-text);--wb-alert-warning-color-icon:var(--wb-palette-orange-9);--wb-alert-error-color-bg:#fdefef;--wb-alert-error-color-text:var(--wb-alert-color-text);--wb-alert-error-color-icon:var(--wb-palette-red-9);--wb-alert-label-color-bg:color-mix(in srgb, currentColor 12%, transparent)}
|
||||
.yc-capability-center .expert-center-dark{--wb-alert-color-text:var(--wb-palette-white-90);--wb-alert-info-color-bg:#1d2852;--wb-alert-info-color-icon:var(--wb-palette-blue-9);--wb-alert-warning-color-bg:#462415;--wb-alert-warning-color-icon:var(--wb-palette-orange-9);--wb-alert-error-color-bg:#461523;--wb-alert-error-color-icon:var(--wb-palette-red-9)}
|
||||
.yc-capability-center{--wb-avatar-border-radius-circle:var(--wb-radius-full);--wb-avatar-border-radius-square:var(--wb-radius-md);--wb-avatar-fallback-border-width:calc(var(--wb-spacing-1) / 4);--wb-avatar-group-ring-width:var(--wb-spacing-1);--wb-avatar-group-margin-left:calc(var(--wb-spacing-3) * -1)}
|
||||
.yc-capability-center{--wb-avatar-bg:var(--wb-bg-tertiary);--wb-avatar-color-text:var(--wb-color-text-primary);--wb-avatar-font-weight:var(--wb-font-weight-500);--wb-avatar-fallback-border-color:color-mix(in srgb, var(--wb-palette-black-100) 8%, transparent);--wb-avatar-icon-color:var(--wb-icon-secondary,var(--wb-color-text-secondary));--wb-avatar-group-ring-color:var(--wb-bg-primary);--wb-avatar-group-overflow-bg:var(--wb-bg-tertiary);--wb-avatar-group-overflow-color:var(--wb-color-text-secondary)}
|
||||
.yc-capability-center .expert-center-dark{--wb-avatar-fallback-border-color:color-mix(in srgb, var(--wb-palette-white-100) 12%, transparent);--wb-avatar-bg:var(--wb-bg-secondary)}
|
||||
.yc-capability-center{--wb-breadcrumb-font-size:var(--wb-font-size-4);--wb-breadcrumb-line-height:var(--wb-font-line-height-5-2);--wb-breadcrumb-font-weight:var(--wb-font-weight-400);--wb-breadcrumb-current-font-weight:var(--wb-font-weight-600);--wb-breadcrumb-font-size-sm:var(--wb-font-size-3);--wb-breadcrumb-line-height-sm:var(--wb-font-line-height-5);--wb-breadcrumb-gap:6px;--wb-breadcrumb-leading-icon-size:var(--wb-icon-size-md);--wb-breadcrumb-leading-icon-border-radius:var(--wb-radius-xs);--wb-breadcrumb-item-max-width:200px;--wb-breadcrumb-link-border-radius:var(--wb-radius-xs);--wb-breadcrumb-transition-duration:var(--wb-motion-duration-instant);--wb-breadcrumb-transition-easing:var(--wb-motion-easing-standard)}
|
||||
.yc-capability-center{--wb-breadcrumb-text-color:var(--wb-color-text-tertiary);--wb-breadcrumb-separator-color:var(--wb-color-text-tertiary);--wb-breadcrumb-leading-icon-color:var(--wb-color-text-tertiary);--wb-breadcrumb-leading-icon-color-hover:var(--wb-color-text-primary);--wb-breadcrumb-current-color:var(--wb-color-text-primary);--wb-breadcrumb-link-color-hover:var(--wb-color-text-primary);--wb-breadcrumb-link-outline-focus:var(--wb-border-focus)}
|
||||
.yc-capability-center{--wb-button-height-xsmall:var(--wb-size-4);--wb-button-height-small:var(--wb-size-control-sm);--wb-button-height-medium:var(--wb-size-control-md);--wb-button-height-large:var(--wb-size-control-xl);--wb-button-min-width-small:var(--wb-size-10);--wb-button-min-width-medium:var(--wb-size-17);--wb-button-padding-small:var(--wb-spacing-3);--wb-button-padding-medium:var(--wb-spacing-4);--wb-button-padding-large:var(--wb-spacing-8);--wb-button-gap-small:var(--wb-spacing-1);--wb-button-gap-medium:var(--wb-spacing-2);--wb-button-gap-large:var(--wb-spacing-3);--wb-button-font-size-small:var(--wb-font-size-3);--wb-button-font-size-medium:var(--wb-font-size-5);--wb-button-font-size-large:var(--wb-font-size-5);--wb-button-line-height-small:var(--wb-font-line-height-5);--wb-button-line-height-medium:var(--wb-font-line-height-5-2);--wb-button-line-height-large:var(--wb-font-line-height-5-2);--wb-button-border-radius-pill:var(--wb-radius-full);--wb-button-border-radius-small:var(--wb-radius-md);--wb-button-border-radius-medium:var(--wb-radius-lg);--wb-button-border-radius-large:var(--wb-radius-xl);--wb-button-icon-size-small:var(--wb-icon-size-sm);--wb-button-icon-size-medium:var(--wb-icon-size-md);--wb-button-transition-duration:var(--wb-motion-duration-instant);--wb-button-transition-easing:var(--wb-motion-easing-standard)}
|
||||
.yc-capability-center{--wb-button-primary-bg:var(--wb-palette-black-90);--wb-button-primary-bg-hover:var(--wb-palette-black-75);--wb-button-primary-bg-active:var(--wb-palette-black-75);--wb-button-primary-bg-disabled:var(--wb-palette-black-20);--wb-button-primary-fg:var(--wb-palette-white-100);--wb-button-primary-fg-disabled:var(--wb-palette-white-100);--wb-button-secondary-bg:var(--wb-palette-white-100);--wb-button-secondary-bg-hover:var(--wb-palette-gray-2);--wb-button-secondary-bg-active:var(--wb-palette-gray-2);--wb-button-secondary-bg-disabled:var(--wb-palette-gray-2);--wb-button-secondary-fg:var(--wb-palette-black-90);--wb-button-secondary-fg-disabled:var(--wb-palette-black-30);--wb-button-secondary-border:var(--wb-color-border-solid);--wb-button-secondary-border-disabled:var(--wb-color-border-secondary);--wb-button-grey-bg:var(--wb-palette-gray-4);--wb-button-grey-bg-hover:var(--wb-palette-gray-5);--wb-button-grey-bg-active:var(--wb-palette-gray-5);--wb-button-grey-bg-disabled:var(--wb-palette-gray-4);--wb-button-grey-fg:var(--wb-palette-black-90);--wb-button-grey-fg-disabled:var(--wb-palette-black-30);--wb-button-ghost-bg:transparent;--wb-button-ghost-bg-hover:#0000000f;--wb-button-ghost-bg-active:#0000000f;--wb-button-ghost-fg:var(--wb-palette-black-90);--wb-button-ghost-fg-hover:var(--wb-palette-black-90);--wb-button-ghost-fg-active:var(--wb-palette-black-90);--wb-button-ghost-fg-disabled:var(--wb-palette-black-30);--wb-button-link-bg:transparent;--wb-button-link-bg-hover:transparent;--wb-button-link-bg-active:transparent;--wb-button-link-bg-disabled:transparent;--wb-button-link-fg:#1470b4;--wb-button-link-fg-hover:#1470b4;--wb-button-link-fg-disabled:#94b7d1;--wb-button-danger-bg:var(--wb-palette-red-7);--wb-button-danger-bg-hover:var(--wb-palette-red-6);--wb-button-danger-bg-active:var(--wb-palette-red-6);--wb-button-danger-bg-disabled:var(--wb-palette-red-3);--wb-button-danger-fg-on-solid:var(--wb-palette-white-100);--wb-button-danger-fg-on-solid-disabled:var(--wb-palette-white-100);--wb-button-danger-fg:var(--wb-palette-red-8);--wb-button-danger-fg-disabled:var(--wb-palette-red-3);--wb-button-danger-border:var(--wb-palette-red-8);--wb-button-danger-border-disabled:var(--wb-palette-red-3);--wb-button-danger-grey-fg:var(--wb-palette-red-8);--wb-button-danger-bg-soft-hover:color-mix(in srgb, var(--wb-color-text-error-default) 8%, transparent);--wb-button-danger-bg-soft-active:color-mix(in srgb, var(--wb-color-text-error-default) 16%, transparent);--wb-button-danger-ghost-fg:var(--wb-palette-red-8);--wb-button-success-fg:var(--wb-color-text-success-default);--wb-button-success-fg-disabled:var(--wb-color-text-success-disabled);--wb-button-success-fg-on-solid:var(--wb-palette-white-100);--wb-button-success-fg-on-solid-disabled:var(--wb-palette-white-100);--wb-button-success-bg-disabled:var(--wb-color-text-success-disabled);--wb-button-danger-secondary-fg:var(--wb-palette-red-9);--wb-button-danger-secondary-border:var(--wb-palette-red-2);--wb-button-danger-secondary-bg:var(--wb-palette-white-100);--wb-button-danger-secondary-bg-hover:var(--wb-palette-red-1);--wb-button-danger-secondary-bg-active:var(--wb-palette-red-1);--wb-button-danger-secondary-fg-disabled:var(--wb-palette-red-3);--wb-button-danger-secondary-border-disabled:var(--wb-palette-red-2);--wb-button-loading-primary-fg:var(--wb-palette-white-100);--wb-button-loading-secondary-fg:var(--wb-palette-black-100);--wb-button-loading-grey-fg:var(--wb-palette-black-100);--wb-button-loading-ghost-fg:var(--wb-palette-black-100);--wb-button-loading-link-fg:#1470b4;--wb-button-loading-danger-fg-on-solid:var(--wb-palette-white-100);--wb-button-loading-danger-fg:var(--wb-color-text-error-disabled);--wb-button-success-bg:#0cbf5b;--wb-button-success-bg-hover:#09a84f;--wb-button-login-bg:#1a1a1a;--wb-button-login-bg-hover:var(--wb-palette-black-100);--wb-button-login-bg-active:var(--wb-palette-black-100);--wb-button-inverse-bg-hover:color-mix(in srgb, var(--wb-palette-black-100) 96%, transparent);--wb-button-inverse-bg-active:color-mix(in srgb, var(--wb-palette-black-100) 80%, transparent);--wb-button-inverse-fg-disabled:var(--wb-palette-white-60);--wb-button-confirm-bg:var(--wb-palette-black-90);--wb-button-cancel-bg:var(--wb-palette-gray-4);--wb-button-soft-bg:color-mix(in srgb, var(--wb-palette-black-100) 8%, transparent);--wb-button-soft-bg-hover:color-mix(in srgb, var(--wb-palette-black-100) 12%, transparent);--wb-button-secondaryfill-bg-hover:#f0f0f0;--wb-button-secondary-fg-hover:var(--wb-palette-black-100);--wb-button-secondary-fg-hover-inverse:#f5f5f5}
|
||||
.yc-capability-center .expert-center-dark{--wb-button-primary-bg:var(--wb-palette-white-90);--wb-button-primary-bg-hover:var(--wb-palette-white-75);--wb-button-primary-bg-active:var(--wb-palette-white-75);--wb-button-primary-bg-disabled:var(--wb-palette-white-20);--wb-button-primary-fg:var(--wb-palette-black-90);--wb-button-primary-fg-disabled:var(--wb-palette-black-90);--wb-button-secondary-bg:#262626;--wb-button-secondary-bg-hover:#2e2e2e;--wb-button-secondary-bg-active:#2e2e2e;--wb-button-secondary-bg-disabled:#262626;--wb-button-secondary-fg:var(--wb-palette-white-90);--wb-button-secondary-fg-disabled:var(--wb-palette-white-30);--wb-button-secondary-border:var(--wb-color-border-solid);--wb-button-secondary-border-disabled:var(--wb-color-border-secondary);--wb-button-grey-bg:#383838;--wb-button-grey-bg-hover:#404040;--wb-button-grey-bg-active:#404040;--wb-button-grey-bg-disabled:#404040;--wb-button-grey-fg:var(--wb-palette-white-90);--wb-button-grey-fg-disabled:var(--wb-palette-white-30);--wb-button-ghost-bg:transparent;--wb-button-ghost-bg-hover:var(--wb-palette-white-10);--wb-button-ghost-bg-active:var(--wb-palette-white-10);--wb-button-ghost-fg:var(--wb-palette-white-90);--wb-button-ghost-fg-hover:var(--wb-palette-white-90);--wb-button-ghost-fg-active:var(--wb-palette-white-90);--wb-button-ghost-fg-disabled:var(--wb-palette-white-30);--wb-button-link-bg:transparent;--wb-button-link-bg-hover:transparent;--wb-button-link-bg-active:transparent;--wb-button-link-bg-disabled:transparent;--wb-button-link-fg:#4294d2;--wb-button-link-fg-hover:#4294d2;--wb-button-link-fg-disabled:#94b7d1;--wb-button-danger-bg:#e85c5c;--wb-button-danger-bg-hover:#ec7474;--wb-button-danger-bg-active:#ec7474;--wb-button-danger-bg-disabled:#803838;--wb-button-danger-fg-on-solid:var(--wb-palette-white-100);--wb-button-danger-fg-on-solid-disabled:var(--wb-palette-white-70);--wb-button-danger-fg:var(--wb-color-text-error-default);--wb-button-danger-fg-disabled:var(--wb-color-text-error-disabled);--wb-button-danger-border:var(--wb-color-text-error-default);--wb-button-danger-border-disabled:var(--wb-color-text-error-disabled);--wb-button-danger-bg-soft-hover:color-mix(in srgb, var(--wb-color-text-error-default) 12%, transparent);--wb-button-danger-bg-soft-active:color-mix(in srgb, var(--wb-color-text-error-default) 20%, transparent);--wb-button-danger-grey-fg:var(--wb-palette-red-9);--wb-button-danger-ghost-fg:var(--wb-palette-red-9);--wb-button-success-fg:var(--wb-color-text-success-default);--wb-button-success-fg-disabled:var(--wb-color-text-success-disabled);--wb-button-success-fg-on-solid:var(--wb-palette-white-90);--wb-button-success-fg-on-solid-disabled:var(--wb-palette-white-70);--wb-button-success-bg-disabled:var(--wb-color-text-success-disabled);--wb-button-danger-secondary-fg:var(--wb-palette-red-9);--wb-button-danger-secondary-border:var(--wb-palette-red-5);--wb-button-danger-secondary-bg:#262626;--wb-button-danger-secondary-bg-hover:color-mix(in srgb, var(--wb-palette-red-3) 40%, var(--wb-button-danger-secondary-bg) 60%);--wb-button-danger-secondary-bg-active:color-mix(in srgb, var(--wb-palette-red-3) 40%, var(--wb-button-danger-secondary-bg) 60%);--wb-button-danger-secondary-fg-disabled:var(--wb-color-text-error-disabled);--wb-button-danger-secondary-border-disabled:var(--wb-color-text-error-disabled);--wb-button-loading-primary-fg:var(--wb-palette-white-90);--wb-button-loading-secondary-fg:var(--wb-palette-white-30);--wb-button-loading-grey-fg:var(--wb-palette-white-30);--wb-button-loading-ghost-fg:var(--wb-palette-white-30);--wb-button-loading-link-fg:#94b7d1;--wb-button-loading-danger-fg-on-solid:var(--wb-palette-white-90);--wb-button-loading-danger-fg:var(--wb-color-text-error-disabled);--wb-button-success-bg:#0cbf5b;--wb-button-success-bg-hover:#09a84f;--wb-button-login-bg:var(--wb-palette-white-100);--wb-button-login-bg-hover:var(--wb-palette-white-90);--wb-button-login-bg-active:var(--wb-palette-white-80);--wb-button-inverse-bg-hover:color-mix(in srgb, var(--wb-palette-white-100) 80%, transparent);--wb-button-inverse-bg-active:color-mix(in srgb, var(--wb-palette-white-100) 70%, transparent);--wb-button-inverse-fg-disabled:color-mix(in srgb, var(--wb-palette-black-100) 40%, transparent);--wb-button-confirm-bg:var(--wb-palette-white-90);--wb-button-cancel-bg:var(--wb-palette-white-20);--wb-button-soft-bg:color-mix(in srgb, var(--wb-palette-white-100) 20%, transparent);--wb-button-soft-bg-hover:var(--wb-palette-black-100);--wb-button-secondaryfill-bg-hover:#474747;--wb-button-secondary-fg-hover:#e6e6e6;--wb-button-secondary-fg-hover-inverse:#404040}
|
||||
.yc-capability-center{--wb-checkbox-size-s:var(--wb-size-3-2);--wb-checkbox-size-m:var(--wb-size-4);--wb-checkbox-size:var(--wb-checkbox-size-m);--wb-checkbox-size-small:var(--wb-checkbox-size-s);--wb-checkbox-size-medium:var(--wb-checkbox-size-m);--wb-checkbox-border-radius:var(--wb-radius-sm);--wb-checkbox-border-width:1px;--wb-checkbox-mark-width:9px;--wb-checkbox-mark-height:1.5px;--wb-checkbox-focus-outline-width:2px;--wb-checkbox-focus-outline-offset:2px;--wb-checkbox-inner-gap:var(--wb-spacing-3);--wb-checkbox-group-gap:var(--wb-spacing-3);--wb-checkbox-font-size:var(--wb-font-size-5);--wb-checkbox-line-height:var(--wb-font-body-line-height);--wb-checkbox-motion-duration:var(--wb-motion-duration-instant);--wb-checkbox-motion-easing:var(--wb-motion-easing-standard);--wb-checkbox-check-duration:var(--wb-motion-duration-fast);--wb-checkbox-check-easing:var(--wb-motion-easing-standard)}
|
||||
.yc-capability-center{--wb-checkbox-color-label:var(--wb-color-text-primary);--wb-checkbox-color-label-disabled:var(--wb-color-text-tertiary);--wb-checkbox-color-bg:transparent;--wb-checkbox-color-bg-checked:#3d3d3d;--wb-checkbox-color-bg-disabled:#ebebeb;--wb-checkbox-color-bg-disabled-checked:#ebebeb;--wb-checkbox-color-border:var(--wb-palette-black-20);--wb-checkbox-color-border-hover:#0006;--wb-checkbox-color-border-checked:#3d3d3d;--wb-checkbox-color-border-disabled:var(--wb-color-border-solid);--wb-checkbox-color-border-disabled-checked:#dedede;--wb-checkbox-color-checkmark:var(--wb-palette-white-100);--wb-checkbox-color-mark-indeterminate:var(--wb-palette-white-100);--wb-checkbox-color-checkmark-disabled:var(--wb-color-text-disabled);--wb-checkbox-label-color:var(--wb-checkbox-color-label);--wb-checkbox-label-disabled-color:var(--wb-checkbox-color-label-disabled);--wb-checkbox-bg:var(--wb-checkbox-color-bg);--wb-checkbox-checked-bg:var(--wb-checkbox-color-bg-checked);--wb-checkbox-disabled-bg:var(--wb-checkbox-color-bg-disabled);--wb-checkbox-disabled-checked-bg:var(--wb-checkbox-color-bg-disabled-checked);--wb-checkbox-border:var(--wb-checkbox-color-border);--wb-checkbox-hover-border:var(--wb-checkbox-color-border-hover);--wb-checkbox-checked-border:var(--wb-checkbox-color-border-checked);--wb-checkbox-disabled-border:var(--wb-checkbox-color-border-disabled);--wb-checkbox-disabled-checked-border:var(--wb-checkbox-color-border-disabled-checked);--wb-checkbox-checkmark:var(--wb-checkbox-color-checkmark);--wb-checkbox-indeterminate-mark:var(--wb-checkbox-color-mark-indeterminate);--wb-checkbox-disabled-checkmark:var(--wb-checkbox-color-checkmark-disabled)}
|
||||
.yc-capability-center .expert-center-dark{--wb-checkbox-color-label:var(--wb-color-text-primary);--wb-checkbox-color-label-disabled:var(--wb-color-text-tertiary);--wb-checkbox-color-bg:transparent;--wb-checkbox-color-bg-checked:#e0e0e0;--wb-checkbox-color-bg-disabled:#3d3d3d;--wb-checkbox-color-bg-disabled-checked:#3d3d3d;--wb-checkbox-color-border:#414141;--wb-checkbox-color-border-hover:#fff6;--wb-checkbox-color-border-checked:#e0e0e0;--wb-checkbox-color-border-disabled:#414141;--wb-checkbox-color-border-disabled-checked:#3d3d3d;--wb-checkbox-color-checkmark:#141414;--wb-checkbox-color-mark-indeterminate:#141414;--wb-checkbox-color-checkmark-disabled:#ffffff4d;--wb-checkbox-label-color:var(--wb-checkbox-color-label);--wb-checkbox-label-disabled-color:var(--wb-checkbox-color-label-disabled);--wb-checkbox-bg:var(--wb-checkbox-color-bg);--wb-checkbox-checked-bg:var(--wb-checkbox-color-bg-checked);--wb-checkbox-disabled-bg:var(--wb-checkbox-color-bg-disabled);--wb-checkbox-disabled-checked-bg:var(--wb-checkbox-color-bg-disabled-checked);--wb-checkbox-border:var(--wb-checkbox-color-border);--wb-checkbox-hover-border:var(--wb-checkbox-color-border-hover);--wb-checkbox-checked-border:var(--wb-checkbox-color-border-checked);--wb-checkbox-disabled-border:var(--wb-checkbox-color-border-disabled);--wb-checkbox-disabled-checked-border:var(--wb-checkbox-color-border-disabled-checked);--wb-checkbox-checkmark:var(--wb-checkbox-color-checkmark);--wb-checkbox-indeterminate-mark:var(--wb-checkbox-color-mark-indeterminate);--wb-checkbox-disabled-checkmark:var(--wb-checkbox-color-checkmark-disabled)}
|
||||
.yc-capability-center{--wb-collapse-header-padding-y:var(--wb-spacing-3);--wb-collapse-header-padding-x:var(--wb-spacing-5);--wb-collapse-header-padding-y-sm:var(--wb-spacing-2-5);--wb-collapse-header-padding-x-sm:var(--wb-spacing-4);--wb-collapse-header-padding-y-lg:var(--wb-spacing-5);--wb-collapse-header-padding-x-lg:var(--wb-spacing-6);--wb-collapse-content-padding-y:var(--wb-spacing-3);--wb-collapse-content-padding-x:var(--wb-spacing-5);--wb-collapse-content-padding-y-sm:var(--wb-spacing-2-5);--wb-collapse-content-padding-x-sm:var(--wb-spacing-4);--wb-collapse-content-padding-y-lg:var(--wb-spacing-5);--wb-collapse-content-padding-x-lg:var(--wb-spacing-6);--wb-collapse-header-padding-compact:var(--wb-spacing-0);--wb-collapse-font-size:var(--wb-font-chatflow-h4-size);--wb-collapse-font-size-sm:var(--wb-font-size-3);--wb-collapse-header-font-size:var(--wb-font-chatflow-h4-size);--wb-collapse-header-font-size-sm:var(--wb-font-size-3);--wb-collapse-header-font-size-lg:var(--wb-font-size-6);--wb-collapse-header-line-height:1.7;--wb-collapse-header-line-height-sm:1.6;--wb-collapse-arrow-gap:var(--wb-spacing-2);--wb-collapse-arrow-size:var(--wb-icon-size-md);--wb-collapse-arrow-size-sm:var(--wb-icon-size-ms);--wb-collapse-border-radius:var(--wb-radius-lg);--wb-collapse-motion-duration:var(--wb-motion-duration-instant);--wb-collapse-motion-easing:var(--wb-motion-easing-standard);--wb-collapse-expand-duration:var(--wb-motion-duration-base);--wb-collapse-expand-easing:var(--wb-motion-easing-standard)}
|
||||
.yc-capability-center{--wb-collapse-border-color:var(--wb-color-border-primary);--wb-collapse-header-color-bg:var(--wb-bg-secondary);--wb-collapse-header-color-bg-hover:var(--wb-bg-hover-light);--wb-collapse-content-color-bg:var(--wb-bg-primary);--wb-collapse-color-text:var(--wb-color-text-primary);--wb-collapse-header-color-text:var(--wb-color-text-secondary);--wb-collapse-color-text-disabled:var(--wb-color-text-disabled);--wb-collapse-arrow-color:var(--wb-color-text-tertiary);--wb-collapse-header-color-text-secondary:var(--wb-color-text-tertiary);--wb-collapse-header-arrow-color-secondary:var(--wb-color-text-disabled);--wb-collapse-header-color-text-hover:#000000e6;--wb-collapse-header-arrow-color-hover:#000000e6}
|
||||
.yc-capability-center .expert-center-dark{--wb-collapse-border-color:var(--wb-color-border-secondary);--wb-collapse-header-color-bg:var(--wb-bg-secondary);--wb-collapse-header-color-bg-hover:var(--wb-bg-hover-light);--wb-collapse-content-color-bg:var(--wb-bg-primary);--wb-collapse-color-text:var(--wb-color-text-primary);--wb-collapse-header-color-text:var(--wb-color-text-secondary);--wb-collapse-color-text-disabled:var(--wb-color-text-disabled);--wb-collapse-arrow-color:var(--wb-color-text-tertiary);--wb-collapse-header-color-text-secondary:var(--wb-color-text-tertiary);--wb-collapse-header-arrow-color-secondary:var(--wb-color-text-disabled);--wb-collapse-header-color-text-hover:#ffffffe6;--wb-collapse-header-arrow-color-hover:#ffffffe6}
|
||||
.yc-capability-center{--wb-input-height-small:var(--wb-size-control-ms);--wb-input-height-medium:var(--wb-size-control-md);--wb-input-height-large:var(--wb-size-control-xl);--wb-input-font-size-small:var(--wb-font-size-3);--wb-input-font-size:var(--wb-font-size-5);--wb-input-font-size-large:var(--wb-font-size-5);--wb-input-line-height:var(--wb-font-body-line-height);--wb-input-padding-x:var(--wb-spacing-3);--wb-input-padding-x-large:var(--wb-spacing-4);--wb-input-has-button-padding-right:var(--wb-spacing-2);--wb-input-has-button-padding-right-large:var(--wb-spacing-3);--wb-input-gap:var(--wb-spacing-2);--wb-input-border-radius-small:var(--wb-radius-md);--wb-input-border-radius:var(--wb-radius-lg);--wb-input-border-radius-large:var(--wb-radius-xl);--wb-input-border-radius-textarea:var(--wb-radius-lg);--wb-input-icon-size-small:var(--wb-icon-size-ms);--wb-input-icon-size:var(--wb-icon-size-md);--wb-input-icon-button-size-small:var(--wb-size-5);--wb-input-icon-button-size:var(--wb-size-6);--wb-input-icon-button-border-radius:var(--wb-radius-md);--wb-textarea-padding-y:var(--wb-spacing-2);--wb-textarea-padding-x:var(--wb-spacing-3);--wb-textarea-font-size:var(--wb-font-body-size);--wb-textarea-line-height:var(--wb-font-body-line-height);--wb-textarea-min-height:calc(var(--wb-textarea-line-height) + var(--wb-textarea-padding-y) * 2 + 2px);--wb-textarea-scrollbar-width:12px;--wb-textarea-scrollbar-gap:6px;--wb-textarea-scrollbar-thumb-radius:100px;--wb-input-addon-padding-x:var(--wb-spacing-3);--wb-input-addon-font-size:var(--wb-font-body-size);--wb-input-addon-line-height:var(--wb-font-body-line-height);--wb-input-count-font-size:var(--wb-font-caption-size);--wb-input-number-controls-width:var(--wb-size-7);--wb-input-number-step-color:var(--wb-color-text-tertiary);--wb-input-number-step-color-hover:var(--wb-color-text-secondary);--wb-input-number-step-color-bg:var(--wb-color-bg-secondary-hover-active);--wb-input-number-step-color-bg-hover:var(--wb-color-bg-primary-hover);--wb-input-motion-duration:var(--wb-motion-duration-instant);--wb-input-motion-easing:var(--wb-motion-easing-standard)}
|
||||
.yc-capability-center{--wb-input-color-text:var(--wb-color-text-primary);--wb-input-color-bg:var(--wb-bg-primary);--wb-input-border-color:var(--wb-color-border-primary);--wb-input-border-color-hover:var(--wb-palette-black-30);--wb-input-border-color-focus:var(--wb-palette-black-30);--wb-input-color-placeholder:var(--wb-color-text-disabled);--wb-input-color-text-disabled:var(--wb-color-text-disabled);--wb-input-disabled-bg:var(--wb-color-bg-secondary-hover-active);--wb-input-disabled-border-color:var(--wb-color-border-primary);--wb-input-color-icon:var(--wb-color-text-tertiary);--wb-input-color-icon-hover:var(--wb-color-text-secondary);--wb-input-color-icon-disabled:var(--wb-color-text-disabled);--wb-input-status-error-color:var(--wb-color-text-error-default);--wb-input-status-error-bg:var(--wb-color-bg-secondary-default);--wb-input-filled-color-bg:var(--wb-bg-inset-strong);--wb-input-filled-color-bg-hover:var(--wb-color-bg-primary-hover);--wb-input-addon-color-text:var(--wb-color-text-secondary);--wb-input-addon-color-bg:var(--wb-bg-secondary);--wb-input-addon-border-color:var(--wb-color-border-primary);--wb-input-count-color:var(--wb-color-text-tertiary);--wb-textarea-color-placeholder:var(--wb-color-text-disabled);--wb-textarea-count-color:var(--wb-color-text-disabled);--wb-textarea-scrollbar-thumb-color:var(--wb-palette-gray-4);--wb-input-number-step-color:var(--wb-color-text-tertiary);--wb-input-number-step-color-hover:var(--wb-color-text-secondary);--wb-input-number-step-color-bg:var(--wb-color-bg-secondary-hover-active);--wb-input-number-step-color-bg-hover:var(--wb-color-bg-primary-hover)}
|
||||
.yc-capability-center .expert-center-dark{--wb-input-color-text:var(--wb-color-text-primary);--wb-input-color-bg:var(--wb-bg-primary);--wb-input-border-color:var(--wb-color-border-solid);--wb-input-border-color-hover:var(--wb-palette-white-30);--wb-input-border-color-focus:var(--wb-palette-white-30);--wb-input-color-placeholder:var(--wb-color-text-disabled);--wb-input-color-text-disabled:var(--wb-color-text-disabled);--wb-input-disabled-bg:var(--wb-color-bg-secondary-hover-active);--wb-input-disabled-border-color:var(--wb-color-border-primary);--wb-input-color-icon:var(--wb-color-text-tertiary);--wb-input-color-icon-hover:var(--wb-color-text-secondary);--wb-input-color-icon-disabled:var(--wb-color-text-disabled);--wb-input-status-error-color:var(--wb-color-text-error-default);--wb-input-status-error-bg:transparent;--wb-input-filled-color-bg:var(--wb-bg-inset-strong);--wb-input-filled-color-bg-hover:var(--wb-color-bg-primary-hover);--wb-input-addon-color-text:var(--wb-color-text-secondary);--wb-input-addon-color-bg:var(--wb-bg-secondary);--wb-input-addon-border-color:var(--wb-color-border-solid);--wb-input-count-color:var(--wb-color-text-tertiary);--wb-textarea-color-placeholder:var(--wb-color-text-disabled);--wb-textarea-count-color:var(--wb-color-text-disabled);--wb-textarea-scrollbar-thumb-color:var(--wb-palette-gray-4);--wb-input-number-step-color:var(--wb-color-text-tertiary);--wb-input-number-step-color-hover:var(--wb-color-text-secondary);--wb-input-number-step-color-bg:var(--wb-color-bg-secondary-hover-active);--wb-input-number-step-color-bg-hover:var(--wb-color-bg-primary-active)}
|
||||
.yc-capability-center{--wb-switch-track-width:26px;--wb-switch-track-height:var(--wb-size-4);--wb-switch-track-padding:var(--wb-spacing-1);--wb-switch-thumb-size:var(--wb-size-3);--wb-switch-thumb-translate:8px;--wb-switch-track-width-small:var(--wb-size-6);--wb-switch-track-height-small:14px;--wb-switch-thumb-size-small:10px;--wb-switch-thumb-translate-small:8px;--wb-switch-track-width-large:34px;--wb-switch-track-height-large:20px;--wb-switch-thumb-size-large:var(--wb-size-4);--wb-switch-thumb-translate-large:12px;--wb-switch-font-size:var(--wb-font-body-size);--wb-switch-font-size-small:var(--wb-font-caption-size);--wb-switch-line-height:var(--wb-font-body-line-height);--wb-switch-line-height-small:var(--wb-font-caption-line-height);--wb-switch-gap:var(--wb-spacing-3);--wb-switch-radius:var(--wb-radius-full);--wb-switch-duration:var(--wb-motion-duration-fast);--wb-switch-easing:var(--wb-motion-easing-standard);--wb-switch-outline-focus-width:2px;--wb-switch-outline-focus-offset:2px;--wb-switch-color-outline-focus:var(--wb-border-focus);--wb-switch-spinner-border-width:1.5px;--wb-switch-spinner-duration:.8s;--wb-switch-track-color-spinner:var(--wb-control-selected-fg)}
|
||||
.yc-capability-center{--wb-switch-color-text:var(--wb-color-text-primary);--wb-switch-track-bg:var(--wb-palette-gray-5);--wb-switch-track-border-color:var(--wb-border-default);--wb-switch-track-bg-hover-border-color:var(--wb-border-strong);--wb-switch-thumb-bg:var(--wb-control-selected-fg);--wb-switch-thumb-shadow:var(--wb-shadow-sm);--wb-switch-track-bg-checked:var(--wb-brand-primary);--wb-switch-track-border-color-checked:var(--wb-brand-primary);--wb-switch-track-bg-checked-hover:var(--wb-color-text-brand-hover);--wb-switch-track-border-color-checked-hover:var(--wb-color-text-brand-hover);--wb-switch-track-bg-checked-disabled:var(--wb-color-text-brand-disabled);--wb-switch-track-bg-disabled:var(--wb-color-bg-primary-default);--wb-switch-thumb-bg-disabled-unchecked:var(--wb-control-selected-fg);--wb-switch-track-bg-danger:var(--wb-palette-red-9);--wb-switch-track-border-color-danger:var(--wb-palette-red-9);--wb-switch-track-bg-danger-hover:var(--wb-color-text-error-hover);--wb-switch-track-border-color-danger-hover:var(--wb-color-text-error-hover);--wb-switch-track-bg-danger-disabled:var(--wb-color-text-error-disabled)}
|
||||
.yc-capability-center .expert-center-dark{--wb-switch-color-text:var(--wb-color-text-primary);--wb-switch-track-bg:var(--wb-bg-elevated);--wb-switch-track-border-color:var(--wb-border-default);--wb-switch-track-bg-hover-border-color:var(--wb-border-strong);--wb-switch-thumb-bg:var(--wb-control-selected-fg);--wb-switch-thumb-shadow:var(--wb-shadow-sm);--wb-switch-track-bg-checked:var(--wb-brand-primary);--wb-switch-track-border-color-checked:var(--wb-brand-primary);--wb-switch-track-bg-checked-hover:var(--wb-color-text-brand-hover);--wb-switch-track-border-color-checked-hover:var(--wb-color-text-brand-hover);--wb-switch-track-bg-checked-disabled:var(--wb-palette-brand-4);--wb-switch-track-bg-disabled:var(--wb-palette-gray-5);--wb-switch-thumb-bg-disabled-unchecked:color-mix(in srgb, var(--wb-palette-white-100) 72%, var(--wb-palette-black-100));--wb-switch-track-color-spinner:var(--wb-switch-thumb-bg-disabled-unchecked);--wb-switch-track-bg-danger:var(--wb-palette-red-9);--wb-switch-track-border-color-danger:var(--wb-palette-red-9);--wb-switch-track-bg-danger-hover:var(--wb-color-text-error-hover);--wb-switch-track-border-color-danger-hover:var(--wb-color-text-error-hover);--wb-switch-track-bg-danger-disabled:var(--wb-palette-red-6)}
|
||||
.yc-capability-center{--wb-modal-overlay-mask-blur:6px;--wb-modal-panel-bg:var(--wb-bg-primary);--wb-modal-border-color:var(--wb-color-border-solid);--wb-modal-radius:var(--wb-radius-4xl);--wb-modal-radius-confirm:var(--wb-radius-xl);--wb-modal-width:100%;--wb-modal-max-height:calc(100vh - 160px);--wb-modal-max-width-s:480px;--wb-modal-max-width-m:640px;--wb-modal-max-width-ml:640px;--wb-modal-max-width-l:960px;--wb-modal-font-title-size:var(--wb-font-h3-size);--wb-modal-font-title-line-height:var(--wb-font-h3-line-height);--wb-modal-font-title-weight:var(--wb-font-h3-weight);--wb-modal-text-color:var(--wb-color-text-primary);--wb-modal-font-body-size:var(--wb-font-body-size);--wb-modal-font-body-line-height:var(--wb-font-body-line-height);--wb-modal-font-body-weight:var(--wb-font-body-weight);--wb-modal-text-color-secondary:var(--wb-color-text-secondary);--wb-modal-body-scrollbar-thumb-color:var(--wb-palette-gray-4);--wb-modal-back-btn-size:var(--wb-size-control-sm);--wb-modal-back-btn-bg-hover:var(--wb-bg-hover);--wb-modal-back-btn-transition-duration:var(--wb-motion-duration-fast);--wb-modal-close-btn-icon-size:var(--wb-icon-size-md);--wb-modal-icon-size-lg:var(--wb-icon-size-lg);--wb-modal-motion-duration-base:var(--wb-motion-duration-base);--wb-modal-motion-easing-standard:var(--wb-motion-easing-standard);--wb-modal-motion-easing-emphasized:var(--wb-motion-easing-emphasized);--wb-modal-motion-duration-enter:var(--wb-motion-duration-medium);--wb-modal-motion-duration-exit:var(--wb-motion-duration-fast);--wb-modal-motion-easing-enter:var(--wb-motion-easing-decelerate);--wb-modal-motion-easing-exit:var(--wb-motion-easing-accelerate);--wb-modal-motion-scale-exit:.96;--wb-modal-spacing-1:var(--wb-spacing-1);--wb-modal-spacing-2:var(--wb-spacing-2);--wb-modal-spacing-3:var(--wb-spacing-3);--wb-modal-spacing-4:var(--wb-spacing-4);--wb-modal-spacing-5:var(--wb-spacing-5);--wb-modal-spacing-7:var(--wb-spacing-7);--wb-modal-spacing-10:var(--wb-spacing-10)}
|
||||
.yc-capability-center{--wb-modal-overlay-mask-color:#00000026;--wb-modal-body-scrollbar-thumb-color:var(--wb-palette-gray-4);--wb-shadow-dialog:0px 8px 48px -4px color-mix(in srgb, var(--wb-palette-black-100) 12%, transparent), 0px 6px 12px -4px color-mix(in srgb, var(--wb-palette-black-100) 6%, transparent)}
|
||||
.yc-capability-center .expert-center-dark{--wb-modal-overlay-mask-color:#0006;--wb-modal-body-scrollbar-thumb-color:var(--wb-palette-white-10);--wb-shadow-dialog:0px 8px 48px -4px color-mix(in srgb, var(--wb-palette-black-100) 60%, transparent), 0px 6px 12px -4px color-mix(in srgb, var(--wb-palette-black-100) 30%, transparent)}
|
||||
.yc-capability-center{--wb-colleagues-layer-dialog:1200;--wb-colleagues-layer-dialog-popover:1205;--wb-colleagues-layer-dialog-local-modal:1208;--wb-colleagues-layer-dialog-hire:1210;--wb-colleagues-layer-dialog-tdocs:1220;--wb-colleagues-layer-dialog-bind-channel:1230;--wb-colleagues-layer-dialog-publish:1240;--wb-modal-spacing-1:var(--wb-spacing-1);--wb-modal-spacing-2:var(--wb-spacing-2);--wb-modal-spacing-3:var(--wb-spacing-3);--wb-modal-spacing-4:var(--wb-spacing-4);--wb-modal-spacing-5:var(--wb-spacing-5);--wb-modal-spacing-7:var(--wb-spacing-7);--wb-modal-spacing-10:var(--wb-spacing-10);--wb-modal-motion-duration-fast:var(--wb-motion-duration-fast);--wb-modal-motion-duration-base:var(--wb-motion-duration-base);--wb-modal-motion-easing-standard:var(--wb-motion-easing-standard);--wb-modal-motion-easing-emphasized:var(--wb-motion-easing-emphasized);--wb-modal-color-mask:var(--wb-color-mask-color);--wb-modal-bg:var(--wb-bg-primary);--wb-modal-bg-hover:var(--wb-bg-hover);--wb-modal-color-text-primary:var(--wb-color-text-primary);--wb-modal-color-text-secondary:var(--wb-color-text-secondary);--wb-modal-color-border:var(--wb-color-border-primary);--wb-modal-font-title-size:var(--wb-font-h3-size);--wb-modal-font-title-line-height:var(--wb-font-h3-line-height);--wb-modal-font-title-weight:var(--wb-font-h3-weight);--wb-modal-font-body-size:var(--wb-font-body-size);--wb-modal-font-body-line-height:var(--wb-font-body-line-height);--wb-modal-font-body-weight:var(--wb-font-body-weight);--wb-modal-radius:var(--wb-radius-4xl);--wb-modal-radius-confirm:var(--wb-radius-xl);--wb-modal-size-control-sm:var(--wb-size-control-sm);--wb-modal-icon-size-md:var(--wb-icon-size-md);--wb-modal-icon-size-lg:var(--wb-icon-size-lg)}
|
||||
.yc-capability-center{--wb-bg-modal:var(--wb-palette-white-100);--wb-color-bg-modal:var(--wb-bg-modal);--wb-modal-scrollbar-thumb:var(--wb-palette-black-10);--wb-shadow-dialog-md:0 8px 24px #00000014}
|
||||
.yc-capability-center .expert-center-dark{--wb-bg-modal:var(--wb-palette-gray-1);--wb-color-bg-modal:#2d2d2d;--wb-modal-scrollbar-thumb:var(--wb-palette-white-10);--wb-shadow-dialog-md:0 8px 24px #00000047}
|
||||
.yc-capability-center{--wb-message-min-height:var(--wb-size-12);--wb-message-max-height:200px;--wb-message-padding-x:var(--wb-spacing-5);--wb-message-padding-y:var(--wb-spacing-4);--wb-message-gap:var(--wb-spacing-3);--wb-message-close-gap:var(--wb-spacing-7);--wb-message-stack-gap:var(--wb-spacing-6);--wb-message-border-radius:var(--wb-radius-xl);--wb-message-font-size:var(--wb-font-body-size);--wb-message-line-height:var(--wb-font-body-line-height);--wb-message-icon-size:var(--wb-icon-size-lg);--wb-message-close-icon-size:var(--wb-icon-size-md);--wb-message-close-size:var(--wb-icon-size-lg);--wb-message-close-padding:var(--wb-spacing-1);--wb-message-close-border-radius:var(--wb-radius-sm);--wb-message-enter-duration:var(--wb-motion-duration-fast);--wb-message-enter-easing:var(--wb-motion-easing-decelerate);--wb-message-exit-duration:var(--wb-motion-duration-instant);--wb-message-exit-easing:var(--wb-motion-easing-accelerate);--wb-message-translate:var(--wb-spacing-4);--wb-message-spin-duration:.7s;--wb-message-reposition-duration:.25s;--wb-message-close-transition-duration:var(--wb-motion-duration-sm);--wb-message-z-index:100010}
|
||||
.yc-capability-center{--wb-message-color-bg:var(--wb-palette-white-100);--wb-message-color-text:var(--wb-palette-black-90);--wb-message-border:1px solid var(--wb-color-border-primary);--wb-message-shadow:var(--wb-shadow-dialog);--wb-message-success-color-icon:var(--wb-status-success);--wb-message-error-color-icon:var(--wb-status-error);--wb-message-info-color-icon:#000;--wb-message-warning-color-icon:var(--wb-status-warning);--wb-message-loading-color-icon:var(--wb-palette-brand-8);--wb-message-close-color-icon:var(--wb-palette-black-50);--wb-message-close-color-icon-hover:var(--wb-palette-black-75);--wb-message-close-color-bg-hover:var(--wb-bg-hover)}
|
||||
.yc-capability-center .expert-center-dark{--wb-message-color-bg:var(--wb-bg-elevated);--wb-message-color-text:var(--wb-palette-white-90);--wb-message-border:1px solid #4a4a4a;--wb-message-success-color-icon:#059465;--wb-message-info-color-icon:#fff;--wb-message-warning-color-icon:#cf6e2d;--wb-message-error-color-icon:#c64751;--wb-message-loading-color-icon:#00c29a;--wb-message-close-color-icon:var(--wb-palette-white-50);--wb-message-close-color-icon-hover:var(--wb-palette-white-80);--wb-message-close-color-bg-hover:var(--wb-bg-hover)}
|
||||
.yc-capability-center{--wb-bg-popover:var(--wb-palette-white-100);--wb-border-popover:color-mix(in srgb, var(--wb-palette-black-100) 6%, transparent);--wb-border-popover-strong:color-mix(in srgb, var(--wb-palette-black-100) 24%, transparent);--wb-popover-border-color:var(--wb-color-border-solid);--wb-popover-shadow:0px 6px 24px 0px #0000000a, 0px 4px 6px 0px #0000000a;--wb-shadow-popover:0 4px 12px -4px color-mix(in srgb, var(--wb-palette-black-100) 4%, transparent), 0 3px 6px -8px color-mix(in srgb, var(--wb-palette-black-100) 4%, transparent);--wb-bg-popover-picker:var(--wb-bg-popover)}
|
||||
.yc-capability-center{--wb-popover-padding:var(--wb-spacing-3);--wb-popover-transition-duration:var(--wb-motion-duration-fast);--wb-popover-transition-easing:var(--wb-motion-easing-standard);--wb-popover-expand-duration-in:var(--wb-motion-duration-fast);--wb-popover-expand-duration-out:var(--wb-motion-duration-fast);--wb-popover-expand-easing-in:var(--wb-motion-easing-decelerate);--wb-popover-expand-easing-out:var(--wb-motion-easing-accelerate);--wb-popover-expand-scale-collapsed:.88;--wb-popover-font-size:var(--wb-font-body-size);--wb-popover-line-height:var(--wb-font-body-line-height);--wb-popover-radius:var(--wb-radius-2xl);--wb-popover-text-color:var(--wb-color-text-primary)}
|
||||
.yc-capability-center .expert-center-dark{--wb-bg-popover:#242424;--wb-border-popover:color-mix(in srgb, var(--wb-palette-white-100) 10%, transparent);--wb-border-popover-strong:color-mix(in srgb, var(--wb-palette-white-100) 28%, transparent);--wb-popover-border-color:var(--wb-color-border-solid);--wb-popover-shadow:var(--shadow-md-offsetx-2,0px) 6px 24px var(--shadow-md-spread-2,-4px) #0006, var(--shadow-md-offsetx-1,0px) 4px 6px var(--shadow-md-spread-1,-8px) #0003;--wb-shadow-popover:0 4px 16px var(--wb-bg-overlay);--wb-bg-popover-picker:var(--wb-bg-popover)}
|
||||
.yc-capability-center{--wb-tabs-height-s:var(--wb-size-control-ms);--wb-tabs-height:var(--wb-size-control-md);--wb-tabs-height-l:var(--wb-size-control-xl);--wb-tabs-height-xl:var(--wb-size-control-2xl);--wb-tabs-font-size-s:var(--wb-font-size-4);--wb-tabs-font-size:var(--wb-font-size-5);--wb-tabs-font-size-l:var(--wb-font-size-6);--wb-tabs-font-size-xl:var(--wb-font-size-8);--wb-tabs-line-height-s:var(--wb-font-line-height-5-2);--wb-tabs-line-height:var(--wb-font-line-height-5-2);--wb-tabs-line-height-l:var(--wb-font-line-height-5-2);--wb-tabs-line-height-xl:28px;--wb-tabs-default-tab-gap:var(--wb-spacing-6);--wb-tabs-text-tab-gap:var(--wb-spacing-8);--wb-tabs-button-tab-gap:var(--wb-spacing-2);--wb-tabs-default-inkbar-height:2px;--wb-tabs-default-inkbar-height-l:3px;--wb-tabs-vertical-default-inkbar-height:3px;--wb-tabs-vertical-default-min-height:36px;--wb-tabs-arrow-btn-shadow-gap:var(--wb-spacing-3);--wb-tabs-arrow-btn-mask-width:28px;--wb-tabs-arrow-btn-icon-size:var(--wb-icon-size-md);--wb-tabs-arrow-btn-icon-size-xl:var(--wb-icon-size-lg);--wb-tabs-arrow-btn-width:var(--wb-size-5);--wb-tabs-arrow-btn-height:var(--wb-size-5);--wb-tabs-arrow-btn-width-xl:var(--wb-size-7);--wb-tabs-arrow-btn-height-xl:var(--wb-size-7);--wb-tabs-button-padding-x-s:var(--wb-spacing-3);--wb-tabs-button-padding-x:var(--wb-spacing-4);--wb-tabs-button-padding-x-l:var(--wb-spacing-6);--wb-tabs-button-padding-x-xl:var(--wb-spacing-7);--wb-tabs-button-border-radius-s:var(--wb-radius-md);--wb-tabs-button-border-radius:var(--wb-radius-lg);--wb-tabs-button-border-radius-l:var(--wb-radius-xl);--wb-tabs-button-border-radius-xl:var(--wb-radius-2xl);--wb-tabs-button-line-height-l:var(--wb-font-line-height-7);--wb-tabs-button-font-size-l:var(--wb-font-size-7);--wb-tabs-addon-gap:var(--wb-spacing-6);--wb-tabs-panel-gap:var(--wb-spacing-4);--wb-tabs-vertical-panel-gap:var(--wb-spacing-5);--wb-tabs-vertical-default-padding-x:var(--wb-spacing-2);--wb-tabs-vertical-default-padding-y:var(--wb-spacing-4);--wb-tabs-panel-duration:var(--wb-motion-duration-gentle);--wb-tabs-panel-easing:var(--wb-motion-easing-standard);--wb-tabs-motion-duration:var(--wb-motion-duration-instant);--wb-tabs-motion-easing:var(--wb-motion-easing-standard);--wb-tabs-inkbar-duration:var(--wb-motion-duration-fast);--wb-tabs-inkbar-easing:var(--wb-motion-easing-standard)}
|
||||
.yc-capability-center{--wb-tabs-default-color-text:var(--wb-color-text-secondary);--wb-tabs-default-color-text-hover:var(--wb-color-text-solid);--wb-tabs-default-color-text-active:var(--wb-color-text-solid);--wb-tabs-default-color-text-disabled:var(--wb-color-text-disabled);--wb-tabs-text-color-text:var(--wb-color-text-tertiary);--wb-tabs-text-color-text-hover:var(--wb-color-text-primary);--wb-tabs-text-color-text-active:var(--wb-color-text-primary);--wb-tabs-text-color-text-disabled:var(--wb-color-text-disabled);--wb-tabs-button-color-bg:transparent;--wb-tabs-button-color-bg-hover:var(--wb-color-bg-primary-active);--wb-tabs-button-color-bg-active:var(--wb-bg-hover-light);--wb-tabs-button-color-bg-disabled:transparent;--wb-tabs-button-color-text:var(--wb-color-text-tertiary);--wb-tabs-button-color-text-hover:var(--wb-color-text-tertiary);--wb-tabs-button-color-text-active:var(--wb-color-text-primary);--wb-tabs-button-color-text-disabled:var(--wb-color-text-disabled);--wb-tabs-button-primary-color-bg:transparent;--wb-tabs-button-primary-color-bg-hover:transparent;--wb-tabs-button-primary-color-bg-active:var(--wb-palette-black-75);--wb-tabs-button-primary-color-bg-disabled:transparent;--wb-tabs-button-primary-color-text:var(--wb-color-text-secondary);--wb-tabs-button-primary-color-text-hover:var(--wb-color-text-primary);--wb-tabs-button-primary-color-text-active:var(--wb-text-on-primary);--wb-tabs-button-primary-color-text-disabled:var(--wb-color-text-disabled);--wb-tabs-inkbar-color-bg:var(--wb-color-text-solid);--wb-tabs-arrow-btn-color-bg:var(--wb-bg-primary);--wb-tabs-arrow-btn-color-text:var(--wb-color-text-tertiary);--wb-tabs-arrow-btn-color-text-hover:var(--wb-color-text-solid);--wb-tabs-arrow-btn-color-mask-left:linear-gradient(to right, var(--wb-bg-primary) 50%, transparent 100%);--wb-tabs-arrow-btn-color-mask-right:linear-gradient(to right, transparent 0%, var(--wb-bg-primary) 50%);--wb-tabs-arrow-btn-color-shadow:var(--wb-shadow-md);--wb-tabs-vertical-divider-color-bg:var(--wb-color-border-primary)}
|
||||
.yc-capability-center .expert-center-dark{--wb-tabs-default-color-text:var(--wb-color-text-secondary);--wb-tabs-default-color-text-hover:var(--wb-color-text-solid);--wb-tabs-default-color-text-active:var(--wb-color-text-solid);--wb-tabs-default-color-text-disabled:var(--wb-color-text-disabled);--wb-tabs-text-color-text:var(--wb-color-text-tertiary);--wb-tabs-text-color-text-hover:var(--wb-color-text-primary);--wb-tabs-text-color-text-active:var(--wb-color-text-primary);--wb-tabs-text-color-text-disabled:var(--wb-color-text-disabled);--wb-tabs-button-color-bg:transparent;--wb-tabs-button-color-bg-hover:var(--wb-color-bg-primary-active);--wb-tabs-button-color-bg-active:var(--wb-bg-hover-light);--wb-tabs-button-color-bg-disabled:transparent;--wb-tabs-button-color-text:var(--wb-color-text-tertiary);--wb-tabs-button-color-text-hover:var(--wb-color-text-tertiary);--wb-tabs-button-color-text-active:var(--wb-color-text-primary);--wb-tabs-button-color-text-disabled:var(--wb-color-text-disabled);--wb-tabs-button-primary-color-bg:transparent;--wb-tabs-button-primary-color-bg-hover:transparent;--wb-tabs-button-primary-color-bg-active:var(--wb-bg-hover-light);--wb-tabs-button-primary-color-bg-disabled:transparent;--wb-tabs-button-primary-color-text:var(--wb-color-text-secondary);--wb-tabs-button-primary-color-text-hover:var(--wb-color-text-primary);--wb-tabs-button-primary-color-text-active:var(--wb-text-on-primary);--wb-tabs-button-primary-color-text-disabled:var(--wb-color-text-disabled);--wb-tabs-inkbar-color-bg:var(--wb-color-text-solid);--wb-tabs-arrow-btn-color-bg:var(--wb-bg-secondary);--wb-tabs-arrow-btn-color-text:var(--wb-color-text-tertiary);--wb-tabs-arrow-btn-color-text-hover:var(--wb-color-text-solid);--wb-tabs-arrow-btn-color-mask-left:linear-gradient(to right, var(--wb-bg-primary) 50%, transparent 100%);--wb-tabs-arrow-btn-color-mask-right:linear-gradient(to right, transparent 0%, var(--wb-bg-primary) 50%);--wb-tabs-arrow-btn-color-shadow:var(--wb-shadow-md);--wb-tabs-vertical-divider-color-bg:var(--wb-color-border-secondary)}
|
||||
.yc-capability-center{--wb-tab-active-border:var(--wb-palette-purple-2);--wb-tab-active-fg:var(--wb-palette-purple-5);--wb-tab-gradient-light:linear-gradient(90deg, #f5f0ff, #fef8e8);--wb-bg-tab-active:#f1f3f6}
|
||||
.yc-capability-center .expert-center-dark{--wb-tab-active-border:var(--wb-palette-purple-3);--wb-tab-active-fg:var(--wb-palette-purple-6);--wb-tab-gradient-light:linear-gradient(90deg, #2c243d, #3a3527);--wb-bg-tab-active:#25282c}
|
||||
.yc-capability-center{--wb-tooltip-padding-TB:var(--wb-spacing-2);--wb-tooltip-padding-LR:var(--wb-spacing-3);--wb-tooltip-font-size:var(--wb-font-size-3);--wb-tooltip-line-height:var(--wb-font-line-height-5);--wb-tooltip-max-width:320px;--wb-tooltip-arrow-width:10px;--wb-tooltip-arrow-height:3px;--wb-tooltip-border-radius:var(--wb-radius-lg);--wb-tooltip-duration-in:var(--wb-motion-duration-fast);--wb-tooltip-duration-out:var(--wb-motion-duration-fast);--wb-tooltip-easing:var(--wb-motion-easing-linear);--wb-tooltip-backdrop-blur:var(--wb-spacing-2);--wb-tooltip-z-index:10200}
|
||||
.yc-capability-center{--wb-tooltip-bg:var(--wb-palette-black-75);--wb-tooltip-text-color:var(--wb-palette-white-100);--wb-bg-tooltip:var(--wb-palette-black-90);--wb-bg-tooltip-hover:var(--wb-palette-black-75);--wb-border-tooltip:#d1d5db;--wb-shadow-tooltip:0 4px 12px #00000029}
|
||||
.yc-capability-center .expert-center-dark{--wb-tooltip-bg:var(--wb-palette-white-75);--wb-tooltip-text-color:var(--wb-palette-black-100);--wb-bg-tooltip:var(--wb-palette-black-90);--wb-bg-tooltip-hover:var(--wb-palette-black-75);--wb-border-tooltip:#ffffff1a;--wb-shadow-tooltip:0 4px 12px #00000052}
|
||||
.yc-capability-center{--wb-dropdown-item-font-size:var(--wb-font-body-size);--wb-dropdown-item-line-height:var(--wb-font-body-line-height);--wb-dropdown-item-min-height:var(--wb-size-control-md);--wb-dropdown-item-icon-size:var(--wb-icon-size-md);--wb-dropdown-item-radius:var(--wb-radius-lg);--wb-dropdown-item-padding-y:var(--wb-spacing-2-5);--wb-dropdown-item-gap:var(--wb-spacing-1);--wb-dropdown-item-empty-padding:var(--wb-spacing-4);--wb-dropdown-item-transition-duration:var(--wb-motion-duration-instant);--wb-dropdown-item-transition-easing:var(--wb-motion-easing-standard);--wb-dropdown-item-icon-color:var(--wb-icon-secondary);--wb-dropdown-item-check-color:var(--wb-color-text-brand-default);--wb-dropdown-item-text-color:var(--wb-color-text-primary);--wb-dropdown-item-text-color-disabled:var(--wb-color-text-disabled);--wb-dropdown-item-text-color-danger:var(--wb-menu-item-danger-fg);--wb-dropdown-highlight-color:var(--wb-brand-primary);--wb-dropdown-search-prefix-color:var(--wb-color-text-secondary);--wb-dropdown-search-clear-color:var(--wb-color-text-tertiary);--wb-dropdown-search-input-bg:var(--wb-bg-secondary);--wb-dropdown-divider-margin-y:6px;--wb-dropdown-divider-color:var(--wb-border-subtle);--wb-dropdown-spacing-3:var(--wb-spacing-3)}
|
||||
.yc-capability-center{--wb-dropdown-item-bg-hover:var(--wb-color-palette-gray-3,#f2f2f2);--wb-dropdown-item-bg-selected:var(--wb-palette-gray-2,#f7f7f7);--wb-dropdown-scrollbar-thumb-color:var(--wb-palette-gray-4);--wb-dropdown-card-shadow:var(--wb-shadow-dialog)}
|
||||
.yc-capability-center .expert-center-dark{--wb-dropdown-item-bg-hover:#2e2e2e;--wb-dropdown-item-bg-selected:var(--wb-palette-gray-2,#292929);--wb-dropdown-scrollbar-thumb-color:var(--wb-palette-white-10);--wb-dropdown-card-shadow:var(--wb-shadow-dialog)}
|
||||
.yc-capability-center{--wb-dropdown-bg-color:var(--wb-bg-popover);--wb-dropdown-item-hover-bg-color:var(--wb-bg-hover-light);--wb-dropdown-border-color:var(--wb-border-default);--wb-shadow-dropdown:0px 6px 24px 0px #0000000a, 0px 4px 6px 0px #0000000a}
|
||||
.yc-capability-center .expert-center-dark{--wb-dropdown-bg-color:var(--wb-bg-primary);--wb-dropdown-item-hover-bg-color:var(--wb-bg-hover-light);--wb-dropdown-bg-color-fc:#2d2d2d;--wb-dropdown-border-color:#ffffff1a;--wb-shadow-dropdown:0 4px 6px -1px #0000004d, 0 2px 4px -1px #0003, 0 12px 28px #00000080}
|
||||
.yc-capability-center{--wb-tag-height-small:var(--wb-size-5);--wb-tag-height-medium:var(--wb-size-control-sm);--wb-tag-height-large:var(--wb-size-control-md);--wb-tag-padding-LR-small:var(--wb-spacing-3);--wb-tag-padding-LR-medium:var(--wb-spacing-3);--wb-tag-padding-LR-large:var(--wb-spacing-4);--wb-tag-gap:var(--wb-spacing-2);--wb-tag-font-size-small:11px;--wb-tag-font-size-medium:var(--wb-font-size-3);--wb-tag-font-size-large:var(--wb-font-size-5);--wb-tag-line-height-small:var(--wb-font-line-height-4-2);--wb-tag-line-height-medium:var(--wb-font-line-height-5);--wb-tag-line-height-large:var(--wb-font-line-height-5-2);--wb-tag-font-weight:400;--wb-tag-font-weight-500:500;--wb-tag-border-radius-full:var(--wb-radius-full);--wb-tag-border-radius-square-small:var(--wb-radius-sm);--wb-tag-border-radius-square-medium:var(--wb-radius-md);--wb-tag-border-radius-square-large:var(--wb-radius-lg);--wb-tag-dot-size:4px;--wb-tag-dot-border-radius:var(--wb-radius-full);--wb-tag-icon-size:14px;--wb-tag-icon-size-large:var(--wb-icon-size-md);--wb-tag-close-size-small:var(--wb-icon-size-sm);--wb-tag-close-size-medium:14px;--wb-tag-close-size-large:var(--wb-icon-size-md);--wb-tag-close-margin-right:-2px;--wb-tag-checked-ring-width:1px;--wb-tag-max-reached-opacity:.5;--wb-tag-focus-outline-width:2px;--wb-tag-focus-outline-offset:1px;--wb-tag-input-min-width:24px;--wb-tag-group-gap:var(--wb-spacing-3);--wb-tag-motion-duration:var(--wb-motion-duration-instant);--wb-tag-motion-easing:var(--wb-motion-easing-standard)}
|
||||
.yc-capability-center{--wb-tag-default-bg:#0000000a;--wb-tag-default-color-text:var(--wb-color-text-secondary);--wb-tag-default-color-text-hover:var(--wb-palette-black-75);--wb-tag-success-bg:var(--wb-palette-green-1);--wb-tag-success-color-text:var(--wb-palette-green-9);--wb-tag-success-color-text-hover:var(--wb-palette-green-9);--wb-tag-warning-bg:var(--wb-palette-orange-1);--wb-tag-warning-color-text:var(--wb-palette-orange-9);--wb-tag-warning-color-text-hover:var(--wb-palette-orange-9);--wb-tag-error-bg:var(--wb-palette-red-1);--wb-tag-error-color-text:var(--wb-palette-red-9);--wb-tag-error-color-text-hover:var(--wb-palette-red-9);--wb-tag-info-bg:var(--wb-palette-brand-1);--wb-tag-info-color-text:var(--wb-palette-brand-9);--wb-tag-info-color-text-hover:var(--wb-palette-brand-9);--wb-tag-default-bg-hover:var(--wb-palette-gray-4);--wb-tag-success-bg-hover:var(--wb-palette-green-2);--wb-tag-warning-bg-hover:var(--wb-palette-orange-2);--wb-tag-error-bg-hover:var(--wb-palette-red-2);--wb-tag-info-bg-hover:var(--wb-palette-brand-2);--wb-tag-default-disabled-bg:#0000000a;--wb-tag-default-disabled-color-text:var(--wb-color-text-disabled);--wb-tag-success-disabled-bg:var(--wb-palette-green-1);--wb-tag-success-disabled-color-text:var(--wb-palette-green-3);--wb-tag-warning-disabled-bg:var(--wb-palette-orange-1);--wb-tag-warning-disabled-color-text:var(--wb-palette-orange-3);--wb-tag-error-disabled-bg:var(--wb-palette-red-1);--wb-tag-error-disabled-color-text:var(--wb-palette-red-3);--wb-tag-info-disabled-bg:var(--wb-palette-brand-1);--wb-tag-info-disabled-color-text:var(--wb-palette-brand-3);--wb-tag-color-outline-focus:var(--wb-border-focus);--wb-tag-input-placeholder-color:var(--wb-color-text-disabled);--wb-tag-default-checked-bg:var(--wb-tag-default-bg-hover);--wb-tag-default-checked-bg-hover:var(--wb-tag-default-bg-hover);--wb-tag-default-checked-color-text:var(--wb-tag-default-color-text-hover);--wb-tag-default-checked-ring-color:currentColor;--wb-tag-success-checked-bg:var(--wb-tag-success-bg-hover);--wb-tag-success-checked-bg-hover:var(--wb-tag-success-bg-hover);--wb-tag-success-checked-color-text:var(--wb-tag-success-color-text);--wb-tag-success-checked-ring-color:currentColor;--wb-tag-warning-checked-bg:var(--wb-tag-warning-bg-hover);--wb-tag-warning-checked-bg-hover:var(--wb-tag-warning-bg-hover);--wb-tag-warning-checked-color-text:var(--wb-tag-warning-color-text);--wb-tag-warning-checked-ring-color:currentColor;--wb-tag-error-checked-bg:var(--wb-tag-error-bg-hover);--wb-tag-error-checked-bg-hover:var(--wb-tag-error-bg-hover);--wb-tag-error-checked-color-text:var(--wb-tag-error-color-text);--wb-tag-error-checked-ring-color:currentColor;--wb-tag-info-checked-bg:var(--wb-tag-info-bg-hover);--wb-tag-info-checked-bg-hover:var(--wb-tag-info-bg-hover);--wb-tag-info-checked-color-text:var(--wb-tag-info-color-text);--wb-tag-info-checked-ring-color:currentColor}
|
||||
.yc-capability-center .expert-center-dark{--wb-tag-default-bg:#ffffff0a;--wb-tag-default-color-text:var(--wb-color-text-secondary);--wb-tag-default-color-text-hover:var(--wb-palette-white-75);--wb-tag-success-bg:var(--wb-palette-green-1);--wb-tag-success-color-text:var(--wb-palette-green-9);--wb-tag-success-color-text-hover:var(--wb-palette-green-9);--wb-tag-warning-bg:var(--wb-palette-orange-1);--wb-tag-warning-color-text:var(--wb-palette-orange-9);--wb-tag-warning-color-text-hover:var(--wb-palette-orange-9);--wb-tag-error-bg:var(--wb-palette-red-1);--wb-tag-error-color-text:var(--wb-palette-red-9);--wb-tag-error-color-text-hover:var(--wb-palette-red-9);--wb-tag-info-bg:var(--wb-palette-brand-1);--wb-tag-info-color-text:var(--wb-palette-brand-9);--wb-tag-info-color-text-hover:var(--wb-palette-brand-9);--wb-tag-default-bg-hover:var(--wb-palette-gray-4);--wb-tag-success-bg-hover:var(--wb-palette-green-2);--wb-tag-warning-bg-hover:var(--wb-palette-orange-2);--wb-tag-error-bg-hover:var(--wb-palette-red-2);--wb-tag-info-bg-hover:var(--wb-palette-brand-2);--wb-tag-default-disabled-bg:#ffffff0a;--wb-tag-default-disabled-color-text:var(--wb-color-text-disabled);--wb-tag-success-disabled-bg:var(--wb-palette-green-1);--wb-tag-success-disabled-color-text:var(--wb-palette-green-3);--wb-tag-warning-disabled-bg:var(--wb-palette-orange-1);--wb-tag-warning-disabled-color-text:var(--wb-palette-orange-5);--wb-tag-error-disabled-bg:var(--wb-palette-red-1);--wb-tag-error-disabled-color-text:var(--wb-palette-red-5);--wb-tag-info-disabled-bg:var(--wb-palette-brand-1);--wb-tag-info-disabled-color-text:var(--wb-palette-brand-3)}
|
||||
.yc-capability-center{--wb-upload-gap:var(--wb-spacing-3);--wb-upload-max-width:432px;--wb-upload-drop-zone-border-width:1px;--wb-upload-drop-zone-border-radius:var(--wb-radius-xl);--wb-upload-drop-zone-padding-y:var(--wb-spacing-7);--wb-upload-drop-zone-padding-x:var(--wb-spacing-5);--wb-upload-drop-zone-gap:var(--wb-spacing-3);--wb-upload-drop-zone-min-height:132px;--wb-upload-drop-zone-title-font-size:var(--wb-font-size-3);--wb-upload-drop-zone-title-font-weight:500;--wb-upload-drop-zone-title-line-height:var(--wb-font-line-height-5);--wb-upload-drop-zone-icon-size:var(--wb-icon-size-xl);--wb-upload-spinner-duration:1s;--wb-upload-motion-duration:var(--wb-motion-duration-instant);--wb-upload-motion-easing:var(--wb-motion-easing-standard)}
|
||||
.yc-capability-center{--wb-upload-drop-zone-color-border:var(--wb-palette-black-30);--wb-upload-drop-zone-color-border-hover:var(--wb-palette-black-60);--wb-upload-drop-zone-color-border-disabled:var(--wb-palette-black-20);--wb-upload-drop-zone-color-bg:var(--wb-palette-gray-1);--wb-upload-drop-zone-color-bg-dragging:var(--wb-palette-gray-2);--wb-upload-drop-zone-color-text:var(--wb-color-text-hint);--wb-upload-drop-zone-color-text-disabled:var(--wb-palette-black-20);--wb-upload-error-color:var(--wb-color-text-error-default);--wb-upload-spinner-color:var(--wb-brand-primary);--wb-upload-scanning-color:var(--wb-color-text-brand-default)}
|
||||
.yc-capability-center .expert-center-dark{--wb-upload-drop-zone-color-border:var(--wb-palette-white-30);--wb-upload-drop-zone-color-border-hover:var(--wb-palette-white-60);--wb-upload-drop-zone-color-border-disabled:var(--wb-palette-white-20);--wb-upload-drop-zone-color-bg:var(--wb-palette-gray-1);--wb-upload-drop-zone-color-bg-dragging:var(--wb-palette-gray-2);--wb-upload-drop-zone-color-text:var(--wb-color-text-hint);--wb-upload-drop-zone-color-text-disabled:var(--wb-palette-white-20);--wb-upload-error-color:var(--wb-color-text-error-default);--wb-upload-spinner-color:var(--wb-brand-primary);--wb-upload-scanning-color:var(--wb-color-text-brand-default)}
|
||||
.yc-capability-center{--wb-table-border-radius:var(--wb-radius-2xl);--wb-table-font-size:var(--wb-font-size-3);--wb-table-line-height:var(--wb-font-line-height-5);--wb-table-font-weight:var(--wb-font-weight-500);--wb-table-compact-font-size:var(--wb-font-caption-size);--wb-table-compact-line-height:var(--wb-font-caption-line-height);--wb-table-pad-y:9px;--wb-table-pad-y-compact:7px;--wb-table-padding-x:var(--wb-spacing-4);--wb-table-padding-x-compact:var(--wb-spacing-3);--wb-table-inset:0px;--wb-table-selection-width:48px;--wb-table-selection-width-compact:40px;--wb-table-selection-padding-x:var(--wb-spacing-4);--wb-table-selection-padding-x-compact:var(--wb-spacing-3);--wb-table-selection-padding-right:var(--wb-spacing-2);--wb-table-sort-indicator-size:16px;--wb-table-sort-indicator-margin:2px;--wb-table-sort-gap:var(--wb-spacing-2);--wb-table-empty-padding-y:var(--wb-spacing-7);--wb-table-empty-padding-x:var(--wb-spacing-4);--wb-table-loading-size:24px;--wb-table-loading-border-width:2px;--wb-table-loading-spin-duration:.8s;--wb-table-fixed-shadow-width:16px;--wb-table-transition-duration:var(--wb-motion-duration-instant);--wb-table-transition-easing:var(--wb-motion-easing-standard);--wb-table-row-motion-duration:var(--wb-motion-duration-base);--wb-table-z-fixed-td:2;--wb-table-z-sticky-th:3;--wb-table-z-fixed-th:4;--wb-table-loading-radius:var(--wb-radius-full)}
|
||||
.yc-capability-center{--wb-table-bg:var(--wb-bg-primary);--wb-table-color-text:var(--wb-color-text-primary);--wb-table-border:1px solid var(--wb-border-default);--wb-table-header-bg:var(--wb-bg-secondary);--wb-table-header-color:var(--wb-palette-black-50);--wb-table-divider-color:var(--wb-palette-gray-4);--wb-table-hover-bg:var(--wb-bg-hover-light);--wb-table-striped-bg:var(--wb-bg-secondary);--wb-table-striped-hover-bg:var(--wb-bg-tertiary);--wb-table-sort-hover-bg:var(--wb-bg-tertiary);--wb-table-selected-bg:color-mix(in srgb, var(--wb-brand-primary) 12%, transparent);--wb-table-selected-hover-bg:color-mix(in srgb, var(--wb-brand-primary) 18%, transparent);--wb-table-empty-color:var(--wb-color-text-tertiary);--wb-table-loading-overlay-bg:color-mix(in srgb, var(--wb-bg-secondary) 60%, transparent);--wb-table-loading-border-color:var(--wb-border-default);--wb-table-loading-spinner-color:var(--wb-color-bg-primary-default);--wb-table-fixed-td-bg:var(--wb-bg-primary);--wb-table-fixed-td-hover-bg:var(--wb-bg-hover-light);--wb-table-fixed-td-striped-bg:var(--wb-bg-secondary);--wb-table-fixed-th-bg:var(--wb-bg-secondary);--wb-table-fixed-shadow-color:#0000000f;--wb-table-sort-indicator-color:var(--wb-color-text-tertiary);--wb-table-sort-indicator-active-color:var(--wb-color-text-primary);--wb-table-sorted-color:var(--wb-color-text-primary)}
|
||||
.yc-capability-center .expert-center-dark{--wb-table-bg:var(--wb-bg-primary);--wb-table-color-text:var(--wb-color-text-primary);--wb-table-border:1px solid var(--wb-border-default);--wb-table-header-bg:var(--wb-bg-secondary);--wb-table-header-color:var(--wb-palette-white-50);--wb-table-divider-color:var(--wb-palette-gray-4);--wb-table-hover-bg:var(--wb-bg-hover-light);--wb-table-striped-bg:var(--wb-bg-secondary);--wb-table-striped-hover-bg:var(--wb-bg-tertiary);--wb-table-sort-hover-bg:var(--wb-bg-tertiary);--wb-table-selected-bg:color-mix(in srgb, var(--wb-brand-primary) 12%, transparent);--wb-table-selected-hover-bg:color-mix(in srgb, var(--wb-brand-primary) 18%, transparent);--wb-table-empty-color:var(--wb-color-text-tertiary);--wb-table-loading-overlay-bg:color-mix(in srgb, var(--wb-bg-secondary) 60%, transparent);--wb-table-loading-border-color:var(--wb-border-default);--wb-table-loading-spinner-color:var(--wb-color-bg-primary-default);--wb-table-fixed-td-bg:var(--wb-bg-primary);--wb-table-fixed-td-hover-bg:var(--wb-bg-hover-light);--wb-table-fixed-td-striped-bg:var(--wb-bg-secondary);--wb-table-fixed-th-bg:var(--wb-bg-secondary);--wb-table-fixed-shadow-color:#ffffff0f;--wb-table-sort-indicator-color:var(--wb-color-text-tertiary);--wb-table-sort-indicator-active-color:var(--wb-color-text-primary);--wb-table-sorted-color:var(--wb-color-text-primary)}
|
||||
.yc-capability-center{--wb-select-spacing-1:var(--wb-spacing-1);--wb-select-spacing-2:var(--wb-spacing-2);--wb-select-spacing-3:var(--wb-spacing-3);--wb-select-spacing-4:var(--wb-spacing-4);--wb-select-padding-x-small:6px;--wb-select-padding-x-multiple:var(--wb-spacing-2-5);--wb-select-transition-duration:var(--wb-motion-duration-instant);--wb-select-transition-easing:var(--wb-motion-easing-standard);--wb-select-radius-md:var(--wb-radius-md);--wb-select-radius-lg:var(--wb-radius-lg);--wb-select-radius-xl:var(--wb-radius-xl);--wb-select-size-6:var(--wb-size-6);--wb-select-size-8:var(--wb-size-8);--wb-select-size-10:var(--wb-size-10);--wb-select-size-control-md:var(--wb-size-control-md);--wb-select-icon-size-md:var(--wb-icon-size-md);--wb-select-font-size:var(--wb-font-body-size);--wb-select-line-height:var(--wb-font-body-line-height);--wb-select-font-size-small:var(--wb-font-caption-size);--wb-select-group-label-line-height:var(--wb-font-line-height-5);--wb-select-option-font-weight-selected:var(--wb-font-weight-500);--wb-select-text-color:var(--wb-color-text-primary);--wb-select-text-color-secondary:var(--wb-color-text-secondary);--wb-select-text-color-tertiary:var(--wb-color-text-tertiary);--wb-select-text-color-disabled:var(--wb-color-text-disabled);--wb-select-option-check-color:var(--wb-color-text-brand-default);--wb-select-bg:var(--wb-bg-primary);--wb-select-search-bg:var(--wb-bg-secondary);--wb-select-bg-hover:var(--wb-dropdown-item-bg-hover);--wb-select-option-bg-selected:var(--wb-dropdown-item-bg-selected);--wb-select-divider-color:var(--wb-border-subtle);--wb-select-border-color-danger:var(--wb-palette-red-4)}
|
||||
.yc-capability-center{--wb-select-border-color:var(--wb-color-border-primary);--wb-select-border-color-open:var(--wb-palette-black-50);--wb-select-border-color-hover:var(--wb-palette-black-30);--wb-select-scrollbar-thumb:var(--wb-palette-gray-4);--wb-select-option-checkbox-border-color:var(--wb-palette-black-10)}
|
||||
.yc-capability-center .expert-center-dark{--wb-select-border-color:var(--wb-color-border-solid);--wb-select-border-color-open:var(--wb-palette-white-50);--wb-select-border-color-hover:var(--wb-palette-white-30);--wb-select-scrollbar-thumb:var(--wb-palette-white-10);--wb-select-search-bg:#2e2e2e;--wb-select-option-checkbox-border-color:var(--wb-palette-white-20)}
|
||||
.yc-capability-center{--wb-timepicker-spacing-2:var(--wb-spacing-3);--wb-timepicker-panel-padding:0;--wb-timepicker-column-gap:var(--wb-spacing-2);--wb-timepicker-cell-gap:var(--wb-spacing-1);--wb-timepicker-column-height:560px;--wb-timepicker-cell-height:var(--wb-size-control-md);--wb-timepicker-trigger-width:200px;--wb-timepicker-popover-min-width:160px;--wb-timepicker-popover-max-width:320px;--wb-timepicker-panel-max-height:320px;--wb-timepicker-footer-height:var(--wb-size-control-md);--wb-timepicker-footer-btn-min-width:66px;--wb-timepicker-thumb-width:var(--wb-spacing-2);--wb-timepicker-thumb-radius:var(--wb-radius-xl);--wb-timepicker-radius:var(--wb-radius-lg);--wb-timepicker-font-size:var(--wb-font-size-5);--wb-timepicker-line-height:var(--wb-font-line-height-5-2);--wb-timepicker-font-weight-selected:var(--wb-font-weight-500);--wb-timepicker-font-weight-confirm:var(--wb-font-weight-600);--wb-timepicker-transition-duration:var(--wb-motion-duration-instant);--wb-timepicker-transition-easing:var(--wb-motion-easing-standard)}
|
||||
.yc-capability-center{--wb-timepicker-cell-color:var(--wb-palette-black-90);--wb-timepicker-cell-hover-bg:var(--wb-palette-gray-3);--wb-timepicker-cell-selected-bg:var(--wb-palette-gray-3);--wb-timepicker-cell-selected-color:var(--wb-color-text-primary);--wb-timepicker-cell-disabled-color:var(--wb-color-text-disabled);--wb-timepicker-border-color:var(--wb-palette-gray-3);--wb-timepicker-icon-color:var(--wb-color-text-tertiary);--wb-timepicker-now-btn-color:var(--wb-color-text-brand-default);--wb-timepicker-now-btn-bg:var(--wb-palette-gray-3);--wb-timepicker-scrollbar-color:var(--wb-palette-black-10)}
|
||||
.yc-capability-center .expert-center-dark{--wb-timepicker-cell-color:var(--wb-palette-white-90);--wb-timepicker-cell-hover-bg:var(--wb-palette-white-10);--wb-timepicker-cell-selected-bg:var(--wb-palette-white-10);--wb-timepicker-cell-selected-color:var(--wb-color-text-primary);--wb-timepicker-cell-disabled-color:var(--wb-color-text-disabled);--wb-timepicker-border-color:var(--wb-palette-white-10);--wb-timepicker-icon-color:var(--wb-color-text-tertiary);--wb-timepicker-now-btn-color:var(--wb-color-text-brand-default);--wb-timepicker-now-btn-bg:var(--wb-palette-white-10);--wb-timepicker-scrollbar-color:var(--wb-palette-white-10)}
|
||||
.yc-capability-center{--wb-datepicker-spacing-1:var(--wb-spacing-2);--wb-datepicker-spacing-2:var(--wb-spacing-3);--wb-datepicker-spacing-4:var(--wb-spacing-5);--wb-datepicker-panel-padding:var(--wb-spacing-5);--wb-datepicker-panel-content-width:250px;--wb-datepicker-range-trigger-width:248px;--wb-datepicker-range-time-trigger-width:380px;--wb-datepicker-trigger-padding-x:var(--wb-spacing-3);--wb-datepicker-header-nav-gap:var(--wb-spacing-1);--wb-datepicker-header-btn-padding-y:var(--wb-spacing-1);--wb-datepicker-header-btn-padding-x:var(--wb-spacing-2);--wb-datepicker-presets-item-width:88px;--wb-datepicker-presets-item-height:var(--wb-size-6);--wb-datepicker-presets-item-padding-y:0;--wb-datepicker-presets-item-padding-x:var(--wb-spacing-3);--wb-datepicker-presets-item-radius:var(--wb-radius-sm);--wb-datepicker-btn-padding-x:var(--wb-spacing-4);--wb-datepicker-time-section-gap:var(--wb-spacing-5);--wb-datepicker-time-divider-top-offset:13px;--wb-datepicker-time-divider-bottom-offset:var(--wb-spacing-3);--wb-datepicker-confirm-footer-offset:7px;--wb-datepicker-cell-size:var(--wb-size-8);--wb-datepicker-cell-inner-size:var(--wb-size-6);--wb-datepicker-week-selection-width:239px;--wb-datepicker-icon-size:var(--wb-icon-size-md);--wb-datepicker-month-cell-width:var(--wb-size-14);--wb-datepicker-option-row-height:var(--wb-size-8);--wb-datepicker-option-row-gap:var(--wb-spacing-4);--wb-datepicker-month-cell-height:var(--wb-size-6);--wb-datepicker-quarter-cell-width:var(--wb-size-12);--wb-datepicker-presets-width:88px;--wb-datepicker-time-section-width:128px;--wb-datepicker-showtime-cell-height:26px;--wb-datepicker-confirm-btn-height:var(--wb-size-8);--wb-datepicker-confirm-btn-min-width:66px;--wb-datepicker-radius:var(--wb-radius-md);--wb-datepicker-radius-hover:var(--wb-radius-lg);--wb-datepicker-trigger-radius:var(--wb-radius-lg);--wb-datepicker-font-size:var(--wb-font-size-5);--wb-datepicker-weekday-font-size:var(--wb-font-size-5);--wb-datepicker-transition-duration:var(--wb-motion-duration-instant);--wb-datepicker-transition-easing:var(--wb-motion-easing-standard)}
|
||||
.yc-capability-center{--wb-datepicker-cell-color:var(--wb-palette-black-90,#1a1a1a);--wb-datepicker-cell-other-month-color:var(--wb-palette-black-30,#b3b3b3);--wb-datepicker-cell-hover-bg:var(--wb-bg-hover);--wb-datepicker-cell-selected-bg:var(--wb-control-selected-bg);--wb-datepicker-cell-selected-color:var(--wb-text-on-primary);--wb-datepicker-week-selected-bg:var(--wb-palette-black-75);--wb-datepicker-cell-disabled-bg:var(--wb-color-bg-primary-default);--wb-datepicker-cell-disabled-color:var(--wb-color-text-disabled);--wb-datepicker-today-border-color:var(--wb-palette-black-50,gray);--wb-datepicker-today-btn-color:var(--wb-color-text-primary);--wb-datepicker-range-bg:var(--wb-bg-hover);--wb-datepicker-preview-bg:var(--wb-bg-hover-light);--wb-datepicker-header-title-color:var(--wb-palette-black-90,#1a1a1a);--wb-datepicker-header-nav-color:var(--wb-palette-black-50,gray);--wb-datepicker-header-nav-hover-bg:var(--wb-palette-black-5,#f2f2f2);--wb-datepicker-weekday-color:var(--wb-palette-black-50,gray);--wb-datepicker-border-color:var(--wb-palette-black-10,#e5e5e5);--wb-datepicker-icon-color:var(--wb-color-text-tertiary);--wb-datepicker-separator-color:var(--wb-palette-black-30,#b3b3b3)}
|
||||
.yc-capability-center .expert-center-dark{--wb-datepicker-cell-color:var(--wb-palette-white-90,#e5e5e5);--wb-datepicker-cell-other-month-color:var(--wb-palette-white-25,#404040);--wb-datepicker-cell-hover-bg:var(--wb-bg-hover);--wb-datepicker-cell-selected-bg:var(--wb-palette-white-100,#fff);--wb-datepicker-cell-selected-color:var(--wb-palette-black-90,#1a1a1a);--wb-datepicker-week-selected-bg:var(--wb-palette-white-75);--wb-datepicker-cell-disabled-color:var(--wb-palette-white-20,#333);--wb-datepicker-today-border-color:var(--wb-palette-white-50,gray);--wb-datepicker-today-btn-color:var(--wb-color-text-primary);--wb-datepicker-range-bg:var(--wb-bg-hover);--wb-datepicker-preview-bg:var(--wb-bg-hover-light);--wb-datepicker-header-title-color:var(--wb-palette-white-90,#e5e5e5);--wb-datepicker-header-nav-color:var(--wb-palette-white-50,gray);--wb-datepicker-header-nav-hover-bg:var(--wb-palette-white-10,#1a1a1a);--wb-datepicker-weekday-color:var(--wb-palette-white-50,gray);--wb-datepicker-border-color:var(--wb-border-default);--wb-datepicker-icon-color:var(--wb-color-text-tertiary);--wb-datepicker-separator-color:var(--wb-palette-white-30,#4d4d4d)}
|
||||
.yc-capability-center{--wb-segmented-track-padding:var(--wb-spacing-1);--wb-segmented-track-border-radius:var(--wb-radius-lg);--wb-segmented-track-border-radius-round:9999px;--wb-segmented-item-gap:var(--wb-spacing-2);--wb-segmented-item-spacing:var(--wb-spacing-1);--wb-segmented-item-border-radius:var(--wb-radius-md);--wb-segmented-item-border-radius-round:9999px;--wb-segmented-item-height:var(--wb-size-control-md);--wb-segmented-item-height-small:var(--wb-size-control-ms);--wb-segmented-item-height-large:var(--wb-size-control-lg);--wb-segmented-item-padding-y:3px;--wb-segmented-item-padding-y-small:1px;--wb-segmented-item-padding-y-large:5px;--wb-segmented-item-padding-x:var(--wb-spacing-4);--wb-segmented-item-padding-x-small:var(--wb-spacing-3);--wb-segmented-item-padding-x-large:var(--wb-spacing-5);--wb-segmented-item-font-size:var(--wb-font-size-5);--wb-segmented-item-font-size-small:var(--wb-font-size-5);--wb-segmented-item-line-height:var(--wb-font-line-height-5-2);--wb-segmented-item-line-height-small:var(--wb-font-line-height-5-2);--wb-segmented-item-font-size-large:var(--wb-font-size-6);--wb-segmented-item-line-height-large:var(--wb-font-line-height-6);--wb-segmented-item-font-weight:400;--wb-segmented-item-font-weight-active:600;--wb-segmented-item-focus-outline-width:2px;--wb-segmented-item-focus-outline-offset:-2px;--wb-segmented-item-icon-size:14px;--wb-segmented-thumb-border-radius:var(--wb-radius-md);--wb-segmented-item-transition-duration:var(--wb-motion-duration-instant);--wb-segmented-item-transition-easing:var(--wb-motion-easing-standard);--wb-segmented-thumb-transition-duration:var(--wb-motion-duration-fast);--wb-segmented-thumb-transition-easing:var(--wb-motion-easing-standard);--wb-segmented-thumb-transition-duration-opacity:var(--wb-motion-duration-fast)}
|
||||
.yc-capability-center{--wb-segmented-track-color-bg:var(--wb-bg-inset-strong);--wb-segmented-thumb-color-bg:var(--wb-bg-primary);--wb-segmented-thumb-color-shadow:0px 1px 4px 0px #0000000d;--wb-segmented-item-color-text:var(--wb-color-text-secondary);--wb-segmented-item-color-text-hover:var(--wb-color-text-primary);--wb-segmented-item-color-text-active:var(--wb-color-text-primary);--wb-segmented-item-color-text-disabled:var(--wb-color-text-disabled);--wb-segmented-item-color-bg-hover:var(--wb-palette-gray-5);--wb-segmented-item-color-outline-focus:var(--wb-border-focus)}
|
||||
.yc-capability-center .expert-center-dark{--wb-segmented-track-color-bg:var(--wb-bg-inset-strong);--wb-segmented-thumb-color-bg:var(--wb-bg-elevated);--wb-segmented-thumb-color-shadow:0px 1px 4px 0px #0000000d;--wb-segmented-item-color-text:var(--wb-color-text-secondary);--wb-segmented-item-color-text-hover:var(--wb-color-text-primary);--wb-segmented-item-color-text-active:var(--wb-color-text-primary);--wb-segmented-item-color-text-disabled:var(--wb-color-text-disabled);--wb-segmented-item-color-bg-hover:var(--wb-bg-hover);--wb-segmented-item-color-outline-focus:var(--wb-border-focus);--wb-segmented-item-count-color-text:var(--wb-color-text-tertiary);--wb-segmented-item-count-color-text-active:var(--wb-color-text-secondary)}
|
||||
.yc-capability-center{--wb-pagination-control-size:var(--wb-size-5);--wb-pagination-icon-size:var(--wb-icon-size-md);--wb-pagination-min-width:var(--wb-size-5);--wb-pagination-font-size:var(--wb-font-size-3);--wb-pagination-total-font-size:var(--wb-font-size-5);--wb-pagination-font-weight:var(--wb-font-weight-600);--wb-pagination-total-font-weight:var(--wb-font-weight-400);--wb-pagination-indicator-font-weight:var(--wb-font-weight-400);--wb-pagination-line-height:var(--wb-font-line-height-5);--wb-pagination-total-line-height:var(--wb-font-line-height-5);--wb-pagination-gap:var(--wb-spacing-4);--wb-pagination-control-gap:var(--wb-spacing-1);--wb-pagination-indicator-gap:var(--wb-spacing-1);--wb-pagination-padding-y:var(--wb-spacing-2-5);--wb-pagination-control-padding-x:var(--wb-spacing-1);--wb-pagination-border-radius:var(--wb-radius-sm);--wb-pagination-transition-duration:var(--wb-motion-duration-instant);--wb-pagination-transition-easing:var(--wb-motion-easing-standard)}
|
||||
.yc-capability-center{--wb-pagination-total-text-color:var(--wb-color-text-tertiary);--wb-pagination-current-text-color:var(--wb-color-text-primary);--wb-pagination-indicator-text-color:var(--wb-color-text-tertiary);--wb-pagination-icon-color:var(--wb-icon-default);--wb-pagination-control-bg:transparent;--wb-pagination-control-bg-hover:var(--wb-bg-hover);--wb-pagination-current-border-color:transparent;--wb-pagination-current-border-color-focus:var(--wb-brand-primary);--wb-pagination-disabled-color:var(--wb-color-text-disabled)}
|
||||
.yc-capability-center{--wb-edge-pager-arrow-size:var(--wb-size-5);--wb-edge-pager-icon-size:var(--wb-icon-size-ms);--wb-edge-pager-arrow-radius:var(--wb-radius-full);--wb-edge-pager-arrow-hit-slop:var(--wb-spacing-3);--wb-edge-pager-arrow-inset:0px;--wb-edge-pager-fade-size:8%;--wb-edge-pager-transition-duration:var(--wb-motion-duration-instant);--wb-edge-pager-transition-easing:var(--wb-motion-easing-standard)}
|
||||
.yc-capability-center{--wb-edge-pager-arrow-color:var(--wb-icon-default);--wb-edge-pager-arrow-bg:var(--wb-bg-primary);--wb-edge-pager-arrow-bg-hover:var(--wb-color-bg-primary-hover);--wb-edge-pager-arrow-bg-active:var(--wb-bg-secondary);--wb-edge-pager-arrow-border-color:var(--wb-color-border-secondary);--wb-edge-pager-arrow-shadow:var(--wb-shadow-sm);--wb-edge-pager-arrow-focus-color:var(--wb-brand-primary)}
|
||||
.yc-capability-center{--wb-drawer-overlay-mask-color:var(--wb-color-mask-color);--wb-drawer-panel-bg:var(--wb-bg-primary);--wb-drawer-panel-text-color:var(--wb-color-text-primary);--wb-drawer-width-default:378px;--wb-drawer-width-large:736px;--wb-drawer-height-default:378px;--wb-drawer-height-large:736px;--wb-drawer-header-gap:var(--wb-spacing-4);--wb-drawer-header-padding-vertical:var(--wb-spacing-4);--wb-drawer-header-padding-horizontal:var(--wb-spacing-5);--wb-drawer-header-border-color:var(--wb-border-subtle);--wb-drawer-title-font-size:var(--wb-font-h3-size);--wb-drawer-title-line-height:var(--wb-font-h3-line-height);--wb-drawer-title-font-weight:var(--wb-font-h3-weight);--wb-drawer-title-text-color:var(--wb-color-text-primary);--wb-drawer-header-extra-gap:var(--wb-spacing-2);--wb-drawer-close-size:32px;--wb-drawer-close-border-radius:100px;--wb-drawer-close-font-size:18px;--wb-drawer-close-text-color:var(--wb-color-text-secondary);--wb-drawer-close-text-color-hover:var(--wb-color-text-primary);--wb-drawer-close-bg:transparent;--wb-drawer-close-bg-hover:var(--wb-bg-hover);--wb-drawer-content-padding:var(--wb-spacing-5);--wb-drawer-content-font-size:var(--wb-font-body-size);--wb-drawer-content-line-height:var(--wb-font-body-line-height);--wb-drawer-footer-padding-vertical:var(--wb-spacing-4);--wb-drawer-footer-padding-horizontal:var(--wb-spacing-5);--wb-drawer-footer-border-color:var(--wb-border-default);--wb-drawer-footer-gap:var(--wb-spacing-3);--wb-drawer-panel-duration-enter:.28s;--wb-drawer-panel-duration-exit:.22s;--wb-drawer-panel-easing-enter:cubic-bezier(.2, 0, 0, 1);--wb-drawer-panel-easing-exit:cubic-bezier(.4, 0, .2, 1);--wb-drawer-mask-duration-enter:.2s;--wb-drawer-mask-duration-exit:.15s;--wb-drawer-mask-easing:var(--wb-motion-easing-standard);--wb-drawer-motion-duration-fast:var(--wb-motion-duration-fast);--wb-drawer-motion-easing-standard:var(--wb-motion-easing-standard)}
|
||||
.yc-capability-center{--wb-drawer-panel-bg:var(--wb-bg-primary);--wb-drawer-panel-shadow:var(--wb-shadow-lg);--wb-drawer-overlay-mask-color:var(--wb-color-mask-color)}
|
||||
.yc-capability-center{--wb-notification-width:360px;--wb-notification-padding:var(--wb-spacing-6);--wb-notification-border-radius:var(--wb-radius-4xl);--wb-notification-border-width:.5px;--wb-notification-gap:var(--wb-spacing-3);--wb-notification-header-gap:var(--wb-spacing-2);--wb-notification-title-font-size:var(--wb-font-spec-h6-size);--wb-notification-title-line-height:var(--wb-font-spec-h6-line-height);--wb-notification-title-font-weight:var(--wb-font-weight-600);--wb-notification-close-size:var(--wb-icon-size-xl);--wb-notification-close-icon-size:var(--wb-icon-size-md);--wb-notification-close-border-radius:var(--wb-radius-full);--wb-notification-content-font-size:var(--wb-font-spec-body-md-size);--wb-notification-content-line-height:var(--wb-font-spec-body-md-line-height);--wb-notification-content-font-weight:var(--wb-font-weight-400);--wb-notification-content-line-clamp:2;--wb-notification-scrollbar-width:6px;--wb-notification-scrollbar-thumb-border-radius:var(--wb-radius-full);--wb-notification-toggle-font-size:var(--wb-font-spec-body-md-size);--wb-notification-toggle-line-height:var(--wb-font-spec-body-md-line-height);--wb-notification-toggle-font-weight:var(--wb-font-weight-400);--wb-notification-toggle-icon-gap:var(--wb-spacing-1);--wb-notification-footer-gap:var(--wb-spacing-2);--wb-notification-footer-margin-top:var(--wb-spacing-3);--wb-notification-actions-gap:var(--wb-spacing-4);--wb-notification-motion-duration:var(--wb-motion-duration-instant);--wb-notification-motion-easing:var(--wb-motion-easing-standard)}
|
||||
.yc-capability-center{--wb-notification-color-bg:var(--wb-bg-primary);--wb-notification-color-border:var(--wb-color-border-solid);--wb-notification-shadow:var(--wb-shadow-dialog);--wb-notification-title-color-text:var(--wb-color-text-solid);--wb-notification-close-color-text:var(--wb-button-ghost-fg);--wb-notification-close-color-text-hover:var(--wb-button-ghost-fg);--wb-notification-close-color-bg-hover:var(--wb-button-ghost-bg-hover);--wb-notification-content-color-text:var(--wb-color-text-tertiary);--wb-notification-scrollbar-thumb-color-bg:var(--wb-border-default);--wb-notification-scrollbar-track-color-bg:transparent;--wb-notification-toggle-color-text:var(--wb-color-text-tertiary);--wb-notification-toggle-color-text-hover:var(--wb-color-text-primary)}
|
||||
.yc-capability-center{--wb-radio-size-s:var(--wb-size-3-2);--wb-radio-size-m:var(--wb-size-4);--wb-radio-size-l:18px;--wb-radio-size:var(--wb-radio-size-m);--wb-radio-dot-size-s:6px;--wb-radio-dot-size-m:7px;--wb-radio-dot-size-l:8px;--wb-radio-border-width:1.5px;--wb-radio-focus-outline-width:2px;--wb-radio-focus-outline-offset:2px;--wb-radio-inner-gap:var(--wb-spacing-3);--wb-radio-group-gap:var(--wb-spacing-3);--wb-radio-font-size:var(--wb-font-size-5);--wb-radio-line-height:var(--wb-font-body-line-height);--wb-radio-motion-duration:var(--wb-motion-duration-instant);--wb-radio-motion-easing:var(--wb-motion-easing-standard);--wb-radio-check-duration:var(--wb-motion-duration-fast);--wb-radio-check-easing:var(--wb-motion-easing-standard)}
|
||||
.yc-capability-center{--wb-radio-color-label:var(--wb-color-text-primary);--wb-radio-color-label-disabled:var(--wb-color-text-tertiary);--wb-radio-color-bg:var(--wb-palette-white-100);--wb-radio-color-bg-disabled:#ebebeb;--wb-radio-color-border:var(--wb-color-border-solid);--wb-radio-color-border-hover:#0006;--wb-radio-color-border-checked:#3d3d3d;--wb-radio-color-border-disabled:var(--wb-color-border-solid);--wb-radio-color-dot:var(--wb-palette-white-100);--wb-radio-color-dot-disabled:var(--wb-color-text-disabled)}
|
||||
.yc-capability-center .expert-center-dark{--wb-radio-color-label:var(--wb-color-text-primary);--wb-radio-color-label-disabled:var(--wb-color-text-tertiary);--wb-radio-color-bg:transparent;--wb-radio-color-bg-disabled:#3d3d3d;--wb-radio-color-border:#414141;--wb-radio-color-border-hover:#fff6;--wb-radio-color-border-checked:#e0e0e0;--wb-radio-color-border-disabled:#414141;--wb-radio-color-dot:#141414;--wb-radio-color-dot-disabled:#ffffff4d}
|
||||
.yc-capability-center{--wb-form-item-margin-bottom:var(--wb-spacing-6);--wb-form-item-inline-margin-right:var(--wb-spacing-5);--wb-form-label-padding-right:var(--wb-spacing-3);--wb-form-label-vertical-padding-bottom:var(--wb-spacing-3);--wb-form-label-wrap-line-height:var(--wb-font-line-height-6);--wb-form-label-text-height:var(--wb-size-control-md);--wb-form-label-text-font-size:var(--wb-font-size-5);--wb-form-label-text-line-height:var(--wb-font-line-height-6);--wb-form-colon-margin-inline:var(--wb-spacing-1);--wb-form-required-margin-inline:var(--wb-spacing-1);--wb-form-tooltip-icon-margin-inline:var(--wb-spacing-1);--wb-form-tooltip-icon-size:var(--wb-icon-size-md);--wb-form-control-input-min-height:var(--wb-size-control-md);--wb-form-help-font-size:var(--wb-font-size-3);--wb-form-help-line-height:var(--wb-font-line-height-5);--wb-form-extra-font-size:var(--wb-font-size-3);--wb-form-extra-line-height:var(--wb-font-line-height-5)}
|
||||
.yc-capability-center{--wb-form-colon-color:var(--wb-color-text-secondary);--wb-form-tooltip-icon-color:var(--wb-color-text-tertiary);--wb-form-label-text-color:var(--wb-color-text-secondary);--wb-form-required-color:var(--wb-color-text-error-default);--wb-form-help-color:var(--wb-color-text-error-default);--wb-form-extra-color:var(--wb-color-text-tertiary)}
|
||||
.yc-capability-center{--wb-cascader-spacing-1:var(--wb-spacing-1);--wb-cascader-spacing-2:var(--wb-spacing-2);--wb-cascader-spacing-3:var(--wb-spacing-3);--wb-cascader-spacing-4:var(--wb-spacing-4);--wb-cascader-transition-duration:var(--wb-motion-duration-instant);--wb-cascader-transition-easing:var(--wb-motion-easing-standard);--wb-cascader-radius-md:var(--wb-radius-md);--wb-cascader-radius-lg:var(--wb-radius-lg);--wb-cascader-radius-xl:var(--wb-radius-xl);--wb-cascader-size-6:var(--wb-size-6);--wb-cascader-size-8:34px;--wb-cascader-size-10:var(--wb-size-10);--wb-cascader-icon-size-md:var(--wb-icon-size-md);--wb-cascader-font-size:var(--wb-font-body-size);--wb-cascader-line-height:var(--wb-font-body-line-height);--wb-cascader-font-size-small:var(--wb-font-caption-size);--wb-cascader-font-weight-selected:var(--wb-font-weight-500);--wb-cascader-column-min-width:140px;--wb-cascader-column-width:180px;--wb-cascader-panel-max-height:280px;--wb-cascader-text-color:var(--wb-color-text-primary);--wb-cascader-text-color-secondary:var(--wb-color-text-secondary);--wb-cascader-text-color-tertiary:var(--wb-color-text-tertiary);--wb-cascader-text-color-disabled:var(--wb-color-text-disabled);--wb-cascader-bg:var(--wb-bg-primary);--wb-cascader-popover-bg:var(--wb-bg-popover);--wb-cascader-option-check-color:var(--wb-color-text-primary);--wb-cascader-option-bg-hover:var(--wb-dropdown-item-bg-hover);--wb-cascader-option-bg-selected:var(--wb-dropdown-item-bg-selected);--wb-cascader-column-border-color:var(--wb-border-subtle);--wb-cascader-popover-border-color:var(--wb-popover-border-color);--wb-cascader-popover-shadow:var(--wb-popover-shadow);--wb-cascader-border-color-danger:var(--wb-palette-red-4);--wb-cascader-search-bg:var(--wb-bg-secondary)}
|
||||
.yc-capability-center{--wb-cascader-border-color:var(--wb-color-border-primary);--wb-cascader-border-color-hover:var(--wb-palette-black-30);--wb-cascader-border-color-open:var(--wb-palette-black-30);--wb-cascader-search-border-focus:var(--wb-palette-black-30);--wb-cascader-scrollbar-thumb:var(--wb-palette-gray-4);--wb-cascader-scrollbar-thumb-hover:var(--wb-palette-gray-5)}
|
||||
.yc-capability-center .expert-center-dark{--wb-cascader-border-color:var(--wb-color-border-solid);--wb-cascader-border-color-hover:var(--wb-palette-white-30);--wb-cascader-border-color-open:var(--wb-palette-white-30);--wb-cascader-search-border-focus:var(--wb-palette-white-30);--wb-cascader-scrollbar-thumb:var(--wb-palette-white-10);--wb-cascader-scrollbar-thumb-hover:var(--wb-palette-white-20);--wb-cascader-search-bg:#2e2e2e}
|
||||
.yc-capability-center{--wb-color-picker-motion-duration:var(--wb-motion-duration-instant);--wb-color-picker-motion-easing:var(--wb-motion-easing-standard);--wb-card-border-radius:var(--wb-radius-xl);--wb-card-padding-none:0;--wb-card-padding-small:var(--wb-spacing-3);--wb-card-padding-medium:var(--wb-spacing-5);--wb-card-padding-large:var(--wb-spacing-7);--wb-card-border-width:1px;--wb-card-motion-duration:var(--wb-motion-duration-instant);--wb-card-motion-easing:var(--wb-motion-easing-standard);--wb-card-skeleton-gap:var(--wb-spacing-3);--wb-card-skeleton-row-height:12px;--wb-card-skeleton-row-border-radius:var(--wb-radius-sm);--wb-card-skeleton-animation-duration:1.4s;--wb-card-skeleton-animation-easing:ease-in-out;--wb-card-meta-title-font-weight:600;--wb-card-meta-description-font-weight:400;--wb-card-text-success:#19af5b;--wb-card-text-danger:#e14445}
|
||||
.yc-capability-center{--wb-card-bg:var(--wb-bg-primary);--wb-card-outlined-border-color:var(--wb-border-subtle);--wb-card-outlined-box-shadow:var(--wb-shadow-none);--wb-card-flat-border-color:transparent;--wb-card-flat-box-shadow:var(--wb-shadow-none);--wb-card-raised-border-color:var(--wb-border-subtle);--wb-card-raised-box-shadow:0px 16px 32px #0000000a;--wb-card-hoverable-bg-hover:#f0f0f0;--wb-card-outlined-border-color-hover:#dbdbdb;--wb-card-raised-border-color-hover:#dbdbdb;--wb-card-skeleton-row-bg-start:var(--wb-bg-tertiary);--wb-card-skeleton-row-bg-mid:var(--wb-bg-primary);--wb-card-skeleton-row-bg-end:var(--wb-bg-tertiary);--wb-card-meta-title-font-size:var(--wb-font-body-size);--wb-card-meta-title-line-height:var(--wb-font-body-line-height);--wb-card-meta-title-color:var(--wb-color-text-primary);--wb-card-meta-description-font-size:var(--wb-font-caption-size);--wb-card-meta-description-line-height:var(--wb-font-caption-line-height);--wb-card-meta-description-color:var(--wb-color-text-secondary);--wb-card-meta-gap:var(--wb-spacing-3);--wb-card-meta-detail-gap:var(--wb-spacing-1)}
|
||||
.yc-capability-center .expert-center-dark{--wb-card-raised-box-shadow:0px 16px 32px #0006;--wb-card-hoverable-bg-hover:#ffffff14;--wb-card-outlined-border-color-hover:#fff3;--wb-card-raised-border-color-hover:#fff3}
|
||||
.yc-capability-center{--wb-list-item-min-height:57px;--wb-list-item-leading-size:var(--wb-size-4);--wb-list-item-meta-width:64px;--wb-list-item-actions-width:var(--wb-size-6);--wb-list-item-divider-inset-left:var(--wb-spacing-8);--wb-list-item-gap:var(--wb-spacing-3);--wb-list-item-padding-y:var(--wb-spacing-2);--wb-list-item-padding-x:var(--wb-spacing-3);--wb-list-item-caption-gap:var(--wb-spacing-2);--wb-list-item-title-font-size:var(--wb-font-size-5);--wb-list-item-title-line-height:var(--wb-font-line-height-5);--wb-list-item-title-font-weight:var(--wb-font-weight-400);--wb-list-item-caption-font-size:11px;--wb-list-item-caption-line-height:var(--wb-font-line-height-4);--wb-list-item-meta-font-size:var(--wb-font-size-3);--wb-list-item-meta-line-height:var(--wb-font-line-height-4);--wb-list-item-border-radius:var(--wb-radius-lg);--wb-list-item-leading-opacity:.96;--wb-list-item-transition-duration:var(--wb-motion-duration-instant);--wb-list-item-transition-easing:var(--wb-motion-easing-standard)}
|
||||
.yc-capability-center{--wb-list-item-title-color:var(--wb-color-text-primary);--wb-list-item-caption-color:var(--wb-color-text-tertiary);--wb-list-item-meta-color:var(--wb-color-text-tertiary);--wb-list-item-leading-color:var(--wb-color-text-primary);--wb-list-item-bg-hover:color-mix(in srgb, var(--wb-bg-hover) 55%, transparent);--wb-list-item-divider-color:color-mix(in srgb, var(--wb-color-text-primary) 4%, transparent)}
|
||||
.yc-capability-center{--wb-status-dot-size:6px;--wb-status-dot-spinner-box-size:var(--wb-size-4)}
|
||||
.yc-capability-center{--wb-status-dot-color-default:var(--wb-color-text-tertiary);--wb-status-dot-color-success:var(--wb-accent-unread);--wb-status-dot-color-warning:var(--wb-status-warning);--wb-status-dot-color-error:var(--wb-status-error)}
|
||||
.yc-capability-center{--wb-image-status-icon-size:var(--wb-size-5);--wb-image-status-gap:var(--wb-spacing-3);--wb-image-status-font-size:var(--wb-font-spec-body-sm-size);--wb-image-status-line-height:var(--wb-font-spec-body-sm-line-height)}
|
||||
.yc-capability-center{--wb-image-placeholder-bg:var(--wb-palette-gray-1);--wb-image-status-icon-color:var(--wb-icon-secondary);--wb-image-status-text-color:var(--wb-color-text-secondary)}
|
||||
:where(.yc-capability-center) .wb-button{box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;transition:background-color var(--wb-button-transition-duration) var(--wb-button-transition-easing), color var(--wb-button-transition-duration) var(--wb-button-transition-easing), border-color var(--wb-button-transition-duration) var(--wb-button-transition-easing), transform var(--wb-button-transition-duration) var(--wb-button-transition-easing);border-radius:var(--wb-button-border-radius-medium);border:none;outline:none;justify-content:center;align-items:center;font-family:PingFang SC,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-weight:500;text-decoration:none;display:inline-flex}
|
||||
:where(.yc-capability-center) .wb-button--medium{height:var(--wb-button-height-medium);padding:0 var(--wb-button-padding-medium);font-size:var(--wb-button-font-size-medium);gap:var(--wb-button-gap-medium);min-width:var(--wb-button-min-width-medium);line-height:var(--wb-button-line-height-medium)}
|
||||
:where(.yc-capability-center) .wb-button--xsmall{height:var(--wb-button-height-xsmall);font-size:var(--wb-button-font-size-small);line-height:var(--wb-button-height-xsmall);border-radius:var(--wb-button-border-radius-small);gap:0;padding:0}
|
||||
:where(.yc-capability-center) .wb-button--xsmall.wb-button--icon-only{width:var(--wb-button-height-xsmall);height:var(--wb-button-height-xsmall)}
|
||||
:where(.yc-capability-center) .wb-button--small{height:var(--wb-button-height-small);padding:0 var(--wb-button-padding-small);font-size:var(--wb-button-font-size-small);gap:var(--wb-button-gap-small);min-width:var(--wb-button-min-width-small);border-radius:var(--wb-button-border-radius-small);line-height:var(--wb-button-line-height-small)}
|
||||
:where(.yc-capability-center) .wb-button--small .wb-button__icon{width:var(--wb-button-icon-size-small);height:var(--wb-button-icon-size-small)}
|
||||
:where(.yc-capability-center) .wb-button--large{height:var(--wb-button-height-large);padding:0 var(--wb-button-padding-large);font-size:var(--wb-button-font-size-large);gap:var(--wb-button-gap-large);border-radius:var(--wb-button-border-radius-large);line-height:var(--wb-button-line-height-large)}
|
||||
:where(.yc-capability-center) .wb-button--primary{background-color:var(--wb-button-primary-bg);color:var(--wb-button-primary-fg)}
|
||||
:where(.yc-capability-center) .wb-button--primary:hover:not(:disabled){background-color:var(--wb-button-primary-bg-hover)}
|
||||
:where(.yc-capability-center) .wb-button--primary:active:not(:disabled){background-color:var(--wb-button-primary-bg-active)}
|
||||
:where(.yc-capability-center) .wb-button--primary:disabled,:where(.yc-capability-center) .wb-button--primary.wb-button--disabled{background-color:var(--wb-button-primary-bg-disabled);color:var(--wb-button-primary-fg-disabled);cursor:not-allowed}
|
||||
:where(.yc-capability-center) .wb-button--secondary{background-color:var(--wb-button-secondary-bg);color:var(--wb-button-secondary-fg);border:1px solid var(--wb-button-secondary-border)}
|
||||
:where(.yc-capability-center) .wb-button--secondary:hover:not(:disabled){background-color:var(--wb-button-secondary-bg-hover)}
|
||||
:where(.yc-capability-center) .wb-button--secondary:active:not(:disabled){background-color:var(--wb-button-secondary-bg-active)}
|
||||
:where(.yc-capability-center) .wb-button--secondary:disabled,:where(.yc-capability-center) .wb-button--secondary.wb-button--disabled{color:var(--wb-button-secondary-fg-disabled);border-color:var(--wb-button-secondary-border-disabled);cursor:not-allowed}
|
||||
:where(.yc-capability-center) .wb-button--grey{background-color:var(--wb-button-grey-bg);color:var(--wb-button-grey-fg)}
|
||||
:where(.yc-capability-center) .wb-button--grey:hover:not(:disabled){background-color:var(--wb-button-grey-bg-hover)}
|
||||
:where(.yc-capability-center) .wb-button--grey:active:not(:disabled){background-color:var(--wb-button-grey-bg-active)}
|
||||
:where(.yc-capability-center) .wb-button--grey:disabled,:where(.yc-capability-center) .wb-button--grey.wb-button--disabled{background-color:var(--wb-button-grey-bg-disabled);color:var(--wb-button-grey-fg-disabled);cursor:not-allowed}
|
||||
:where(.yc-capability-center) .wb-button--ghost{background-color:var(--wb-button-ghost-bg);color:var(--wb-button-ghost-fg);font-weight:400}
|
||||
:where(.yc-capability-center) .wb-button--ghost:hover:not(:disabled){background-color:var(--wb-button-ghost-bg-hover);color:var(--wb-button-ghost-fg-hover)}
|
||||
:where(.yc-capability-center) .wb-button--ghost:active:not(:disabled){background-color:var(--wb-button-ghost-bg-active);color:var(--wb-button-ghost-fg-hover)}
|
||||
:where(.yc-capability-center) .wb-button--ghost:disabled,:where(.yc-capability-center) .wb-button--ghost.wb-button--disabled{background-color:var(--wb-button-ghost-bg);color:var(--wb-button-ghost-fg-disabled);cursor:not-allowed}
|
||||
:where(.yc-capability-center) .wb-button--link{background-color:var(--wb-button-link-bg);color:var(--wb-button-link-fg);min-width:0;height:auto;padding:0;font-weight:400}
|
||||
:where(.yc-capability-center) .wb-button--link:hover:not(:disabled){background-color:var(--wb-button-link-bg-hover);color:var(--wb-button-link-fg-hover);text-decoration:underline}
|
||||
:where(.yc-capability-center) .wb-button--link:active:not(:disabled){background-color:var(--wb-button-link-bg-active);color:var(--wb-button-link-fg-hover);text-decoration:underline}
|
||||
:where(.yc-capability-center) .wb-button--link:disabled,:where(.yc-capability-center) .wb-button--link.wb-button--disabled{background-color:var(--wb-button-link-bg);color:var(--wb-button-link-fg-disabled);cursor:not-allowed}
|
||||
:where(.yc-capability-center) .wb-button--icon-only{min-width:unset;padding:0}
|
||||
:where(.yc-capability-center) .wb-button--icon-only.wb-button--medium{width:var(--wb-button-height-medium);height:var(--wb-button-height-medium)}
|
||||
:where(.yc-capability-center) .wb-button--icon-only.wb-button--small{width:var(--wb-button-height-small);height:var(--wb-button-height-small)}
|
||||
:where(.yc-capability-center) .wb-button--icon-only.wb-button--large{width:var(--wb-button-height-large);height:var(--wb-button-height-large)}
|
||||
:where(.yc-capability-center) .wb-button--plain{color:var(--wb-button-plain-fg,inherit);font-weight:var(--wb-button-plain-font-weight,inherit);background-color:#0000}
|
||||
:where(.yc-capability-center) .wb-button--circle{border-radius:50%}
|
||||
:where(.yc-capability-center) .wb-button--pill{border-radius:var(--wb-button-border-radius-pill)}
|
||||
:where(.yc-capability-center) .wb-button--danger.wb-button--primary{background-color:var(--wb-button-danger-bg);color:var(--wb-button-danger-fg-on-solid)}
|
||||
:where(.yc-capability-center) .wb-button--danger.wb-button--primary:hover:not(:disabled){background-color:var(--wb-button-danger-bg-hover);color:var(--wb-button-danger-fg-on-solid)}
|
||||
:where(.yc-capability-center) .wb-button--danger.wb-button--primary:active:not(:disabled){background-color:var(--wb-button-danger-bg-active)}
|
||||
:where(.yc-capability-center) .wb-button--danger.wb-button--primary:disabled,:where(.yc-capability-center) .wb-button--danger.wb-button--primary.wb-button--disabled{background-color:var(--wb-button-danger-bg-disabled);color:var(--wb-button-danger-fg-on-solid-disabled);cursor:not-allowed}
|
||||
:where(.yc-capability-center) .wb-button--danger.wb-button--secondary{color:var(--wb-button-danger-secondary-fg);border-color:var(--wb-button-danger-secondary-border);background-color:var(--wb-button-danger-secondary-bg)}
|
||||
:where(.yc-capability-center) .wb-button--danger.wb-button--secondary:hover:not(:disabled){background-color:var(--wb-button-danger-secondary-bg-hover);color:var(--wb-button-danger-secondary-fg);border-color:var(--wb-button-danger-secondary-border)}
|
||||
:where(.yc-capability-center) .wb-button--danger.wb-button--secondary:active:not(:disabled){background-color:var(--wb-button-danger-secondary-bg-active);color:var(--wb-button-danger-secondary-fg);border-color:var(--wb-button-danger-secondary-border)}
|
||||
:where(.yc-capability-center) .wb-button--danger.wb-button--secondary:disabled,:where(.yc-capability-center) .wb-button--danger.wb-button--secondary.wb-button--disabled{color:var(--wb-button-danger-secondary-fg-disabled);border-color:var(--wb-button-danger-secondary-border-disabled);cursor:not-allowed}
|
||||
:where(.yc-capability-center) .wb-button--danger.wb-button--grey{color:var(--wb-button-danger-grey-fg)}
|
||||
:where(.yc-capability-center) .wb-button--danger.wb-button--grey:hover:not(:disabled){background-color:var(--wb-button-grey-bg-hover);color:var(--wb-button-danger-grey-fg)}
|
||||
:where(.yc-capability-center) .wb-button--danger.wb-button--grey:active:not(:disabled){background-color:var(--wb-button-grey-bg-active)}
|
||||
:where(.yc-capability-center) .wb-button--danger.wb-button--grey:disabled,:where(.yc-capability-center) .wb-button--danger.wb-button--grey.wb-button--disabled{background-color:var(--wb-button-grey-bg-disabled);color:var(--wb-button-danger-fg-disabled);cursor:not-allowed}
|
||||
:where(.yc-capability-center) .wb-button--danger.wb-button--ghost{color:var(--wb-button-danger-ghost-fg)}
|
||||
:where(.yc-capability-center) .wb-button--danger.wb-button--ghost:hover:not(:disabled){background-color:var(--wb-button-danger-bg-soft-hover);color:var(--wb-button-danger-ghost-fg)}
|
||||
:where(.yc-capability-center) .wb-button--danger.wb-button--ghost:active:not(:disabled){background-color:var(--wb-button-danger-bg-soft-active)}
|
||||
:where(.yc-capability-center) .wb-button--danger.wb-button--ghost:disabled,:where(.yc-capability-center) .wb-button--danger.wb-button--ghost.wb-button--disabled{background-color:var(--wb-button-ghost-bg);color:var(--wb-button-danger-fg-disabled);cursor:not-allowed}
|
||||
:where(.yc-capability-center) .wb-button--success.wb-button--primary{background-color:var(--wb-button-success-bg);color:var(--wb-button-success-fg-on-solid)}
|
||||
:where(.yc-capability-center) .wb-button--success.wb-button--primary:hover:not(:disabled),:where(.yc-capability-center) .wb-button--success.wb-button--primary:active:not(:disabled){background-color:var(--wb-button-success-bg-hover)}
|
||||
:where(.yc-capability-center) .wb-button--success.wb-button--primary:disabled,:where(.yc-capability-center) .wb-button--success.wb-button--primary.wb-button--disabled{background-color:var(--wb-button-success-bg-disabled);color:var(--wb-button-success-fg-on-solid-disabled);cursor:not-allowed}
|
||||
:where(.yc-capability-center) .wb-button--success.wb-button--secondary,:where(.yc-capability-center) .wb-button--success.wb-button--grey,:where(.yc-capability-center) .wb-button--success.wb-button--ghost,:where(.yc-capability-center) .wb-button--success.wb-button--plain,:where(.yc-capability-center) .wb-button--success.wb-button--link{color:var(--wb-button-success-fg)}
|
||||
:where(.yc-capability-center) .wb-button--success.wb-button--secondary:disabled,:where(.yc-capability-center) .wb-button--success.wb-button--secondary.wb-button--disabled,:where(.yc-capability-center) .wb-button--success.wb-button--grey:disabled,:where(.yc-capability-center) .wb-button--success.wb-button--grey.wb-button--disabled,:where(.yc-capability-center) .wb-button--success.wb-button--ghost:disabled,:where(.yc-capability-center) .wb-button--success.wb-button--ghost.wb-button--disabled,:where(.yc-capability-center) .wb-button--success.wb-button--plain:disabled,:where(.yc-capability-center) .wb-button--success.wb-button--plain.wb-button--disabled,:where(.yc-capability-center) .wb-button--success.wb-button--link:disabled,:where(.yc-capability-center) .wb-button--success.wb-button--link.wb-button--disabled{color:var(--wb-button-success-fg-disabled)}
|
||||
:where(.yc-capability-center) .wb-button--loading{pointer-events:none;position:relative}
|
||||
:where(.yc-capability-center) .wb-button--loading .wb-button__content,:where(.yc-capability-center) .wb-button--loading .wb-button__icon{opacity:0}
|
||||
:where(.yc-capability-center) .wb-button--loading.wb-button--secondary .wb-button__loading-icon{color:var(--wb-button-loading-secondary-fg)}
|
||||
:where(.yc-capability-center) .wb-button--loading.wb-button--grey .wb-button__loading-icon{color:var(--wb-button-loading-grey-fg)}
|
||||
:where(.yc-capability-center) .wb-button--loading.wb-button--ghost .wb-button__loading-icon{color:var(--wb-button-loading-ghost-fg)}
|
||||
:where(.yc-capability-center) .wb-button--loading.wb-button--danger .wb-button__loading-icon{color:var(--wb-button-loading-danger-fg)}
|
||||
:where(.yc-capability-center) .wb-button--loading.wb-button--danger.wb-button--primary .wb-button__loading-icon{color:var(--wb-button-loading-danger-fg-on-solid)}
|
||||
:where(.yc-capability-center) .wb-button--loading.wb-button--link .wb-button__loading-icon{color:var(--wb-button-loading-link-fg)}
|
||||
:where(.yc-capability-center) .wb-button--loading.wb-button--small .wb-button__loading-icon{width:var(--wb-button-icon-size-small);height:var(--wb-button-icon-size-small)}
|
||||
:where(.yc-capability-center) .wb-button__loading{justify-content:center;align-items:center;display:flex;position:absolute;inset:0}
|
||||
:where(.yc-capability-center) .wb-button__loading-icon{color:var(--wb-button-loading-primary-fg);width:var(--wb-button-icon-size-medium);height:var(--wb-button-icon-size-medium);animation:.8s linear infinite wb-button-spin}
|
||||
:where(.yc-capability-center) .wb-button--full-width{width:100%}
|
||||
:where(.yc-capability-center) .wb-button__icon{width:var(--wb-button-icon-size-medium);height:var(--wb-button-icon-size-medium);flex-shrink:0;justify-content:center;align-items:center;display:inline-flex}
|
||||
:where(.yc-capability-center) .wb-button__icon svg{color:currentColor;fill:currentColor;width:100%;height:100%}
|
||||
:where(.yc-capability-center) .wb-button__content{align-items:center;display:inline-flex}
|
||||
:where(.yc-capability-center) .wb-input{font-family:inherit;line-height:var(--wb-input-line-height);color:var(--wb-input-color-text);font-size:var(--wb-input-font-size)}
|
||||
:where(.yc-capability-center) .wb-input::placeholder{color:var(--wb-input-color-placeholder)}
|
||||
:where(.yc-capability-center) .wb-input{box-sizing:border-box;width:100%;padding:0 var(--wb-input-padding-x);background:var(--wb-input-color-bg);border:1px solid var(--wb-input-border-color);border-radius:var(--wb-input-border-radius);transition:border-color var(--wb-input-motion-duration) var(--wb-input-motion-easing), box-shadow var(--wb-input-motion-duration) var(--wb-input-motion-easing);outline:none;align-items:center;display:inline-flex}
|
||||
:where(.yc-capability-center) .wb-input:hover:not(:disabled):not(:read-only){border-color:var(--wb-input-border-color-hover)}
|
||||
:where(.yc-capability-center) .wb-input:focus:not(:disabled):not(:read-only){border-color:var(--wb-input-border-color-focus)}
|
||||
:where(.yc-capability-center) .wb-input:disabled{color:var(--wb-input-color-text-disabled);background:var(--wb-input-disabled-bg);border-color:var(--wb-input-disabled-border-color);cursor:not-allowed}
|
||||
:where(.yc-capability-center) .wb-input--small{font-family:inherit;line-height:var(--wb-input-line-height);color:var(--wb-input-color-text);font-size:var(--wb-input-font-size-small)}
|
||||
:where(.yc-capability-center) .wb-input--small::placeholder{color:var(--wb-input-color-placeholder)}
|
||||
:where(.yc-capability-center) .wb-input--small{height:var(--wb-input-height-small);border-radius:var(--wb-input-border-radius-small)}
|
||||
:where(.yc-capability-center) .wb-input--medium{height:var(--wb-input-height-medium);border-radius:var(--wb-input-border-radius)}
|
||||
:where(.yc-capability-center) .wb-input--large{font-family:inherit;line-height:var(--wb-input-line-height);color:var(--wb-input-color-text);font-size:var(--wb-input-font-size-large)}
|
||||
:where(.yc-capability-center) .wb-input--large::placeholder{color:var(--wb-input-color-placeholder)}
|
||||
:where(.yc-capability-center) .wb-input--large{height:var(--wb-input-height-large);padding:0 var(--wb-input-padding-x-large);border-radius:var(--wb-input-border-radius-large)}
|
||||
:where(.yc-capability-center) .wb-input--status-error{border-color:var(--wb-input-status-error-color)}
|
||||
:where(.yc-capability-center) .wb-input--status-error:hover:not(:disabled):not(:read-only),:where(.yc-capability-center) .wb-input--status-error:focus:not(:disabled):not(:read-only){background:var(--wb-input-status-error-bg);border-color:var(--wb-input-status-error-color)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper{box-sizing:border-box;align-items:center;gap:var(--wb-input-gap);width:100%;padding:0 var(--wb-input-padding-x);background:var(--wb-input-color-bg);border:1px solid var(--wb-input-border-color);border-radius:var(--wb-input-border-radius);color:var(--wb-input-color-text);transition:border-color var(--wb-input-motion-duration) var(--wb-input-motion-easing), box-shadow var(--wb-input-motion-duration) var(--wb-input-motion-easing);display:inline-flex}
|
||||
:where(.yc-capability-center) .wb-input-wrapper:hover:not(.wb-input-wrapper--disabled){border-color:var(--wb-input-border-color-hover)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper:focus-within:not(.wb-input-wrapper--disabled){border-color:var(--wb-input-border-color-focus)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper:has(>.wb-input__icon-button),:where(.yc-capability-center) .wb-input-wrapper:has(>.wb-input__suffix>.wb-input__icon-button){padding-right:var(--wb-input-has-button-padding-right)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--large:has(>.wb-input__icon-button),:where(.yc-capability-center) .wb-input-wrapper--large:has(>.wb-input__suffix>.wb-input__icon-button){padding-right:var(--wb-input-has-button-padding-right-large)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--small{height:var(--wb-input-height-small);border-radius:var(--wb-input-border-radius-small)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--small .wb-input__inner{font-family:inherit;line-height:var(--wb-input-line-height);color:var(--wb-input-color-text);font-size:var(--wb-input-font-size-small)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--small .wb-input__inner::placeholder{color:var(--wb-input-color-placeholder)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--small .wb-input__prefix svg,:where(.yc-capability-center) .wb-input-wrapper--small .wb-input__suffix svg{font-size:var(--wb-input-icon-size-small)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--small .wb-input__icon-button{width:var(--wb-input-icon-button-size-small);height:var(--wb-input-icon-button-size-small)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--small .wb-input__icon-button svg{font-size:var(--wb-input-icon-size-small)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--medium{height:var(--wb-input-height-medium);border-radius:var(--wb-input-border-radius)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--large{height:var(--wb-input-height-large);padding:0 var(--wb-input-padding-x-large);border-radius:var(--wb-input-border-radius-large)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--large .wb-input__inner{font-family:inherit;line-height:var(--wb-input-line-height);color:var(--wb-input-color-text);font-size:var(--wb-input-font-size-large)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--large .wb-input__inner::placeholder{color:var(--wb-input-color-placeholder)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--status-error,:where(.yc-capability-center) .wb-input-wrapper--status-error:hover:not(.wb-input-wrapper--disabled),:where(.yc-capability-center) .wb-input-wrapper--status-error:focus-within:not(.wb-input-wrapper--disabled){background:var(--wb-input-status-error-bg);border-color:var(--wb-input-status-error-color)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--disabled{color:var(--wb-input-color-text-disabled);background:var(--wb-input-disabled-bg);border-color:var(--wb-input-disabled-border-color);cursor:not-allowed}
|
||||
:where(.yc-capability-center) .wb-input__prefix,:where(.yc-capability-center) .wb-input__suffix{color:var(--wb-input-color-icon);flex:none;align-items:center;display:inline-flex}
|
||||
:where(.yc-capability-center) .wb-input__prefix svg,:where(.yc-capability-center) .wb-input__suffix svg{width:1em;height:1em;font-size:var(--wb-input-icon-size)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--disabled .wb-input__prefix,:where(.yc-capability-center) .wb-input-wrapper--disabled .wb-input__suffix{color:var(--wb-input-color-icon-disabled)}
|
||||
:where(.yc-capability-center) .wb-input__inner{font-family:inherit;line-height:var(--wb-input-line-height);color:var(--wb-input-color-text);font-size:var(--wb-input-font-size)}
|
||||
:where(.yc-capability-center) .wb-input__inner::placeholder{color:var(--wb-input-color-placeholder)}
|
||||
:where(.yc-capability-center) .wb-input__inner{background:0 0;border:none;outline:none;flex:auto;min-width:0;height:100%;padding:0}
|
||||
:where(.yc-capability-center) .wb-input__inner:disabled{color:var(--wb-input-color-text-disabled);cursor:not-allowed}
|
||||
:where(.yc-capability-center) .wb-input__icon-button{width:var(--wb-input-icon-button-size);height:var(--wb-input-icon-button-size);border-radius:var(--wb-input-icon-button-border-radius);color:var(--wb-input-color-icon);cursor:pointer;transition:color var(--wb-input-motion-duration) var(--wb-input-motion-easing);background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}
|
||||
:where(.yc-capability-center) .wb-input__icon-button:hover{color:var(--wb-input-color-icon-hover)}
|
||||
:where(.yc-capability-center) .wb-input__icon-button:disabled,:where(.yc-capability-center) .wb-input__icon-button--disabled{color:var(--wb-input-color-icon-disabled);cursor:not-allowed;pointer-events:none}
|
||||
:where(.yc-capability-center) .wb-input__icon-button svg{width:1em;height:1em;font-size:var(--wb-input-icon-size)}
|
||||
:where(.yc-capability-center) .wb-input__clear--hidden{visibility:hidden;pointer-events:none}
|
||||
:where(.yc-capability-center) .wb-input--filled{background:var(--wb-input-filled-color-bg);border-color:#0000}
|
||||
:where(.yc-capability-center) .wb-input--filled:hover:not(:focus):not(:disabled):not(:read-only){background:var(--wb-input-filled-color-bg-hover);border-color:var(--wb-input-filled-color-bg-hover)}
|
||||
:where(.yc-capability-center) .wb-input--filled:focus:not(:disabled):not(:read-only){box-shadow:none;border-color:#0000}
|
||||
:where(.yc-capability-center) .wb-input--filled.wb-input--status-error{border-color:var(--wb-input-status-error-color)}
|
||||
:where(.yc-capability-center) .wb-input--filled.wb-input--status-error:hover:not(:focus):not(:disabled):not(:read-only){background:var(--wb-input-filled-color-bg-hover);border-color:var(--wb-input-status-error-color)}
|
||||
:where(.yc-capability-center) .wb-input--filled.wb-input--status-error:focus:not(:disabled):not(:read-only){background:var(--wb-input-filled-color-bg);border-color:var(--wb-input-status-error-color)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--filled{background:var(--wb-input-filled-color-bg);border-color:#0000}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--filled:hover:not(:focus-within):not(.wb-input-wrapper--disabled){background:var(--wb-input-filled-color-bg-hover);border-color:var(--wb-input-filled-color-bg-hover)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--filled:focus-within:not(.wb-input-wrapper--disabled){box-shadow:none;border-color:#0000}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--filled.wb-input-wrapper--status-error{border-color:var(--wb-input-status-error-color)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--filled.wb-input-wrapper--status-error:hover:not(:focus-within):not(.wb-input-wrapper--disabled){background:var(--wb-input-filled-color-bg-hover);border-color:var(--wb-input-status-error-color)}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--filled.wb-input-wrapper--status-error:focus-within:not(.wb-input-wrapper--disabled){background:var(--wb-input-filled-color-bg);border-color:var(--wb-input-status-error-color)}
|
||||
:where(.yc-capability-center) .wb-input-tips-wrapper{flex-direction:column;width:100%;display:inline-flex}
|
||||
:where(.yc-capability-center) .wb-input__tips{margin-top:var(--wb-spacing-1);font-size:var(--wb-font-body-size);line-height:var(--wb-font-body-line-height);color:var(--wb-color-text-tertiary)}
|
||||
:where(.yc-capability-center) .wb-input-tips-wrapper--status-error .wb-input__tips{color:var(--wb-input-status-error-color)}
|
||||
:where(.yc-capability-center) .wb-input__count{min-width:6ch;font-size:var(--wb-input-count-font-size);color:var(--wb-input-count-color);font-variant-numeric:tabular-nums;user-select:none;flex:none;justify-content:flex-end;align-items:center;line-height:1;display:inline-flex}
|
||||
:where(.yc-capability-center) .wb-input-group{box-sizing:border-box;align-items:stretch;width:100%;display:inline-flex}
|
||||
:where(.yc-capability-center) .wb-input-group .wb-input-wrapper,:where(.yc-capability-center) .wb-input-group .wb-input{flex:auto;min-width:0}
|
||||
:where(.yc-capability-center) .wb-input-group>.wb-input-group__addon:first-child+.wb-input-wrapper,:where(.yc-capability-center) .wb-input-group>.wb-input-group__addon:first-child+.wb-input{border-top-left-radius:0;border-bottom-left-radius:0}
|
||||
:where(.yc-capability-center) .wb-input-group>.wb-input-wrapper:has(+.wb-input-group__addon),:where(.yc-capability-center) .wb-input-group>.wb-input:has(+.wb-input-group__addon){border-top-right-radius:0;border-bottom-right-radius:0}
|
||||
:where(.yc-capability-center) .wb-input-group--small .wb-input-group__addon{height:var(--wb-input-height-small);font-size:var(--wb-input-font-size-small);border-radius:var(--wb-input-border-radius-small)}
|
||||
:where(.yc-capability-center) .wb-input-group--medium .wb-input-group__addon{height:var(--wb-input-height-medium);border-radius:var(--wb-input-border-radius)}
|
||||
:where(.yc-capability-center) .wb-input-group--large .wb-input-group__addon{height:var(--wb-input-height-large);width:var(--wb-input-height-large);border-radius:var(--wb-radius-xl);justify-content:center;padding:0}
|
||||
:where(.yc-capability-center) .wb-input-group__addon{box-sizing:border-box;padding:0 var(--wb-input-addon-padding-x);font-size:var(--wb-input-addon-font-size);line-height:var(--wb-input-addon-line-height);color:var(--wb-input-addon-color-text);background:var(--wb-input-addon-color-bg);border:1px solid var(--wb-input-addon-border-color);white-space:nowrap;flex:none;align-items:center;display:inline-flex}
|
||||
:where(.yc-capability-center) .wb-input-group__addon svg{width:1em;height:1em;font-size:var(--wb-input-icon-size)}
|
||||
:where(.yc-capability-center) .wb-input-group__addon:first-child{border-right:0;border-top-right-radius:0;border-bottom-right-radius:0}
|
||||
:where(.yc-capability-center) .wb-input-group__addon:last-child:not(:first-child){border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}
|
||||
:where(.yc-capability-center) .wb-input-group--disabled .wb-input-group__addon{color:var(--wb-input-color-text-disabled);background:var(--wb-input-disabled-bg);border-color:var(--wb-input-disabled-border-color)}
|
||||
:where(.yc-capability-center) .wb-input-group__addon:has(.wb-button){background:0 0;border:none;padding:0}
|
||||
:where(.yc-capability-center) .wb-input-group__addon:has(.wb-button)>.wb-button{border-top-left-radius:0;border-bottom-left-radius:0;height:100%}
|
||||
:where(.yc-capability-center) .wb-input-number{box-sizing:border-box;background:var(--wb-input-color-bg);border:1px solid var(--wb-input-border-color);border-radius:var(--wb-input-border-radius);width:100%;color:var(--wb-input-color-text);transition:border-color var(--wb-input-motion-duration) var(--wb-input-motion-easing), box-shadow var(--wb-input-motion-duration) var(--wb-input-motion-easing);align-items:center;display:inline-flex;overflow:hidden}
|
||||
:where(.yc-capability-center) .wb-input-number:hover:not(.wb-input-number--disabled){border-color:var(--wb-input-border-color-hover)}
|
||||
:where(.yc-capability-center) .wb-input-number:focus-within:not(.wb-input-number--disabled){border-color:var(--wb-input-border-color-focus)}
|
||||
:where(.yc-capability-center) .wb-input-number--small{height:var(--wb-input-height-small);border-radius:var(--wb-input-border-radius-small)}
|
||||
:where(.yc-capability-center) .wb-input-number--medium{height:var(--wb-input-height-medium)}
|
||||
:where(.yc-capability-center) .wb-input-number--large{height:var(--wb-input-height-large);border-radius:var(--wb-input-border-radius-large)}
|
||||
:where(.yc-capability-center) .wb-input-number__inner{min-width:0;height:100%;padding:0 var(--wb-input-padding-x);font-family:inherit;font-size:var(--wb-input-font-size);line-height:var(--wb-input-line-height);color:var(--wb-input-color-text);font-variant-numeric:tabular-nums;text-align:left;background:0 0;border:none;outline:none;flex:auto}
|
||||
:where(.yc-capability-center) .wb-input-number__inner::placeholder{color:var(--wb-input-color-placeholder)}
|
||||
:where(.yc-capability-center) .wb-input-number__inner:disabled{color:var(--wb-input-color-text-disabled);cursor:not-allowed}
|
||||
:where(.yc-capability-center) .wb-input-number--small .wb-input-number__inner{font-size:var(--wb-input-font-size-small)}
|
||||
:where(.yc-capability-center) .wb-input-number--large .wb-input-number__inner{padding:0 var(--wb-input-padding-x-large)}
|
||||
:where(.yc-capability-center) .wb-input-number__controls{opacity:0;pointer-events:none;height:100%;transition:opacity var(--wb-input-motion-duration) var(--wb-input-motion-easing);flex-direction:column;flex:none;gap:2px;display:inline-flex}
|
||||
:where(.yc-capability-center) .wb-input-number:hover:not(.wb-input-number--disabled) .wb-input-number__controls{opacity:1;pointer-events:auto}
|
||||
:where(.yc-capability-center) .wb-input-number__step{min-height:0;width:var(--wb-input-number-controls-width);background:var(--wb-input-number-step-color-bg);height:100%;color:var(--wb-input-number-step-color);border-radius:0;flex:1 1 0}
|
||||
:where(.yc-capability-center) .wb-input-number__step svg{width:var(--wb-input-icon-size);height:var(--wb-input-icon-size);font-size:var(--wb-input-icon-size)}
|
||||
:where(.yc-capability-center) .wb-input-number__step:hover:not(:disabled){color:var(--wb-input-number-step-color-hover);background:var(--wb-input-number-step-color-bg-hover)}
|
||||
:where(.yc-capability-center) .wb-input-number__step:disabled,:where(.yc-capability-center) .wb-input-number__step.wb-input__icon-button--disabled{cursor:not-allowed;pointer-events:auto}
|
||||
:where(.yc-capability-center) .wb-input-number__step:disabled:hover,:where(.yc-capability-center) .wb-input-number__step.wb-input__icon-button--disabled:hover{color:var(--wb-input-number-step-color);background:var(--wb-input-number-step-color-bg)}
|
||||
:where(.yc-capability-center) .wb-input-number .wb-input-number__step:disabled,:where(.yc-capability-center) .wb-input-number .wb-input-number__step.wb-input__icon-button--disabled{cursor:not-allowed;pointer-events:auto;color:var(--wb-input-color-icon-disabled);background:var(--wb-input-number-step-color-bg)}
|
||||
:where(.yc-capability-center) .wb-input-number .wb-input-number__step:disabled:hover,:where(.yc-capability-center) .wb-input-number .wb-input-number__step.wb-input__icon-button--disabled:hover{color:var(--wb-input-color-icon-disabled);background:var(--wb-input-number-step-color-bg)}
|
||||
:where(.yc-capability-center) .wb-input-number--status-error,:where(.yc-capability-center) .wb-input-number--status-error:hover:not(.wb-input-number--disabled),:where(.yc-capability-center) .wb-input-number--status-error:focus-within:not(.wb-input-number--disabled){border-color:var(--wb-input-status-error-color)}
|
||||
:where(.yc-capability-center) .wb-input-number--disabled{color:var(--wb-input-color-text-disabled);background:var(--wb-input-disabled-bg);border-color:var(--wb-input-disabled-border-color);cursor:not-allowed}
|
||||
:where(.yc-capability-center) .wb-input-wrapper--disabled .wb-timepicker-trigger__icon{color:var(--wb-input-color-icon-disabled)}
|
||||
:where(.yc-capability-center) .wb-segmented{align-items:stretch;gap:var(--wb-segmented-item-spacing);padding:var(--wb-segmented-track-padding);background:var(--wb-segmented-track-color-bg);border-radius:var(--wb-segmented-track-border-radius);box-sizing:border-box;--wb-segmented-thumb-left:0px;--wb-segmented-thumb-width:0px;--wb-segmented-thumb-top:0px;--wb-segmented-thumb-height:0px;display:inline-flex;position:relative;overflow:hidden}
|
||||
:where(.yc-capability-center) .wb-segmented--full{width:100%;display:flex}
|
||||
:where(.yc-capability-center) .wb-segmented--full .wb-segmented__item{flex:1 1 0;justify-content:center;min-width:0}
|
||||
:where(.yc-capability-center) .wb-segmented--vertical{flex-direction:column;align-items:stretch;width:max-content}
|
||||
:where(.yc-capability-center) .wb-segmented--vertical .wb-segmented__item{justify-content:flex-start;width:100%}
|
||||
:where(.yc-capability-center) .wb-segmented--vertical .wb-segmented__thumb{top:0;left:var(--wb-segmented-track-padding);width:calc(100% - var(--wb-segmented-track-padding) * 2);height:var(--wb-segmented-thumb-height);transform:translateY(var(--wb-segmented-thumb-top));transition:transform var(--wb-segmented-thumb-transition-duration) var(--wb-segmented-thumb-transition-easing), height var(--wb-segmented-thumb-transition-duration) var(--wb-segmented-thumb-transition-easing), opacity var(--wb-segmented-thumb-transition-duration-opacity) var(--wb-segmented-thumb-transition-easing)}
|
||||
:where(.yc-capability-center) .wb-segmented__thumb{top:var(--wb-segmented-track-padding);height:calc(100% - var(--wb-segmented-track-padding) * 2);width:var(--wb-segmented-thumb-width);background:var(--wb-segmented-thumb-color-bg);border-radius:var(--wb-segmented-thumb-border-radius);box-shadow:var(--wb-segmented-thumb-color-shadow);transform:translateX(var(--wb-segmented-thumb-left));opacity:0;pointer-events:none;z-index:0;transition:opacity var(--wb-segmented-thumb-transition-duration-opacity) var(--wb-segmented-thumb-transition-easing);position:absolute;left:0}
|
||||
:where(.yc-capability-center) .wb-segmented__thumb--measured{opacity:1;transition:transform var(--wb-segmented-thumb-transition-duration) var(--wb-segmented-thumb-transition-easing), width var(--wb-segmented-thumb-transition-duration) var(--wb-segmented-thumb-transition-easing), opacity var(--wb-segmented-thumb-transition-duration-opacity) var(--wb-segmented-thumb-transition-easing)}
|
||||
:where(.yc-capability-center) .wb-segmented__item{z-index:1;justify-content:center;align-items:center;gap:var(--wb-segmented-item-gap);box-sizing:border-box;border-radius:var(--wb-segmented-item-border-radius);cursor:pointer;color:var(--wb-segmented-item-color-text);font-size:var(--wb-segmented-item-font-size);line-height:var(--wb-segmented-item-line-height);font-weight:var(--wb-segmented-item-font-weight);height:var(--wb-segmented-item-height);padding:var(--wb-segmented-item-padding-y) var(--wb-segmented-item-padding-x);white-space:nowrap;transition:color var(--wb-segmented-item-transition-duration) var(--wb-segmented-item-transition-easing), background-color var(--wb-segmented-item-transition-duration) var(--wb-segmented-item-transition-easing);background:0 0;border:none;display:inline-flex;position:relative}
|
||||
:where(.yc-capability-center) .wb-segmented:not(.wb-segmented--full):not(.wb-segmented--vertical) .wb-segmented__item{min-width:0}
|
||||
:where(.yc-capability-center) .wb-segmented__item:hover:not(.wb-segmented__item--active):not(:disabled){color:var(--wb-segmented-item-color-text-hover)}
|
||||
:where(.yc-capability-center) .wb-segmented--show-hover .wb-segmented__item:hover:not(.wb-segmented__item--active):not(:disabled){background-color:var(--wb-segmented-item-color-bg-hover)}
|
||||
:where(.yc-capability-center) .wb-segmented .wb-segmented__item:focus-visible{outline:var(--wb-segmented-item-focus-outline-width) solid var(--wb-segmented-item-color-outline-focus);outline-offset:var(--wb-segmented-item-focus-outline-offset)}
|
||||
:where(.yc-capability-center) .wb-segmented__item--active{color:var(--wb-segmented-item-color-text-active);box-shadow:var(--wb-segmented-thumb-color-shadow)}
|
||||
:where(.yc-capability-center) .wb-segmented__item--active.wb-segmented__item--string-label{font-weight:var(--wb-segmented-item-font-weight-active)}
|
||||
:where(.yc-capability-center) .wb-segmented__item:disabled{color:var(--wb-segmented-item-color-text-disabled);cursor:not-allowed}
|
||||
:where(.yc-capability-center) .wb-segmented--disabled{pointer-events:none}
|
||||
:where(.yc-capability-center) .wb-segmented__item-label{text-overflow:ellipsis;white-space:nowrap;max-width:100%;display:inline-block;overflow:hidden}
|
||||
:where(.yc-capability-center) .wb-segmented__item-label:after{content:attr(data-text);visibility:hidden;height:0;font-weight:var(--wb-segmented-item-font-weight-active);display:block;overflow:hidden}
|
||||
:where(.yc-capability-center) .wb-segmented__item-icon{width:var(--wb-segmented-item-icon-size);height:var(--wb-segmented-item-icon-size);color:inherit;justify-content:center;align-items:center;display:inline-flex}
|
||||
:where(.yc-capability-center) .wb-segmented__item-icon>svg{width:100%;height:100%;display:block}
|
||||
:where(.yc-capability-center) .wb-segmented__item-right-icon{color:var(--wb-segmented-item-color-text);font-weight:var(--wb-segmented-item-font-weight);align-items:center;display:inline-flex}
|
||||
:where(.yc-capability-center) .wb-segmented--round{border-radius:var(--wb-segmented-track-border-radius-round)}
|
||||
:where(.yc-capability-center) .wb-segmented--round .wb-segmented__item,:where(.yc-capability-center) .wb-segmented--round .wb-segmented__thumb{border-radius:var(--wb-segmented-item-border-radius-round)}
|
||||
:where(.yc-capability-center) .wb-segmented--small .wb-segmented__item{font-size:var(--wb-segmented-item-font-size-small);line-height:var(--wb-segmented-item-line-height-small);height:var(--wb-segmented-item-height-small);padding:var(--wb-segmented-item-padding-y-small) var(--wb-segmented-item-padding-x-small)}
|
||||
:where(.yc-capability-center) .wb-segmented--large .wb-segmented__item{font-size:var(--wb-segmented-item-font-size-large);line-height:var(--wb-segmented-item-line-height-large);height:var(--wb-segmented-item-height-large);padding:var(--wb-segmented-item-padding-y-large) var(--wb-segmented-item-padding-x-large)}
|
||||
:where(.yc-capability-center) .wb-segmented--full.wb-segmented--fluid{container-type:inline-size}
|
||||
:where(.yc-capability-center) .wb-segmented--full.wb-segmented--fluid.wb-segmented--small .wb-segmented__item{padding-left:clamp(6px,5cqw,12px);padding-right:clamp(6px,5cqw,12px)}
|
||||
:where(.yc-capability-center) .wb-segmented--full.wb-segmented--fluid.wb-segmented--medium .wb-segmented__item{padding-left:clamp(8px,5cqw,16px);padding-right:clamp(8px,5cqw,16px)}
|
||||
:where(.yc-capability-center) .wb-segmented--full.wb-segmented--fluid.wb-segmented--large .wb-segmented__item{padding-left:clamp(12px,5cqw,24px);padding-right:clamp(12px,5cqw,24px)}
|
||||
:where(.yc-capability-center) .wb-segmented--fluid:not(.wb-segmented--full).wb-segmented--small .wb-segmented__item{padding-left:clamp(6px,1vw,12px);padding-right:clamp(6px,1vw,12px)}
|
||||
:where(.yc-capability-center) .wb-segmented--fluid:not(.wb-segmented--full).wb-segmented--medium .wb-segmented__item{padding-left:clamp(8px,1.5vw,16px);padding-right:clamp(8px,1.5vw,16px)}
|
||||
:where(.yc-capability-center) .wb-segmented--fluid:not(.wb-segmented--full).wb-segmented--large .wb-segmented__item{padding-left:clamp(12px,2vw,24px);padding-right:clamp(12px,2vw,24px)}
|
||||
:where(.yc-capability-center) .um-header__inner{flex-shrink:0;align-items:center;height:32px;display:inline-flex}
|
||||
:where(.yc-capability-center) .um-header__tabs{flex-wrap:nowrap;flex-shrink:0;align-items:center;gap:24px;height:32px;display:flex}
|
||||
:where(.yc-capability-center) .um-tab{white-space:nowrap;height:32px;color:var(--wb-color-text-tertiary);cursor:pointer;transition:color var(--wb-motion-duration-sm) var(--wb-motion-easing-standard);background:0 0;border:none;border-radius:0;flex-shrink:0;align-items:center;gap:4px;padding:0;font-family:inherit;font-size:18px;font-weight:500;line-height:26px;display:flex}
|
||||
:where(.yc-capability-center) .um-tab:hover{color:var(--wb-color-text-primary)}
|
||||
:where(.yc-capability-center) .um-tab--active{color:var(--wb-color-text-primary);font-weight:600}
|
||||
:where(.yc-capability-center) .um-tab--active:hover{color:var(--wb-color-text-primary)}
|
||||
:where(.yc-capability-center) ._page_1yu3o_7{--ec-page-gutter:24px;background:var(--wb-home-bg-secondary);height:100%;color:var(--wb-color-text-primary);flex-direction:column;display:flex;position:relative;overflow:hidden}
|
||||
:where(.yc-capability-center) ._inactive_1yu3o_17{opacity:.96}
|
||||
:where(.yc-capability-center) ._layout_1yu3o_20{flex:1;min-height:0;display:flex;overflow:hidden}
|
||||
:where(.yc-capability-center) ._main_1yu3o_26{flex-direction:column;flex:1;min-width:0;display:flex;position:relative;overflow:hidden}
|
||||
:where(.yc-capability-center) ._topbar_1yu3o_35{height:var(--wb-titlebar-height);padding:0 var(--ec-page-gutter);-webkit-app-region:drag;flex-shrink:0;align-items:center;display:flex}
|
||||
:where(.yc-capability-center) ._topbarLeft_1yu3o_43{-webkit-app-region:no-drag;flex-shrink:0;align-items:center;gap:4px;display:flex}
|
||||
:where(.yc-capability-center) ._topbarNavDivider_1yu3o_50{background:var(--wb-border-default);flex-shrink:0;width:1px;height:16px;margin:0 8px}
|
||||
:where(.yc-capability-center) ._topbarRight_1yu3o_57{-webkit-app-region:no-drag;flex-shrink:1;align-items:center;gap:12px;min-width:0;margin-left:auto;display:flex}
|
||||
:where(.yc-capability-center) ._topbarRight_1yu3o_57>.wb-button{flex:none}
|
||||
:where(.yc-capability-center) ._topbarActionBtn_1yu3o_69.wb-button--medium:not(.wb-button--icon-only){padding:0 16px}
|
||||
:where(.yc-capability-center) ._topbarRightMyExperts_1yu3o_72{flex:0 auto}
|
||||
:where(.yc-capability-center) ._topbarSubpage_1yu3o_75 ._subpageNav_1yu3o_75{min-width:0;color:var(--wb-color-text-primary);cursor:pointer;text-align:left;-webkit-app-region:no-drag;background:0 0;border:none;align-items:center;gap:12px;padding:0;display:inline-flex}
|
||||
:where(.yc-capability-center) ._topbarSubpage_1yu3o_75 ._subpageNav_1yu3o_75:hover{opacity:.8}
|
||||
:where(.yc-capability-center) ._topbarSubpage_1yu3o_75 ._subpageBack_1yu3o_91{flex-shrink:0;justify-content:center;align-items:center;width:20px;height:20px;display:inline-flex}
|
||||
:where(.yc-capability-center) ._topbarSubpage_1yu3o_75 ._subpageTitle_1yu3o_99{color:inherit;white-space:nowrap;align-items:center;gap:4px;margin:0;font-size:18px;font-weight:600;line-height:1.4;display:inline-flex}
|
||||
:where(.yc-capability-center) ._mainScroll_1yu3o_110{--ec-main-scrollbar-width:4px;min-height:0;padding:0 calc(var(--ec-page-gutter) - var(--ec-main-scrollbar-width)) 0 var(--ec-page-gutter);scrollbar-width:thin;scrollbar-color:var(--wb-border-default) transparent;flex:1;margin-right:8px;overflow:hidden auto}
|
||||
:where(.yc-capability-center) ._mainScroll_1yu3o_110::-webkit-scrollbar{width:4px}
|
||||
:where(.yc-capability-center) ._mainScroll_1yu3o_110::-webkit-scrollbar-track{background:0 0}
|
||||
:where(.yc-capability-center) ._mainScroll_1yu3o_110::-webkit-scrollbar-thumb{background:var(--wb-border-default);border-radius:2px}
|
||||
:where(.yc-capability-center) ._myExpertsScroll_1yu3o_131{display:flex}
|
||||
:where(.yc-capability-center) ._mainScrollSplit_1yu3o_134{box-sizing:border-box;width:auto;max-width:none;min-height:0;padding:0 calc(var(--ec-page-gutter) - var(--ec-main-scrollbar-width)) 28px var(--ec-page-gutter);flex:1;align-self:stretch;display:block;overflow:hidden auto}
|
||||
:where(.yc-capability-center) ._mainScrollSplit_1yu3o_134>*{box-sizing:border-box;width:100%}
|
||||
:where(.yc-capability-center) ._categoryTabsPlaceholder_1yu3o_151{flex-shrink:0;height:30px;margin-bottom:12px}
|
||||
:where(.yc-capability-center) ._expertScroll_1yu3o_156{min-height:0}
|
||||
:where(.yc-capability-center) ._pageError_1yu3o_159{height:100%;color:var(--wb-color-text-tertiary);flex-direction:column;justify-content:center;align-items:center;gap:16px;padding:60px 20px;display:flex}
|
||||
:where(.yc-capability-center) ._pageErrorMsg_1yu3o_169{text-align:center;margin:0;font-size:14px}
|
||||
:where(.yc-capability-center) ._listError_1yu3o_174{color:var(--wb-color-text-tertiary);flex-direction:column;justify-content:center;align-items:center;gap:16px;padding:60px 20px;display:flex}
|
||||
:where(.yc-capability-center) ._listErrorMsg_1yu3o_183{text-align:center;margin:0;font-size:14px}
|
||||
:where(.yc-capability-center) ._loadMoreSpinner_1yu3o_188{justify-content:center;align-items:center;padding:12px 0;display:flex}
|
||||
:where(.yc-capability-center) ._loadMoreSentinel_1yu3o_194{height:0;margin:0;padding:0}
|
||||
@media (width<=767px){:where(.yc-capability-center) ._page_1yu3o_7{--ec-page-gutter:20px;--ec-content-gutter:20px}:where(.yc-capability-center) ._mainScroll_1yu3o_110,:where(.yc-capability-center) ._mainScrollSplit_1yu3o_134{padding:12px var(--ec-page-gutter) 24px}}
|
||||
:where(.yc-capability-center) .teams-container.is-mobile ._page_1yu3o_7{--ec-page-gutter:var(--wb-spacing-5);--ec-content-gutter:var(--wb-spacing-5)}
|
||||
:where(.yc-capability-center) .teams-container.is-mobile ._topbar_1yu3o_35{-webkit-overflow-scrolling:touch;scrollbar-width:none;flex-wrap:nowrap;overflow:auto hidden}
|
||||
:where(.yc-capability-center) .teams-container.is-mobile ._topbar_1yu3o_35::-webkit-scrollbar{display:none}
|
||||
:where(.yc-capability-center) .teams-container.is-mobile ._topbarLeft_1yu3o_43,:where(.yc-capability-center) .teams-container.is-mobile ._topbarRight_1yu3o_57{flex-shrink:0}
|
||||
/* YCode host sizing; the source Electron token supplies the 64px topbar. */
|
||||
.yc-capability-center{height:100%;width:100%;min-height:0;min-width:0;display:flex;font-family:var(--wb-font-family);font-size:13px;isolation:isolate}
|
||||
.yc-capability-center>.expert-center-page{flex:1;min-width:0}
|
||||
.yc-capability-center .capability-page-host{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;overflow:hidden}
|
||||
.yc-capability-center .capability-title-slot{display:contents}
|
||||
.yc-capability-center .wb-icon{flex:none;display:block}
|
||||
.yc-capability-center .capability-icon-button{border:0;background:transparent;color:var(--wb-color-text-secondary);display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:6px;border-radius:6px}
|
||||
.yc-capability-center .capability-text-button{border:0;background:transparent;color:var(--wb-color-text-secondary);font-size:13px;cursor:pointer;padding:4px 8px;border-radius:6px}
|
||||
.yc-capability-center .capability-text-button:hover{background:var(--wb-color-bg-primary-hover)}
|
||||
@media(max-width:700px){.yc-capability-center .ec-topbar{flex-wrap:wrap;height:auto;min-height:64px;gap:8px;padding-top:12px;padding-bottom:12px}.yc-capability-center .ec-topbar__right{max-width:100%;flex-wrap:wrap}.yc-capability-center .um-header__tabs{gap:16px}.yc-capability-center .um-tab{font-size:16px}.yc-capability-center ._root_mbn4e_1{width:140px}}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/* Host sizing for the existing local skill view inside the recovered cr SubMenu.
|
||||
Current cr skills panel is 320px wide / at most 392px high. */
|
||||
.yc-skill-picker[data-embedded]{width:320px;max-width:100%;display:flex;flex-direction:column;min-height:0}
|
||||
.yc-skill-picker[data-embedded]>._panel_1mrhu_14{width:100%;max-height:min(392px,calc(100vh - 16px));min-height:0}
|
||||
/* The legacy local capability views normally inherit cb tokens from ChatPane.
|
||||
cr SubMenu renders in its own theme portal, so bridge the same roles there. */
|
||||
.yc-source-composer-add-menu .yc-expert-picker[data-embedded],
|
||||
.yc-source-composer-add-menu .yc-skill-picker[data-embedded]{
|
||||
--cb-dropdown-bg-color:var(--cr-bg-elevated);
|
||||
--cb-popover-border:var(--cr-border-elevated);
|
||||
--cb-shadow-popover:var(--cr-shadow-panel-light);
|
||||
--cb-search-input-bg:var(--cr-bg-input-inset);
|
||||
--cb-dropdown-item-hover-bg-color:var(--cr-bg-hover);
|
||||
--cb-text-primary:var(--cr-text-primary);
|
||||
--cb-text-secondary:var(--cr-text-secondary);
|
||||
--cb-text-tertiary:var(--cr-text-tertiary);
|
||||
--cb-text-muted:var(--cr-text-tertiary);
|
||||
--cb-popover-divider:var(--cr-popover-divider);
|
||||
--cb-green-color:var(--wb-brand-primary);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/* YCode capability host bindings; source rule bodies are in connectors-source.css. */
|
||||
.ycode-connectors.connectors-page{height:100%;min-height:0;color:var(--wb-color-text-primary);font-family:var(--wb-font-family,"PingFang SC",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif)}
|
||||
.ycode-connectors.connectors-toolbar{display:flex;align-items:center;gap:12px;-webkit-app-region:no-drag}
|
||||
.ycode-connectors .connectors-toolbar .ec-search-wrapper{width:180px}
|
||||
.ycode-connectors .yc-mcp-search{display:flex;align-items:center;gap:8px;box-sizing:border-box;width:100%;min-height:32px;border:1px solid var(--wb-border-default);border-radius:8px;padding:0 10px;background:var(--wb-bg-primary)}
|
||||
.ycode-connectors .yc-mcp-search .wb-input__inner{flex:1;min-width:0;width:100%;height:30px;outline:0;border:0;background:none;color:var(--wb-color-text-primary);padding:0;font:inherit}
|
||||
.ycode-connectors .yc-mcp-search .wb-input__prefix{display:flex;color:var(--wb-color-text-tertiary)}
|
||||
.ycode-connectors .yc-mcp-search-clear{display:flex;align-items:center;justify-content:center;background:none;border:0;padding:0;cursor:pointer;color:var(--wb-color-text-tertiary)}
|
||||
.ycode-connectors .wb-button__content{display:inline-flex;align-items:center;gap:6px}
|
||||
.ycode-connectors .wb-button__content>svg{width:16px;height:16px}
|
||||
.ycode-connectors .wb-button__icon>svg{width:16px;height:16px}
|
||||
.ycode-connectors .cb-search-input{--cb-input-background:var(--wb-bg-primary);--cb-input-border:var(--wb-border-default);--cb-text-primary:var(--wb-color-text-primary);--cb-text-tertiary:var(--wb-color-text-tertiary);--cb-text-placeholder:var(--wb-color-text-tertiary)}
|
||||
.ycode-connectors .yc-mcp-error{color:var(--wb-error,#d32f2f);background:color-mix(in srgb,var(--wb-error,#d32f2f) 8%,transparent);padding:10px 14px;margin:8px 24px;border-radius:8px;font-size:13px;white-space:pre-wrap}
|
||||
.ycode-connectors .yc-mcp-editor-note{padding:8px 16px;margin:0;font-size:12px;color:var(--wb-color-text-secondary);line-height:1.5}
|
||||
.ycode-connectors .mcp-config-textarea{box-sizing:border-box;resize:none;width:100%;height:100%;padding:8px 16px;border:0;outline:0;background:var(--wb-bg-primary);color:var(--wb-color-text-primary);font:13px/20px Consolas,monospace;tab-size:2}
|
||||
.ycode-connectors .yc-mcp-confirm-mask{position:fixed;inset:0;z-index:10001;display:flex;justify-content:center;align-items:center;background:var(--wb-modal-overlay-mask-color,#0006)}
|
||||
.ycode-connectors .yc-mcp-confirm{box-sizing:border-box;width:360px;max-width:calc(100vw - 32px);border-radius:12px;background:var(--wb-bg-primary);color:var(--wb-color-text-primary);padding:24px;box-shadow:var(--wb-shadow-lg)}
|
||||
.ycode-connectors .yc-mcp-confirm>h3{margin:0 0 12px;font-size:16px}.ycode-connectors .yc-mcp-confirm>p{font-size:14px;line-height:1.6}.ycode-connectors .yc-mcp-confirm>div{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}
|
||||
.ycode-connectors svg{display:block}.ycode-connectors button:focus-visible,.ycode-connectors [role=button]:focus-visible{outline:2px solid var(--wb-border-focus,#1683ff);outline-offset:2px}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { McpServerInput, McpServerView } from '@shared/capabilities'
|
||||
|
||||
export function mcpEditorDocument(server?: McpServerView): string {
|
||||
const config: Record<string, unknown> = server ? server.transport === 'stdio'
|
||||
? { command: server.command, args: server.args || [], ...(server.cwd ? { cwd: server.cwd } : {}) }
|
||||
: { url: server.url } : { command: '', args: [] }
|
||||
if (server?.envKeys.length) config.env = Object.fromEntries(server.envKeys.map(key => [key, '']))
|
||||
if (server?.headerKeys.length) config.headers = Object.fromEntries(server.headerKeys.map(key => [key, '']))
|
||||
return JSON.stringify({ mcpServers: { [server?.name || 'my-mcp']: config } }, null, 2)
|
||||
}
|
||||
function isMap(value: unknown): value is Record<string, unknown> { return !!value && typeof value === 'object' && !Array.isArray(value) }
|
||||
/** This editor changes one server only. Other saved servers are never implicitly removed. */
|
||||
export function parseMcpEditor(content: string, previous?: McpServerView): McpServerInput {
|
||||
if (content.length > 1024 * 1024) throw new Error('MCP 配置内容过大')
|
||||
let parsed: unknown
|
||||
try { parsed = JSON.parse(content) } catch { throw new Error('JSON 格式无效,请检查配置内容') }
|
||||
if (!isMap(parsed) || !isMap(parsed.mcpServers) || Object.keys(parsed.mcpServers).length !== 1) throw new Error('请在 mcpServers 中配置一个 MCP 服务')
|
||||
const [name, config] = Object.entries(parsed.mcpServers)[0]
|
||||
if (!name.trim() || !isMap(config)) throw new Error('请输入有效的 MCP 名称与配置')
|
||||
if (config.type && !['http', 'streamable-http', 'stdio'].includes(String(config.type))) throw new Error('当前支持 stdio 和 Streamable HTTP 连接')
|
||||
const transport = typeof config.command === 'string' ? 'stdio' : typeof config.url === 'string' ? 'http' : undefined
|
||||
if (!transport) throw new Error('请输入 command 或 HTTP url')
|
||||
if (transport === 'stdio' && !(config.command as string).trim()) throw new Error('请输入 MCP 启动命令')
|
||||
if (config.args !== undefined && (!Array.isArray(config.args) || config.args.some(value => typeof value !== 'string'))) throw new Error('args 必须为字符串数组')
|
||||
if (config.cwd !== undefined && typeof config.cwd !== 'string') throw new Error('cwd 必须为字符串')
|
||||
for (const field of ['env', 'headers']) if (config[field] !== undefined && (!isMap(config[field]) || Object.values(config[field]).some(value => typeof value !== 'string'))) throw new Error(`${field} 必须为字符串键值对象`)
|
||||
return { id: previous?.id, name: name.trim(), transport, command: config.command as string | undefined,
|
||||
args: config.args as string[] | undefined, cwd: config.cwd as string | undefined, url: config.url as string | undefined,
|
||||
env: config.env as Record<string, string> | undefined, headers: config.headers as Record<string, string> | undefined,
|
||||
enabled: previous?.enabled ?? false }
|
||||
}
|
||||
export function mcpIconColor(name: string): string {
|
||||
const colors = ['#6366f1', '#8b5cf6', '#ec4899', '#f97316', '#14b8a6', '#3b82f6', '#10b981', '#f43f5e', '#06b6d4', '#a855f7']
|
||||
let hash = 0
|
||||
for (let index = 0; index < name.length; index++) { hash = (hash << 5) - hash + name.charCodeAt(index); hash |= 0 }
|
||||
return colors[Math.abs(hash) % colors.length]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/* REA ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94; skills-panel-CsnIUki4.css SHA256 c1d5c5de51554a886fde0399c5202cd45063fa091b213b842014bea91dbb69a0. Rule bodies retained; selector scope adapter only. */
|
||||
:where(.ycode-connectors) :is(.connector-panel){background:var(--wb-home-bg-secondary);flex-direction:column;height:100%;display:flex;overflow:hidden}:where(.ycode-connectors) :is(.connector-panel .workbuddy-topbar.ec-topbar){height:var(--wb-titlebar-height);background:var(--wb-home-bg-secondary);-webkit-app-region:drag;flex-shrink:0;align-items:center;margin-bottom:0;padding:0 24px;display:flex}:where(.ycode-connectors) :is(.connector-panel .ec-topbar__left){-webkit-app-region:no-drag;align-items:center;gap:4px;display:flex}:where(.ycode-connectors) :is(.connector-panel .ec-topbar__right){-webkit-app-region:no-drag;align-items:center;gap:12px;min-width:0;margin-left:auto;display:flex}:where(.ycode-connectors) :is(.connector-panel .ec-topbar__right>.wb-button){flex:none}:where(.ycode-connectors) :is(.connector-panel .connector-topbar-action-btn.wb-button--medium:not(.wb-button--icon-only)){padding:0 16px}:where(.ycode-connectors) :is(.connector-panel .ec-search-wrapper){flex:0 180px;width:180px;min-width:120px;position:relative}:where(.ycode-connectors) :is(.connector-panel .ec-search-wrapper .wb-input-wrapper){background:var(--wb-bg-primary);border-color:var(--wb-border-default)}:where(.ycode-connectors) :is(.connector-panel .ec-search-wrapper .wb-input-wrapper:hover:not(.wb-input-wrapper--disabled)){border-color:var(--wb-border-hover)}:where(.ycode-connectors) :is(.connector-panel .ec-search-wrapper .wb-input-wrapper:focus-within:not(.wb-input-wrapper--disabled)){border-color:var(--wb-border-popover-strong)}:where(.ycode-connectors) :is(.connector-panel .ec-search-wrapper .wb-input__inner::placeholder){color:color-mix(in srgb, var(--wb-color-text-primary) 30%, transparent)}@media (width<=540px){:where(.ycode-connectors) :is(.connector-panel .ec-search-wrapper){flex-shrink:1;width:100%;min-width:0}}:where(.ycode-connectors) :is(.connector-panel-detail-wrapper){flex-direction:column;height:100%;display:flex;overflow:hidden}:where(.ycode-connectors) :is(.connector-panel-detail-wrapper .connectors-panel){padding:0}:where(.ycode-connectors) :is(.connector-panel-detail-wrapper .connectors-panel .connectors-panel__detail-header){padding:16px 0 12px}:where(.ycode-connectors) :is(.connector-panel-detail-wrapper .connectors-panel .connectors-panel__scroll-content){padding:12px 40px}:where(.ycode-connectors) :is(.connector-panel-detail-wrapper .connectors-panel .connectors-panel__detail-card){margin-bottom:16px;padding:12px 16px}:where(.ycode-connectors) :is(.connector-panel-detail-wrapper .connectors-panel .connectors-panel__action-buttons){margin-bottom:20px}:where(.ycode-connectors) :is(.connector-panel-detail-wrapper .connectors-panel .connectors-panel__action-buttons .connectors-panel__btn){padding:8px 16px}:where(.ycode-connectors) :is(.connector-panel-detail-wrapper .connectors-panel .connectors-panel__section-divider){margin:12px 0}:where(.ycode-connectors) :is(.connector-panel-detail-wrapper .connectors-panel .connectors-panel__danger-zone){padding:16px}:where(.ycode-connectors) :is(.connector-panel-detail-wrapper .connectors-panel .connectors-panel__intermediate-alert){padding:14px}:where(.ycode-connectors) :is(.connector-panel-header){flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:16px;padding:8px 24px 14px;display:flex}:where(.ycode-connectors) :is(.connector-panel-heading){min-width:260px}:where(.ycode-connectors) :is(.connector-panel-title){color:var(--wb-color-text-primary);margin:0;font-size:18px;font-weight:600;line-height:1.33}:where(.ycode-connectors) :is(.connector-panel-subtitle){color:var(--wb-color-text-secondary);margin:8px 0 0;font-size:14px;line-height:1.5}:where(.ycode-connectors) :is(.connector-panel-toolbar){flex-shrink:0;align-items:center;gap:12px;display:flex}:where(.ycode-connectors) :is(.connector-panel-modal-toolbar){padding:8px 24px 12px}:where(.ycode-connectors) :is(.connector-panel-search){width:180px;min-width:180px}:where(.ycode-connectors) :is(.connector-panel-search .cb-search-input){width:100%}:where(.ycode-connectors) :is(.connector-panel-search .cb-search-input .cb-search-input__input){border-radius:10px;height:36px}:where(.ycode-connectors) :is(.connector-panel-content){scrollbar-width:thin;scrollbar-color:var(--wb-palette-black-10,#0000001a) transparent;flex:1;min-height:0;padding:0 24px 24px;overflow:hidden auto}:where(.ycode-connectors) :is(.connector-panel-content::-webkit-scrollbar){width:4px}:where(.ycode-connectors) :is(.connector-panel-content::-webkit-scrollbar-track){background:0 0}:where(.ycode-connectors) :is(.connector-panel-content::-webkit-scrollbar-thumb){background:var(--wb-palette-black-10,#0000001a);border-radius:12px}:where(.ycode-connectors) :is(body.vscode-dark .connector-panel-content),:where(.ycode-connectors) :is(body[data-vscode-theme-kind=vscode-dark] .connector-panel-content){scrollbar-color:var(--wb-palette-white-10,#ffffff1a) transparent}:where(.ycode-connectors) :is(body.vscode-dark .connector-panel-content::-webkiLine truncated
|
||||
|
||||
/* Retained shared primitive bodies. */
|
||||
:where(.ycode-connectors) :is(.wb-input-footer){flex-direction:row;flex-shrink:0;align-items:center;gap:4px;height:32px;margin-top:4px;padding:0 12px;display:flex}:where(.ycode-connectors) :is(.wb-input-footer .task-starter){margin-bottom:0;padding:0}:where(.ycode-connectors) :is(.wb-input-footer--hint-row){height:32px}:where(.ycode-connectors) :is(.wb-input-footer__external-model-warning){color:var(--cb-error-foreground,#cf222e);white-space:nowrap;flex-shrink:0;margin-left:auto;font-size:13px;font-weight:450;line-height:20px}:where(.ycode-connectors) :is(.wb-button._footer-cancel-btn_nq6f5_185),:where(.ycode-connectors) :is(.wb-button._footer-confirm-btn_nq6f5_186){font-weight:var(--wb-font-weight-600)}:where(.ycode-connectors) :is(.wb-button._footer-confirm-btn_nq6f5_186:not(:disabled)){color:var(--kb-picker-shell-btn-primary-fg)}:where(.ycode-connectors) :is(.wb-button){box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;transition:background-color var(--wb-button-transition-duration) var(--wb-button-transition-easing), color var(--wb-button-transition-duration) var(--wb-button-transition-easing), border-color var(--wb-button-transition-duration) var(--wb-button-transition-easing), transform var(--wb-button-transition-duration) var(--wb-button-transition-easing);border-radius:var(--wb-button-border-radius-medium);border:none;outline:none;justify-content:center;align-items:center;font-family:PingFang SC,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-weight:500;text-decoration:none;display:inline-flex}:where(.ycode-connectors) :is(.wb-button--medium){height:var(--wb-button-height-medium);padding:0 var(--wb-button-padding-medium);font-size:var(--wb-button-font-size-medium);gap:var(--wb-button-gap-medium);min-width:var(--wb-button-min-width-medium);line-height:var(--wb-button-line-height-medium)}:where(.ycode-connectors) :is(.wb-button--xsmall){height:var(--wb-button-height-xsmall);font-size:var(--wb-button-font-size-small);line-height:var(--wb-button-height-xsmall);border-radius:var(--wb-button-border-radius-small);gap:0;padding:0}:where(.ycode-connectors) :is(.wb-button--xsmall.wb-button--icon-only){width:var(--wb-button-height-xsmall);height:var(--wb-button-height-xsmall)}:where(.ycode-connectors) :is(.wb-button--small){height:var(--wb-button-height-small);padding:0 var(--wb-button-padding-small);font-size:var(--wb-button-font-size-small);gap:var(--wb-button-gap-small);min-width:var(--wb-button-min-width-small);border-radius:var(--wb-button-border-radius-small);line-height:var(--wb-button-line-height-small)}:where(.ycode-connectors) :is(.wb-button--small .wb-button__icon){width:var(--wb-button-icon-size-small);height:var(--wb-button-icon-size-small)}:where(.ycode-connectors) :is(.wb-button--large){height:var(--wb-button-height-large);padding:0 var(--wb-button-padding-large);font-size:var(--wb-button-font-size-large);gap:var(--wb-button-gap-large);border-radius:var(--wb-button-border-radius-large);line-height:var(--wb-button-line-height-large)}:where(.ycode-connectors) :is(.wb-button--primary){background-color:var(--wb-button-primary-bg);color:var(--wb-button-primary-fg)}:where(.ycode-connectors) :is(.wb-button--primary:hover:not(:disabled)){background-color:var(--wb-button-primary-bg-hover)}:where(.ycode-connectors) :is(.wb-button--primary:active:not(:disabled)){background-color:var(--wb-button-primary-bg-active)}:where(.ycode-connectors) :is(.wb-button--primary:disabled),:where(.ycode-connectors) :is(.wb-button--primary.wb-button--disabled){background-color:var(--wb-button-primary-bg-disabled);color:var(--wb-button-primary-fg-disabled);cursor:not-allowed}:where(.ycode-connectors) :is(.wb-button--secondary){background-color:var(--wb-button-secondary-bg);color:var(--wb-button-secondary-fg);border:1px solid var(--wb-button-secondary-border)}:where(.ycode-connectors) :is(.wb-button--secondary:hover:not(:disabled)){background-color:var(--wb-button-secondary-bg-hover)}:where(.ycode-connectors) :is(.wb-button--secondary:active:not(:disabled)){background-color:var(--wb-button-secondary-bg-active)}:where(.ycode-connectors) :is(.wb-button--secondary:disabled),:where(.ycode-connectors) :is(.wb-button--secondary.wb-button--disabled){color:var(--wb-button-secondary-fg-disabled);border-color:var(--wb-button-secondary-border-disabled);cursor:not-allowed}:where(.ycode-connectors) :is(.wb-button--grey){background-color:var(--wb-button-grey-bg);color:var(--wb-button-grey-fg)}:where(.ycode-connectors) :is(.wb-button--grey:hover:not(:disabled)){background-color:var(--wb-button-grey-bg-hover)}:where(.ycode-connectors) :is(.wb-button--grey:active:not(:disabled)){background-color:var(--wb-button-grey-bg-active)}:where(.ycode-connectors) :is(.wb-button--grey:disabled),:where(.ycode-connectors) :is(.wb-button--grey.wb-button--disabled){background-color:var(--wb-button-grey-bg-disabled);color:var(--wb-button-grey-fg-disabled);cursor:not-allowed}:where(.ycode-connectors) :is(.wb-button--gLine truncated
|
||||
@@ -0,0 +1,277 @@
|
||||
/* Generated from pinned REA CSS. Source declarations retained; local scope and explicit host portal wrapper. */
|
||||
.yc-created-expert-source{display:contents}
|
||||
:where(.yc-created-expert-source) .wb-popover{background:var(--wb-bg-popover);border-radius:var(--wb-popover-radius);color:var(--wb-popover-text-color);animation:wb-popover-fade-in var(--wb-popover-transition-duration) var(--wb-popover-transition-easing);padding:var(--wb-popover-padding);font-size:var(--wb-popover-font-size);line-height:var(--wb-popover-line-height);border:.5px solid var(--wb-popover-border-color);box-shadow:var(--wb-popover-shadow);outline:none;flex-direction:column;display:flex}
|
||||
:where(.yc-created-expert-source) .wb-popover__arrow{background:var(--wb-bg-popover);border:.5px solid var(--wb-popover-border-color);transform:rotate(45deg)}
|
||||
:where(.yc-created-expert-source) .wb-popover--placement-top .wb-popover__arrow{border-top:none;border-left:none}
|
||||
:where(.yc-created-expert-source) .wb-popover--placement-bottom .wb-popover__arrow{border-bottom:none;border-right:none}
|
||||
:where(.yc-created-expert-source) .wb-popover--placement-left .wb-popover__arrow{border-bottom:none;border-left:none}
|
||||
:where(.yc-created-expert-source) .wb-popover--placement-right .wb-popover__arrow{border-top:none;border-right:none}
|
||||
@keyframes wb-popover-fade-in{0%{opacity:0;transform:translateY(2px)}to{opacity:1;transform:translateY(0)}}
|
||||
:where(.yc-created-expert-source) .wb-popover--expand-initial,:where(.yc-created-expert-source) .wb-popover--expand-open,:where(.yc-created-expert-source) .wb-popover--expand-close{opacity:0;transform:scale(var(--wb-popover-expand-scale-collapsed));transition:transform var(--wb-popover-expand-duration-in) var(--wb-popover-expand-easing-in), opacity var(--wb-popover-expand-duration-in) var(--wb-popover-expand-easing-in);animation:none}
|
||||
:where(.yc-created-expert-source) .wb-popover--expand-open{opacity:1;transform:scale(1)}
|
||||
:where(.yc-created-expert-source) .wb-popover--expand-close{opacity:0;transform:scale(var(--wb-popover-expand-scale-collapsed));transition:transform var(--wb-popover-expand-duration-out) var(--wb-popover-expand-easing-out), opacity var(--wb-popover-expand-duration-out) var(--wb-popover-expand-easing-out)}
|
||||
@media (prefers-reduced-motion:reduce){:where(.yc-created-expert-source) .wb-popover--expand-initial,:where(.yc-created-expert-source) .wb-popover--expand-open,:where(.yc-created-expert-source) .wb-popover--expand-close{transition-duration:1ms;transform:none}}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-popconfirm{padding:var(--wb-spacing-5);width:240px}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-dropdown{-webkit-app-region:no-drag}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-dropdown--card{--wb-popover-shadow:var(--wb-dropdown-card-shadow)}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-dropdown--scrollable{padding-top:0;overflow:hidden}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-dropdown--scrollable .wb-dropdown__search,:where(.yc-created-expert-source) .wb-popover.wb-dropdown--scrollable .wb-dropdown__list{padding-top:var(--wb-popover-padding)}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-dropdown--scrollable .wb-dropdown__search+.wb-dropdown__list{padding-top:0}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-dropdown--scrollable .wb-dropdown__scroll{margin-right:calc(2px - var(--wb-popover-padding));padding-right:2px}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__scroll,:where(.yc-created-expert-source) .wb-dropdown__virtual-scroll{flex-direction:column;min-height:0;display:flex}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__scroll::-webkit-scrollbar{width:4px}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__virtual-scroll::-webkit-scrollbar{width:4px}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__scroll::-webkit-scrollbar-thumb{border-radius:var(--wb-dropdown-item-radius);background:0 0}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__virtual-scroll::-webkit-scrollbar-thumb{border-radius:var(--wb-dropdown-item-radius);background:0 0}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__scroll:hover,:where(.yc-created-expert-source) .wb-dropdown__virtual-scroll:hover{background-color:var(--wb-bg-popover)}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__scroll:hover::-webkit-scrollbar-thumb{background:var(--wb-dropdown-scrollbar-thumb-color)}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__virtual-scroll:hover::-webkit-scrollbar-thumb{background:var(--wb-dropdown-scrollbar-thumb-color)}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__scroll--virtual>.wb-dropdown__virtual-scroll:first-child{padding-top:var(--wb-popover-padding);box-sizing:border-box}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__virtual-scroll{flex:auto;width:100%;min-height:0;display:block;overflow-y:auto}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__virtual-scroll .wb-dropdown__item{height:var(--wb-dropdown-item-min-height)}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-dropdown--scrollable .wb-dropdown__virtual-scroll .wb-dropdown__list{padding-top:0}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__list{gap:var(--wb-dropdown-item-gap);flex-direction:column;min-width:112px;margin:0;padding:0;list-style:none;display:flex}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__scroll--has-footer>.wb-dropdown__list{padding-bottom:var(--wb-dropdown-spacing-3)}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__item{box-sizing:border-box;align-items:center;gap:var(--wb-dropdown-spacing-3);padding:var(--wb-dropdown-item-padding-y) var(--wb-dropdown-spacing-3);min-height:var(--wb-dropdown-item-min-height);border-radius:var(--wb-dropdown-item-radius);font-size:var(--wb-dropdown-item-font-size);line-height:var(--wb-dropdown-item-line-height);color:var(--wb-dropdown-item-text-color);cursor:pointer;user-select:none;transition:background var(--wb-dropdown-item-transition-duration) var(--wb-dropdown-item-transition-easing);display:flex;position:relative}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__item:hover,:where(.yc-created-expert-source) .wb-dropdown__item:focus-visible{background:var(--wb-dropdown-item-bg-hover);outline:none}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__item-hit{appearance:none;color:inherit;font:inherit;cursor:pointer;background:0 0;border:0;margin:0;padding:0;position:absolute;inset:0}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__item-hit:focus{outline:none}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__item--selected{color:var(--wb-dropdown-item-text-color);background:var(--wb-dropdown-item-bg-selected);font-weight:500}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__item--selected:hover,:where(.yc-created-expert-source) .wb-dropdown__item--selected:focus-visible{background:var(--wb-dropdown-item-bg-selected)}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__item--danger{color:var(--wb-dropdown-item-text-color-danger)}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__item--danger .wb-dropdown__icon{color:inherit}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__item--disabled{color:var(--wb-dropdown-item-text-color-disabled);cursor:not-allowed}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__item--disabled .wb-dropdown__item-hit{cursor:not-allowed}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__item--disabled:hover,:where(.yc-created-expert-source) .wb-dropdown__item--disabled:focus-visible{background:0 0}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__item--disabled .wb-dropdown__icon{color:inherit}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__item-reason-hit{border-radius:inherit;background:0 0;position:absolute;inset:0}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__item--empty{color:var(--wb-dropdown-item-text-color-disabled);font-size:var(--wb-dropdown-item-font-size);line-height:var(--wb-dropdown-item-line-height);padding:var(--wb-dropdown-item-empty-padding);text-align:center}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__icon{color:inherit;width:var(--wb-dropdown-item-icon-size);height:var(--wb-dropdown-item-icon-size);font-size:var(--wb-dropdown-item-icon-size);flex-shrink:0;align-items:center;display:inline-flex}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__icon svg{max-width:100%;max-height:100%;color:inherit}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__label{flex:1;min-width:0}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__label--ellipsis{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__arrow{color:var(--wb-dropdown-item-text-color-disabled);flex-shrink:0;align-items:center;display:inline-flex}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__check{color:var(--wb-dropdown-item-check-color);width:var(--wb-dropdown-item-icon-size);height:var(--wb-dropdown-item-icon-size);flex-shrink:0;align-items:center;display:inline-flex}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__highlight{color:var(--wb-dropdown-highlight-color);background:0 0}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__search{padding-bottom:var(--wb-dropdown-spacing-3)}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__input{box-sizing:border-box;height:var(--wb-dropdown-item-min-height);border-radius:var(--wb-dropdown-item-radius);gap:var(--wb-dropdown-spacing-3);color:var(--wb-dropdown-item-text-color);font-size:var(--wb-dropdown-item-font-size);line-height:var(--wb-dropdown-item-line-height);background:var(--wb-dropdown-search-input-bg);border:0}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__input:hover:not(:focus-within):not(.wb-input-wrapper--disabled){background:var(--wb-dropdown-search-input-bg)}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__input .wb-input__prefix svg{width:var(--wb-dropdown-item-icon-size);height:var(--wb-dropdown-item-icon-size);color:var(--wb-dropdown-search-prefix-color)}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__input .wb-input__icon-button svg{width:var(--wb-dropdown-item-icon-size);height:var(--wb-dropdown-item-icon-size);color:var(--wb-dropdown-search-clear-color)}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__divider{height:1px;margin:var(--wb-dropdown-divider-margin-y) 0;background:var(--wb-dropdown-divider-color)}
|
||||
:where(.yc-created-expert-source) .wb-dropdown__footer{padding-top:var(--wb-dropdown-spacing-3);border-top:1px solid var(--wb-dropdown-divider-color)}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-select-popover{border-radius:var(--wb-select-radius-lg);box-sizing:border-box;flex-direction:column;min-width:120px;max-height:100%;padding-top:0;padding-bottom:0;display:flex;overflow:hidden}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-select-popover .wb-select__panel{flex-direction:column;flex:auto;min-height:0;display:flex}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-select-popover .wb-select__search{flex:none}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-select-popover .wb-select__list{flex:auto;min-height:0}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-select-popover .wb-select__search{padding-top:var(--wb-popover-padding)}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-select-popover .wb-select__search+.wb-select__list{padding-top:0}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-timepicker-popover{border-radius:var(--wb-timepicker-radius);box-sizing:border-box;min-width:var(--wb-timepicker-popover-min-width,160px);max-width:var(--wb-timepicker-popover-max-width,320px);overflow:hidden}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-datepicker-popover{box-sizing:border-box;width:var(--wb-datepicker-popover-width,282px);border-radius:var(--wb-radius-lg);padding:0}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-datepicker-popover:has(.wb-datepicker-panel-container--wide){width:auto;min-width:var(--wb-datepicker-popover-width,282px)}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-rangepicker-popover{box-sizing:border-box;border-radius:var(--wb-radius-lg);padding:0}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-context-menu{max-width:min(240px,100vw - 16px)}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-context-menu .wb-dropdown__label{flex:1;min-width:0}
|
||||
.yc-created-expert-source{--cb-font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;--cb-font-family-mono:"Menlo", "Monaco", "Courier New", monospace;--cb-spacing-xs:4px;--cb-spacing-sm:8px;--cb-spacing-md:12px;--cb-spacing-lg:16px;--cb-spacing-xl:20px;--cb-spacing-xxl:24px;--cb-border-radius-sm:4px;--cb-border-radius-md:6px;--cb-border-radius-lg:8px;--cb-border-radius-xl:16px;--cb-border-radius-2xl:24px;--cb-transition-fast:.15s ease;--cb-transition-base:.2s ease;--cb-transition-slow:.3s ease;--cb-z-index-dropdown:100;--cb-z-index-modal:1000;--cb-z-index-modal-confirm:100001;--cb-z-index-tooltip:1100;--cb-vscode-editor-background:var(--vscode-editor-background,#fff);--cb-vscode-sideBar-background:var(--vscode-sideBar-background,#f3f3f3);--cb-vscode-input-background:var(--vscode-input-background,#fff);--cb-vscode-dropdown-background:var(--vscode-dropdown-background,#fff);--cb-vscode-foreground:var(--vscode-foreground,#333);--cb-vscode-editor-foreground:var(--vscode-editor-foreground,#4c4f69);--cb-vscode-titleBar-activeBackground:var(--vscode-titleBar-activeBackground,#ddd);--cb-vscode-titleBar-activeForeground:var(--vscode-titleBar-activeForeground,#333);--cb-vscode-titleBar-inactiveBackground:var(--vscode-titleBar-inactiveBackground,#ddd);--cb-vscode-titleBar-inactiveForeground:var(--vscode-titleBar-inactiveForeground,#666);--cb-titlebar-control-hover-background:#0000000f;--cb-vscode-descriptionForeground:var(--vscode-descriptionForeground,#717171);--cb-vscode-widget-border:var(--vscode-widget-border,#e1e1e1);--cb-vscode-panel-border:var(--vscode-panel-border,#e1e1e1);--cb-vscode-list-hoverBackground:var(--vscode-list-hoverBackground,#f0f0f0);--cb-vscode-toolbar-hoverBackground:var(--vscode-toolbar-hoverBackground,#f0f0f0);--cb-vscode-button-background:var(--vscode-button-background,#007acc);--cb-vscode-button-foreground:var(--vscode-button-foreground,#fff);--cb-vscode-button-hoverBackground:var(--vscode-button-hoverBackground,#0062a3);--cb-vscode-scrollbarSlider-background:var(--vscode-scrollbarSlider-background,#c1c1c1);--cb-vscode-scrollbarSlider-hoverBackground:var(--vscode-scrollbarSlider-hoverBackground,#a8a8a8);--cb-vscode-textLink-foreground:var(--vscode-textLink-foreground,#006ab1);--cb-panel-bg-primary:var(--vscode-editor-background,#fff);--cb-text-primary:#333;--cb-text-secondary:#666;--cb-text-disabled:#0000004d;--cb-text-link:#006ab1;--cb-stroke-secondary:#d8dee4;--cb-markdown-hr-border-color:#ebebeb;--cb-bg-primary:#0000000d;--cb-bg-secondary:#f5f5f5;--cb-team-member-card-background:#f5f5f5;--cb-team-member-icon-background:#f5f5f5;--cb-button-dark-background:#1a1a1a;--cb-button-dark-foreground:#fff;--cb-button-dark-hover-background:#333;--cb-text-error-active:#b42c3f;--cb-button-danger-secondary-fg:#b42c3f;--cb-text-link-hover:#0e58a0;--cb-text-solid:#000;--cb-bg-primary-hover:#ebebeb;--cb-bg-primary-active:#f2f2f2;--cb-border-secondary:#ebebeb;--cb-shadow-popover:0 6px 24px 0 #0000000a, 0 4px 6px 0 #0000000a;--cb-border-drag-indicator:#1470b4;--cb-tooltip-bg:#000000b3}
|
||||
.expert-center-dark .yc-created-expert-source,.yc-created-expert-source.expert-center-dark{--cb-vscode-editor-background:var(--vscode-editor-background,#1e1e1e);--cb-vscode-sideBar-background:var(--vscode-sideBar-background,#252526);--cb-vscode-input-background:var(--vscode-input-background,#3c3c3c);--cb-vscode-dropdown-background:var(--vscode-dropdown-background,#3c3c3c);--cb-vscode-foreground:var(--vscode-foreground,#ccc);--cb-vscode-editor-foreground:var(--vscode-editor-foreground,#ccc);--cb-vscode-titleBar-activeBackground:var(--vscode-titleBar-activeBackground,#3c3c3c);--cb-vscode-titleBar-activeForeground:var(--vscode-titleBar-activeForeground,#ccc);--cb-vscode-titleBar-inactiveBackground:var(--vscode-titleBar-inactiveBackground,#3c3c3c);--cb-vscode-titleBar-inactiveForeground:var(--vscode-titleBar-inactiveForeground,#999);--cb-titlebar-control-hover-background:#ffffff14;--cb-vscode-descriptionForeground:var(--vscode-descriptionForeground,#9d9d9d);--cb-vscode-widget-border:var(--vscode-widget-border,#414141);--cb-vscode-panel-border:var(--vscode-panel-border,#3c3c3c);--cb-vscode-list-hoverBackground:var(--vscode-list-hoverBackground,#2a2d2e);--cb-vscode-toolbar-hoverBackground:var(--vscode-toolbar-hoverBackground,#353535);--cb-vscode-button-background:var(--vscode-button-background,#0e639c);--cb-vscode-button-foreground:var(--vscode-button-foreground,#fff);--cb-vscode-button-hoverBackground:var(--vscode-button-hoverBackground,#17b);--cb-vscode-button-secondaryBackground:var(--vscode-button-secondaryBackground,#3a3d41);--cb-vscode-button-secondaryForeground:var(--vscode-button-secondaryForeground,#fff);--cb-vscode-button-secondaryHoverBackground:var(--vscode-button-secondaryHoverBackground,#45494e);--cb-vscode-scrollbarSlider-background:var(--vscode-scrollbarSlider-background,#797979);--cb-vscode-scrollbarSlider-hoverBackground:var(--vscode-scrollbarSlider-hoverBackground,#646464);--cb-vscode-textLink-foreground:var(--vscode-textLink-foreground,#3794ff);--cb-vscode-list-activeSelectionForeground:var(--vscode-list-activeSelectionForeground,#fff);--cb-vscode-list-inactiveForeground:var(--vscode-foreground,#c5c5c5);--cb-vscode-focusBorder:var(--vscode-focusBorder,#007fd4);--cb-vscode-list-inactiveSelectionBackground:var(--vscode-list-inactiveSelectionBackground);--cb-vscode-widget-shadow:var(--vscode-widget-shadow,#0000005c);--cb-vscode-menu-foreground:var(--vscode-menu-foreground,#ccc);--cb-vscode-menu-separatorBackground:var(--vscode-menu-separatorBackground,#454545);--cb-vscode-menu-border:var(--vscode-menu-border,#454545);--cb-vscode-quickInputList-focusBackground:var(--vscode-quickInputList-focusBackground,#04395e);--cb-icon-foreground:var(--cb-vscode-foreground,#ccc);--cb-list-activeSelectionBackground:var(--cb-vscode-list-activeSelectionBackground,#094771);--cb-list-focusBackground:var(--cb-vscode-list-focusBackground,#062f4a);--cb-tree-indent-guides-stroke:var(--cb-vscode-tree-indentGuidesStroke,#ffffff1a);--cb-focus-outline-color:var(--cb-vscode-focusBorder,#007fd4);--cb-list-item-bg:#1e2127;--cb-list-item-hover-bg:#272c34;--cb-list-item-selected-bg:#ffffff0f;--cb-scrollbar-thumb:var(--cb-vscode-scrollbarSlider-background,#79797966);--cb-scrollbar-thumb-hover:var(--cb-vscode-scrollbarSlider-hoverBackground,#646464b3);--cb-list-item-foreground:#c5c5c5;--cb-list-item-selected-foreground:#fff;--cb-success-color:#28b894;--cb-project-invitation-button-background:#16a34a;--cb-project-invitation-button-hover-background:#16a34a;--cb-project-invitation-card-background:#fff;--cb-project-invitation-hero-gradient:linear-gradient(#f0fdf4 0%, #fff 100%);--cb-button-primary:#6c4dff;--cb-mermaid-toggle-active:var(--cb-vscode-button-background,#0e639c);--cb-text-primary:#d2d3e0;--cb-text-secondary:#858699;--cb-text-tertiary:#a0a0a0;--cb-text-muted:#e8ecf1b3;--cb-text-highlight:#40a6ff;--cb-text-disabled:#ffffff40;--cb-text-error-active:#f14c4c;--cb-button-danger-secondary-fg:#dc4258;--cb-text-link-hover:#7ab8e8;--cb-text-solid:#fff;--cb-bg-primary-hover:#ffffff14;--cb-bg-primary-active:#ffffff1f;--cb-border-secondary:#ffffff1a;--cb-shadow-popover:0 6px 24px 0 #0006, 0 4px 6px 0 #0006;--cb-shadow-message-toolbar:0px 4px 12px 0px #0006, 0px 3px 6px 0px #0006;--cb-border-drag-indicator:#4da3e0;--cb-input-foreground:var(--cb-text-primary);--cb-input-background:var(--cb-vscode-input-background);--cb-input-border-color:#cccccc1f;--cb-input-focus-border-color:#cccccc4d;--cb-input-placeholder:var(--cb-text-secondary);--cb-input-button-foreground:#d2d3e0;--cb-input-button-background:#4c4f6b;--cb-panel-border-color:#cccccc1f;--cb-content-background:color-mix(in srgb, var(--vscode-foreground) 4%, transparent);--cb-content-border-color:#ffffff14;--cb-main-area-background:var(--cb-vscode-input-background);--cb-main-area-border-color:var(--cb-content-border-color);--cb-main-area-box-shadow:none;--cb-marker-foreground:var(--cb-text-highlight);--cb-marker-background:#34414b;--cb-tag-background:#80808033;--cb-tag-selected-backgrLine truncated
|
||||
.yc-created-expert-source{--cb-vscode-editor-background:var(--vscode-editor-background,#1e1e1e);--cb-vscode-sideBar-background:var(--vscode-sideBar-background,#252526);--cb-vscode-input-background:var(--vscode-input-background,#3c3c3c);--cb-vscode-dropdown-background:var(--vscode-dropdown-background,#3c3c3c);--cb-vscode-foreground:var(--vscode-foreground,#ccc);--cb-vscode-editor-foreground:var(--vscode-editor-foreground,#ccc);--cb-vscode-titleBar-activeBackground:var(--vscode-titleBar-activeBackground,#3c3c3c);--cb-vscode-titleBar-activeForeground:var(--vscode-titleBar-activeForeground,#ccc);--cb-vscode-titleBar-inactiveBackground:var(--vscode-titleBar-inactiveBackground,#3c3c3c);--cb-vscode-titleBar-inactiveForeground:var(--vscode-titleBar-inactiveForeground,#999);--cb-titlebar-control-hover-background:#ffffff14;--cb-vscode-descriptionForeground:var(--vscode-descriptionForeground,#9d9d9d);--cb-vscode-widget-border:var(--vscode-widget-border,#414141);--cb-vscode-panel-border:var(--vscode-panel-border,#3c3c3c);--cb-vscode-list-hoverBackground:var(--vscode-list-hoverBackground,#2a2d2e);--cb-vscode-toolbar-hoverBackground:var(--vscode-toolbar-hoverBackground,#353535);--cb-vscode-button-background:var(--vscode-button-background,#0e639c);--cb-vscode-button-foreground:var(--vscode-button-foreground,#fff);--cb-vscode-button-hoverBackground:var(--vscode-button-hoverBackground,#17b);--cb-vscode-button-secondaryBackground:var(--vscode-button-secondaryBackground,#3a3d41);--cb-vscode-button-secondaryForeground:var(--vscode-button-secondaryForeground,#fff);--cb-vscode-button-secondaryHoverBackground:var(--vscode-button-secondaryHoverBackground,#45494e);--cb-vscode-scrollbarSlider-background:var(--vscode-scrollbarSlider-background,#797979);--cb-vscode-scrollbarSlider-hoverBackground:var(--vscode-scrollbarSlider-hoverBackground,#646464);--cb-vscode-textLink-foreground:var(--vscode-textLink-foreground,#3794ff);--cb-vscode-list-activeSelectionForeground:var(--vscode-list-activeSelectionForeground,#fff);--cb-vscode-list-inactiveForeground:var(--vscode-foreground,#c5c5c5);--cb-vscode-list-inactiveSelectionBackground:var(--vscode-list-inactiveSelectionBackground);--cb-vscode-widget-shadow:var(--vscode-widget-shadow,#0000005c);--cb-vscode-menu-foreground:var(--vscode-menu-foreground,#ccc);--cb-vscode-menu-separatorBackground:var(--vscode-menu-separatorBackground,#454545);--cb-vscode-menu-border:var(--vscode-menu-border,#454545);--cb-vscode-quickInputList-focusBackground:var(--vscode-quickInputList-focusBackground,#04395e);--cb-panel-bg-primary:var(--vscode-panel-background,#1e1e1e);--cb-colleagues-dashboard-bg:var(--cb-input-area-container-secondary,var(--cb-panel-bg-primary));--cb-colleagues-card-border:var(--cb-border);--cb-colleagues-prompt-bg:var(--cb-bg-secondary);--cb-colleagues-mode-switch-bg:var(--cb-bg-secondary);--cb-colleagues-mode-switch-active-bg:#00c29a;--cb-colleagues-mode-switch-inactive-fg:var(--cb-text-secondary);--cb-colleagues-mode-switch-active-fg:#fff;--cb-colleagues-remote-chip-bg:var(--cb-panel-bg-primary);--cb-colleagues-remote-chip-border:var(--cb-border);--cb-colleagues-remote-chip-fg:var(--cb-text-primary);--cb-colleague-card-border:var(--cb-border);--cb-colleague-card-shadow:0 4px 10px #00000052;--cb-colleague-card-status-pill-bg:var(--cb-bg-secondary);--cb-colleague-card-status-pill-fg:var(--cb-text-primary);--cb-colleague-card-chat-bg:var(--cb-bg-secondary);--cb-icon-foreground:var(--cb-vscode-foreground,#ccc);--cb-list-activeSelectionBackground:var(--cb-vscode-list-activeSelectionBackground,#094771);--cb-list-focusBackground:var(--cb-vscode-list-focusBackground,#062f4a);--cb-tree-indent-guides-stroke:var(--cb-vscode-tree-indentGuidesStroke,#ffffff1a);--cb-focus-outline-color:var(--cb-vscode-focusBorder,#007fd4);--cb-list-item-bg:#1e2127;--cb-list-item-hover-bg:#272c34;--cb-list-item-selected-bg:#ffffff0f;--cb-scrollbar-thumb:var(--cb-vscode-scrollbarSlider-background,#79797966);--cb-scrollbar-thumb-hover:var(--cb-vscode-scrollbarSlider-hoverBackground,#646464b3);--cb-list-item-foreground:#c5c5c5;--cb-list-item-selected-foreground:#fff;--cb-text-link:#3794ff;--cb-success-color:#28b894;--cb-project-invitation-button-background:#16a34a;--cb-project-invitation-button-hover-background:#16a34a;--cb-project-invitation-card-background:#fff;--cb-project-invitation-header-background:#fffc;--cb-project-invitation-header-border:#f3f4f6;--cb-project-invitation-hero-icon-border:#d1fae5;--cb-project-invitation-brand-badge-background:#1a1a1a;--cb-project-invitation-brand-badge-foreground:#fff;--cb-project-invitation-brand-text:#1a1a1a;--cb-project-invitation-footer-text:#c0c5cc;--cb-project-invitation-callout-border:#fed7aa;--cb-project-invitation-callout-background:#fff7ed;--cb-project-invitation-callout-foreground:#9a3412;--cb-project-invitation-callout-icon:#ea580c;--cb-project-invitation-feedback-success-background:#f0fdf4;--cb-project-invitation-feedback-success-border:#d1fae5;--cb-project-invitation-feedbLine truncated
|
||||
.expert-center-light .yc-created-expert-source,.yc-created-expert-source.expert-center-light{--cb-vscode-editor-background:var(--vscode-editor-background,#fff);--cb-vscode-sideBar-background:var(--vscode-sideBar-background,#f3f3f3);--cb-vscode-input-background:var(--vscode-input-background,#fff);--cb-vscode-dropdown-background:var(--vscode-dropdown-background,#fff);--cb-vscode-foreground:var(--vscode-foreground,#333);--cb-vscode-editor-foreground:var(--vscode-editor-foreground,#4c4f69);--cb-vscode-titleBar-activeBackground:var(--vscode-titleBar-activeBackground,#ddd);--cb-vscode-titleBar-activeForeground:var(--vscode-titleBar-activeForeground,#333);--cb-vscode-titleBar-inactiveBackground:var(--vscode-titleBar-inactiveBackground,#ddd);--cb-vscode-titleBar-inactiveForeground:var(--vscode-titleBar-inactiveForeground,#666);--cb-titlebar-control-hover-background:#0000000f;--cb-vscode-descriptionForeground:var(--vscode-descriptionForeground,#717171);--cb-vscode-widget-border:var(--vscode-widget-border,#e1e1e1);--cb-vscode-panel-border:var(--vscode-panel-border,#e1e1e1);--cb-vscode-list-hoverBackground:var(--vscode-list-hoverBackground,#f0f0f0);--cb-vscode-toolbar-hoverBackground:var(--vscode-toolbar-hoverBackground,#f0f0f0);--cb-vscode-button-background:var(--vscode-button-background,#007acc);--cb-vscode-button-foreground:var(--vscode-button-foreground,#fff);--cb-vscode-button-hoverBackground:var(--vscode-button-hoverBackground,#0062a3);--cb-vscode-button-secondaryBackground:var(--vscode-button-secondaryBackground,#5f6a79);--cb-vscode-button-secondaryForeground:var(--vscode-button-secondaryForeground,#fff);--cb-vscode-button-secondaryHoverBackground:var(--vscode-button-secondaryHoverBackground,#4c5561);--cb-vscode-scrollbarSlider-background:var(--vscode-scrollbarSlider-background,#c1c1c1);--cb-vscode-scrollbarSlider-hoverBackground:var(--vscode-scrollbarSlider-hoverBackground,#a8a8a8);--cb-vscode-textLink-foreground:var(--vscode-textLink-foreground,#006ab1);--cb-vscode-list-activeSelectionForeground:var(--vscode-list-activeSelectionForeground,#333);--cb-vscode-list-inactiveForeground:var(--vscode-foreground,#616161);--cb-vscode-list-inactiveSelectionBackground:var(--vscode-list-inactiveSelectionBackground);--cb-vscode-widget-shadow:var(--vscode-widget-shadow,#00000029);--cb-vscode-menu-foreground:var(--vscode-menu-foreground,#333);--cb-vscode-menu-separatorBackground:var(--vscode-menu-separatorBackground,#d4d4d4);--cb-vscode-menu-border:var(--vscode-menu-border,#d4d4d4);--cb-vscode-quickInputList-focusBackground:var(--vscode-quickInputList-focusBackground,#e8e8e8);--cb-icon-foreground:var(--cb-vscode-foreground,#333);--cb-list-activeSelectionBackground:var(--cb-vscode-list-activeSelectionBackground,#e1e1e1);--cb-list-focusBackground:var(--cb-vscode-list-focusBackground,#e8e8e8);--cb-tree-indent-guides-stroke:var(--cb-vscode-tree-indentGuidesStroke,#0000001a);--cb-focus-outline-color:var(--cb-vscode-focusBorder,#006ab1);--cb-list-item-bg:#f7f7f7;--cb-list-item-hover-bg:#efefef;--cb-list-item-selected-bg:#f7f7f7;--cb-scrollbar-thumb:var(--cb-vscode-scrollbarSlider-background,#c1c1c166);--cb-scrollbar-thumb-hover:var(--cb-vscode-scrollbarSlider-hoverBackground,#a8a8a8b3);--cb-list-item-foreground:#616161;--cb-list-item-selected-foreground:#333;--cb-panel-bg-primary:var(--vscode-editor-background,#fff);--cb-colleagues-dashboard-bg:#fafafa;--cb-colleagues-card-border:#f2f2f2;--cb-colleagues-prompt-bg:#fafafa;--cb-colleagues-mode-switch-bg:#f7f7f7;--cb-colleagues-mode-switch-active-bg:#00c29a;--cb-colleagues-mode-switch-inactive-fg:#000000b3;--cb-colleagues-mode-switch-active-fg:#fff;--cb-colleagues-remote-chip-bg:#fff;--cb-colleagues-remote-chip-border:#ebebeb;--cb-colleagues-remote-chip-fg:#000;--cb-colleague-card-border:#ebebeb;--cb-colleague-card-shadow:0 4px 10px #0000000d;--cb-colleague-card-status-pill-bg:#f8f8f8;--cb-colleague-card-status-pill-fg:#000000e6;--cb-colleague-card-chat-bg:#f7f7f7;--cb-text-link:#006ab1;--cb-success-color:#28b894;--cb-button-primary:#6c4dff;--cb-mermaid-toggle-active:var(--cb-vscode-button-background,#007acc);--cb-text-primary:#333;--cb-text-secondary:#666;--cb-text-tertiary:gray;--cb-text-muted:#1a1a1a99;--cb-text-highlight:#266fe8;--cb-text-disabled:#0000004d;--cb-text-quaternary:#00000073;--cb-text-error-active:#b42c3f;--cb-button-danger-secondary-fg:#b42c3f;--cb-text-link-hover:#0e58a0;--cb-text-solid:#000;--cb-bg-primary-hover:#ebebeb;--cb-bg-primary-active:#f2f2f2;--cb-border-secondary:#ebebeb;--cb-shadow-popover:0 6px 24px 0 #0000000a, 0 4px 6px 0 #0000000a;--cb-shadow-message-toolbar:0px 4px 12px 0px #0000000a, 0px 3px 6px 0px #0000000a;--cb-border-drag-indicator:#1470b4;--cb-border-radius-xl:16px;--cb-border-radius-2xl:24px;--cb-input-foreground:var(--cb-text-primary);--cb-input-background:var(--cb-vscode-input-background);--cb-input-border-color:#00000026;--cb-input-focus-border-color:#00000080;--cb-input-placeholder:var(--cb-text-secondary);--cb-input-button-foreground:#3Line truncated
|
||||
:where(.yc-created-expert-source) .cb-button{cursor:pointer;appearance:none;white-space:nowrap;user-select:none;border:none;border-radius:4px;outline:none;justify-content:center;align-items:center;gap:6px;font-weight:500;text-decoration:none;transition:all .2s;display:inline-flex;position:relative}
|
||||
:where(.yc-created-expert-source) .cb-button:disabled{opacity:.5;cursor:not-allowed;pointer-events:none}
|
||||
:where(.yc-created-expert-source) .cb-button:focus-visible{outline:2px solid var(--cb-focus-border,var(--vscode-focusBorder));outline-offset:2px}
|
||||
:where(.yc-created-expert-source) .cb-button--small{height:22px;padding:2px 12px;font-size:12px;line-height:18px}
|
||||
:where(.yc-created-expert-source) .cb-button--medium{height:32px;padding:6px 16px;font-size:13px;line-height:20px}
|
||||
:where(.yc-created-expert-source) .cb-button--large{height:40px;padding:8px 20px;font-size:14px;line-height:22px}
|
||||
:where(.yc-created-expert-source) .cb-button--primary{background-color:var(--cb-button-primary,#6c4dff);color:var(--cb-button-primary-foreground,#fff)}
|
||||
:where(.yc-created-expert-source) .cb-button--primary:hover:not(:disabled),:where(.yc-created-expert-source) .cb-button--primary:focus:not(:disabled),:where(.yc-created-expert-source) .cb-button--primary:active:not(:disabled){filter:brightness(1.05)}
|
||||
:where(.yc-created-expert-source) .cb-button--secondary{background-color:var(--cb-input-button-background,#313248);color:var(--cb-input-button-foreground,#d2d3e0)}
|
||||
:where(.yc-created-expert-source) .cb-button--secondary:hover:not(:disabled),:where(.yc-created-expert-source) .cb-button--secondary:focus-visible:not(:disabled),:where(.yc-created-expert-source) .cb-button--secondary:active:not(:disabled){background-color:var(--cb-dropdown-item-hover-bg-color)}
|
||||
:where(.yc-created-expert-source) .cb-button--outline{border:1px solid var(--cb-input-border-color,#606060)!important;color:var(--cb-text-primary,#a0a0a0)!important;background:0 0!important}
|
||||
:where(.yc-created-expert-source) .cb-button--outline:hover:not(:disabled){background-color:var(--cb-dropdown-item-hover-bg-color)!important}
|
||||
:where(.yc-created-expert-source) .cb-button--ghost{color:var(--cb-button-ghost-foreground,var(--vscode-foreground));background:0 0;border:1px solid #0000}
|
||||
:where(.yc-created-expert-source) .cb-button--ghost:hover:not(:disabled){background:var(--cb-button-ghost-hover-background,var(--cb-icon-button-hover-background,#ffffff1a))}
|
||||
:where(.yc-created-expert-source) .cb-button--ghost:active:not(:disabled){background:var(--cb-button-ghost-hover-background,var(--cb-icon-button-hover-background,#ffffff1a));filter:brightness(.9)}
|
||||
:where(.yc-created-expert-source) .cb-button--danger{background:var(--cb-button-danger-background,var(--vscode-errorForeground));color:var(--cb-button-danger-foreground,var(--vscode-errorBackground,#fff));border:1px solid var(--cb-button-danger-border,transparent)}
|
||||
:where(.yc-created-expert-source) .cb-button--danger:hover:not(:disabled){background:var(--cb-button-danger-hover-background,var(--vscode-errorForeground));opacity:.9}
|
||||
:where(.yc-created-expert-source) .cb-button--danger:active:not(:disabled){transform:translateY(1px)}
|
||||
:where(.yc-created-expert-source) .cb-button--disabled{opacity:.5;cursor:not-allowed;pointer-events:none}
|
||||
:where(.yc-created-expert-source) .cb-button--loading{pointer-events:none}
|
||||
:where(.yc-created-expert-source) .cb-button--loading .cb-button__content{opacity:.6}
|
||||
:where(.yc-created-expert-source) .cb-button--icon-only{width:var(--cb-button-icon-size,32px);height:var(--cb-button-icon-size,32px);padding:0}
|
||||
:where(.yc-created-expert-source) .cb-button--icon-only.cb-button--small{width:22px;height:22px}
|
||||
:where(.yc-created-expert-source) .cb-button--icon-only.cb-button--large{width:40px;height:40px}
|
||||
:where(.yc-created-expert-source) .cb-button--full-width{width:100%}
|
||||
:where(.yc-created-expert-source) .cb-button__content{align-items:center;gap:6px;display:flex}
|
||||
:where(.yc-created-expert-source) .cb-button__icon{flex-shrink:0;justify-content:center;align-items:center;width:16px;height:16px;display:flex}
|
||||
:where(.yc-created-expert-source) .cb-button__icon svg{width:100%;height:100%}
|
||||
:where(.yc-created-expert-source) .cb-button__loading{border:2px solid;border-top-color:#0000;border-radius:50%;width:14px;height:14px;animation:1s linear infinite cb-button-spin;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}
|
||||
@keyframes cb-button-spin{0%{transform:translate(-50%,-50%)rotate(0)}to{transform:translate(-50%,-50%)rotate(360deg)}}
|
||||
:where(.yc-created-expert-source) ._dialogOverlay_peiew_7{z-index:var(--cb-z-index-modal-confirm,100001);background:#00000080;justify-content:center;align-items:center;width:100vw;padding:0 24px;display:flex;position:fixed;inset:0}
|
||||
:where(.yc-created-expert-source) ._dialogOverlay_peiew_7[data-theme=light]{background:#0000004d}
|
||||
:where(.yc-created-expert-source) ._dialogOverlay_peiew_7[data-theme=light] ._dialogContainer_peiew_21{box-shadow:0 18px 42px #0000002e}
|
||||
:where(.yc-created-expert-source) ._dialogOverlay_peiew_7[data-appearance=workbuddy]{background:var(--cb-dialog-mask,#0009)!important}
|
||||
:where(.yc-created-expert-source) ._dialogOverlayContained_peiew_28{width:100%;height:100%;position:absolute}
|
||||
:where(.yc-created-expert-source) ._dialogContainer_peiew_21{border:1px solid color-mix(in srgb, var(--cb-vscode-widget-border,var(--vscode-widget-border,#313248)) 72%, transparent);background-color:var(--cb-vscode-editor-background,var(--vscode-editor-background,#1e1e1e));border-radius:12px;flex-grow:1;width:100%;max-width:400px;padding:18px;position:relative;box-shadow:0 18px 42px #00000059}
|
||||
:where(.yc-created-expert-source) .claw-qr-scan-dialog ._dialogContainer_peiew_21{box-sizing:border-box;flex-direction:column;flex-grow:0;width:360px;max-width:360px;height:424px;padding:12px 24px 24px;display:flex}
|
||||
:where(.yc-created-expert-source) .claw-qr-scan-dialog ._dialogContent_peiew_56{flex-direction:column;flex:auto;min-height:0;margin-bottom:0;display:flex}
|
||||
:where(.yc-created-expert-source) ._dialogClose_peiew_64{min-width:24px;min-height:24px;position:absolute;top:12px;right:12px}
|
||||
:where(.yc-created-expert-source) ._dialogHeader_peiew_72{color:var(--cb-vscode-foreground,var(--vscode-foreground,#d2d3e0));justify-content:space-between;align-items:center;margin-bottom:8px;margin-right:32px;font-size:16px;font-weight:650;line-height:22px;display:flex}
|
||||
:where(.yc-created-expert-source) ._dialogContent_peiew_56{color:var(--cb-vscode-descriptionForeground,var(--vscode-descriptionForeground,#858699));margin-bottom:20px;font-size:12px;font-weight:400;line-height:18px}
|
||||
:where(.yc-created-expert-source) ._dialogDetailContent_peiew_92{background-color:color-mix(in srgb, var(--cb-vscode-editor-background,var(--vscode-editor-background,#1e1e1e)) 92%, #64748b 8%);color:var(--cb-vscode-foreground,var(--vscode-foreground,#d2d3e0));border:0;border-radius:8px;max-height:180px;margin-bottom:16px;padding:12px;overflow-y:auto}
|
||||
:where(.yc-created-expert-source) ._dialogDetailContent_peiew_92::-webkit-scrollbar{width:6px}
|
||||
:where(.yc-created-expert-source) ._dialogDetailContent_peiew_92::-webkit-scrollbar-track{background:0 0}
|
||||
:where(.yc-created-expert-source) ._dialogDetailContent_peiew_92::-webkit-scrollbar-thumb{background-color:var(--cb-vscode-scrollbarSlider-background,#fff3);border-radius:3px}
|
||||
:where(.yc-created-expert-source) ._dialogDetailContent_peiew_92::-webkit-scrollbar-thumb:hover{background-color:var(--cb-vscode-scrollbarSlider-hoverBackground,#ffffff4d)}
|
||||
:where(.yc-created-expert-source) ._dialogFooter_peiew_116{justify-content:flex-end;align-items:center;gap:8px;display:flex}
|
||||
:where(.yc-created-expert-source) ._dialogFooterAlignStart_peiew_123{justify-content:flex-start}
|
||||
:where(.yc-created-expert-source) ._dialogFooterAlignCenter_peiew_127{justify-content:center}
|
||||
:where(.yc-created-expert-source) ._footerLeftSlot_peiew_131{flex:1;min-width:0}
|
||||
:where(.yc-created-expert-source) ._footerButtons_peiew_136{flex-shrink:0;justify-content:flex-end;align-items:center;gap:8px;display:flex}
|
||||
:where(.yc-created-expert-source) ._footerButtons_peiew_136 .cb-button{border-radius:6px;min-width:68px;height:32px;padding:0 14px;font-size:12px;font-weight:400;line-height:18px}
|
||||
:where(.yc-created-expert-source) ._cancelButtonStatic_peiew_153,:where(.yc-created-expert-source) ._cancelButtonStatic_peiew_153:hover:not(:disabled),:where(.yc-created-expert-source) ._cancelButtonStatic_peiew_153:focus-visible:not(:disabled),:where(.yc-created-expert-source) ._cancelButtonStatic_peiew_153:active:not(:disabled){background-color:var(--cb-input-button-background,#313248);color:var(--cb-input-button-foreground,#d2d3e0);filter:none;transform:none}
|
||||
:where(.yc-created-expert-source) ._footerButtonsAlignStart_peiew_160{justify-content:flex-start}
|
||||
:where(.yc-created-expert-source) ._footerButtonsAlignCenter_peiew_164{justify-content:center}
|
||||
:where(.yc-created-expert-source) ._dontAskAgain_peiew_168{cursor:pointer;user-select:none;flex-shrink:0;align-items:center;gap:6px;display:flex}
|
||||
:where(.yc-created-expert-source) ._checkbox_peiew_177{cursor:pointer;width:14px;height:14px;accent-color:var(--cb-vscode-button-background,var(--vscode-button-background,#4a9eff));margin:0}
|
||||
:where(.yc-created-expert-source) ._checkboxLabel_peiew_185{color:var(--cb-vscode-descriptionForeground,var(--vscode-descriptionForeground,#858699));font-size:12px}
|
||||
:where(.yc-created-expert-source) ._dialogContainerWorkbuddy_peiew_193{border:.5px solid var(--cb-popover-border,#e5e5e5);background-color:var(--cb-dropdown-bg-color,#fff);max-width:480px;box-shadow:var(--cb-shadow-popover,0 6px 12px -4px #0000000a, 0 8px 48px -4px #00000014);border-radius:24px;flex-grow:0;padding:24px}
|
||||
:where(.yc-created-expert-source) ._dialogContainerWorkbuddy_peiew_193 ._dialogHeader_peiew_72{color:var(--cb-text-primary);margin-bottom:8px;margin-right:0;font-weight:600}
|
||||
:where(.yc-created-expert-source) ._dialogContainerWorkbuddy_peiew_193 ._dialogContent_peiew_56{color:var(--cb-text-secondary);margin-bottom:24px;font-size:14px;font-weight:400;line-height:22px}
|
||||
:where(.yc-created-expert-source) ._dialogContainerWorkbuddy_peiew_193 ._dialogFooter_peiew_116,:where(.yc-created-expert-source) ._dialogContainerWorkbuddy_peiew_193 ._footerButtons_peiew_136{gap:12px}
|
||||
:where(.yc-created-expert-source) ._dialogContainerWorkbuddy_peiew_193 ._footerButtons_peiew_136 .cb-button{border-radius:8px;min-width:0;height:32px;padding:0 12px;font-size:14px;font-weight:600;line-height:22px}
|
||||
:where(.yc-created-expert-source) ._dialogContainerWorkbuddy_peiew_193 ._footerButtons_peiew_136 .cb-button:disabled{opacity:1}
|
||||
:where(.yc-created-expert-source) ._dialogContainerWorkbuddy_peiew_193 ._cancelButtonStatic_peiew_153,:where(.yc-created-expert-source) ._dialogContainerWorkbuddy_peiew_193 ._cancelButtonStatic_peiew_153:hover:not(:disabled),:where(.yc-created-expert-source) ._dialogContainerWorkbuddy_peiew_193 ._cancelButtonStatic_peiew_153:focus-visible:not(:disabled),:where(.yc-created-expert-source) ._dialogContainerWorkbuddy_peiew_193 ._cancelButtonStatic_peiew_153:active:not(:disabled){background-color:var(--cb-button-grey-bg,#ebebeb);color:var(--cb-button-grey-fg,#000000e6)}
|
||||
:where(.yc-created-expert-source) .cb-snapshot-wrapper .cb-button{cursor:pointer;appearance:none;white-space:nowrap;user-select:none;border:none;border-radius:4px;outline:none;justify-content:center;align-items:center;font-weight:500;transition:all .2s;display:inline-flex}
|
||||
:where(.yc-created-expert-source) .cb-snapshot-wrapper .cb-button--small{height:22px;padding:2px 12px;font-size:12px;line-height:18px}
|
||||
:where(.yc-created-expert-source) .cb-snapshot-wrapper .cb-button--primary{background-color:var(--cb-vscode-button-background,#0e639c);color:#fff}
|
||||
:where(.yc-created-expert-source) .cb-snapshot-wrapper .cb-button--primary:hover,:where(.yc-created-expert-source) .cb-snapshot-wrapper .cb-button--primary:focus,:where(.yc-created-expert-source) .cb-snapshot-wrapper .cb-button--primary:active{background-color:var(--cb-vscode-button-hoverBackground)}
|
||||
:where(.yc-created-expert-source) .cb-snapshot-wrapper .cb-button--outline{border:1px solid var(--cb-vscode-panel-border,#3c3c3c)!important;color:var(--cb-vscode-descriptionForeground)!important;background:0 0!important}
|
||||
:where(.yc-created-expert-source) .cb-snapshot-wrapper .cb-button--outline:hover{background-color:var(--cb-vscode-list-hoverBackground)!important}
|
||||
:where(.yc-created-expert-source) .cb-snapshot-wrapper .cb-button:not(.cb-snapshot-wrapper .cb-button--primary):not(.cb-snapshot-wrapper .cb-button--outline){background-color:var(--cb-button-secondary-background,#fff);color:var(--cb-button-secondary-foreground,#4c4f6b)}
|
||||
:where(.yc-created-expert-source) .cb-snapshot-wrapper .cb-button:not(.cb-snapshot-wrapper .cb-button--primary):not(.cb-snapshot-wrapper .cb-button--outline):hover,:where(.yc-created-expert-source) .cb-snapshot-wrapper .cb-button:not(.cb-snapshot-wrapper .cb-button--primary):not(.cb-snapshot-wrapper .cb-button--outline):focus,:where(.yc-created-expert-source) .cb-snapshot-wrapper .cb-button:not(.cb-snapshot-wrapper .cb-button--primary):not(.cb-snapshot-wrapper .cb-button--outline):active{background-color:var(--cb-button-secondary-background,#606060)}
|
||||
:where(.yc-created-expert-source) .wb-popover.claw-workspace-title-card-popover{width:calc(var(--wb-spacing-10) * 4 + var(--wb-spacing-6));margin-left:calc(-1 * (var(--wb-spacing-6) + var(--wb-spacing-1)));padding:var(--wb-spacing-0);border:1px solid var(--wb-border-subtle);border-radius:calc(var(--wb-radius-2xl) + var(--wb-spacing-3));background:linear-gradient(135deg, color-mix(in srgb, var(--wb-status-success) 8%, var(--wb-bg-primary)) 0%, var(--wb-bg-primary) 21%, var(--wb-bg-primary) 100%);color:var(--wb-color-text-primary);box-shadow:var(--wb-shadow-lg);overflow:visible}
|
||||
:where(.yc-created-expert-source) .wb-popover.claw-workspace-title-card-popover:before{content:"";top:calc(-1 * (var(--wb-spacing-3) + var(--wb-spacing-1)));left:calc(var(--wb-spacing-8) + var(--wb-spacing-1));width:var(--wb-spacing-6);height:calc(var(--wb-spacing-3) + var(--wb-spacing-1));background:linear-gradient(135deg, color-mix(in srgb, var(--wb-status-success) 8%, var(--wb-bg-primary)) 0%, var(--wb-bg-primary) 100%);clip-path:polygon(50% 0,100% 100%,0 100%);position:absolute}
|
||||
:where(.yc-created-expert-source) .custom-menu-dialog__context-menu.wb-popover-compact.cb-context-menu{width:max-content;min-width:160px;max-width:min(260px,100vw - 20px);padding:var(--wb-spacing-3,8px);border-radius:var(--wb-radius-2xl,16px);background:var(--wb-bg-popover);box-shadow:var(--wb-shadow-popover);color:var(--wb-color-text-primary);font-size:14px;line-height:22px;overflow:hidden;border:.5px solid var(--wb-border-subtle)!important}
|
||||
:where(.yc-created-expert-source) .custom-menu-dialog__context-menu.wb-popover-compact.cb-context-menu .cb-context-menu-item{box-sizing:border-box;width:100%;height:32px;padding:0 var(--wb-spacing-3,8px);border-radius:var(--wb-radius-lg,8px);color:var(--wb-color-text-primary);text-overflow:ellipsis;border:1px solid #0000;transition:background-color .15s;overflow:hidden}
|
||||
:where(.yc-created-expert-source) .custom-menu-dialog__context-menu.wb-popover-compact.cb-context-menu .cb-context-menu-item:hover:not(.disabled):not(.separator){background:var(--wb-bg-hover);color:var(--wb-color-text-primary)}
|
||||
:where(.yc-created-expert-source) .custom-menu-dialog__context-menu.wb-popover-compact.cb-context-menu .cb-context-menu-item:hover:not(.disabled):not(.separator) .cb-context-menu-item-shortcut{color:var(--wb-color-text-secondary)}
|
||||
:where(.yc-created-expert-source) .custom-menu-dialog__context-menu.wb-popover-compact.cb-context-menu .cb-context-menu-item.disabled{color:var(--wb-color-text-disabled)}
|
||||
:where(.yc-created-expert-source) .custom-menu-dialog__context-menu.wb-popover-compact.cb-context-menu .cb-context-menu-item.separator,:where(.yc-created-expert-source) .custom-menu-dialog__context-menu.wb-popover-compact.cb-context-menu .cb-context-menu-item.separator:hover{background-color:var(--wb-border-default);border:0;border-radius:0;height:1px}
|
||||
:where(.yc-created-expert-source) .custom-menu-dialog__context-menu.wb-popover-compact.cb-context-menu .cb-context-menu-item-label{text-overflow:ellipsis;min-width:0;overflow:hidden}
|
||||
:where(.yc-created-expert-source) .custom-menu-dialog__context-menu.wb-popover-compact.cb-context-menu .cb-context-menu-item-icon{color:currentColor;justify-content:center;width:16px;height:16px}
|
||||
:where(.yc-created-expert-source) .custom-menu-dialog__context-menu.wb-popover-compact.cb-context-menu .cb-context-menu-item-shortcut{color:var(--wb-color-text-secondary);flex-shrink:0;font-size:12px;line-height:18px}
|
||||
:where(.yc-created-expert-source) .wb-popover.task-filter-popover{background:var(--wb-bg-card);border:.5px solid var(--wb-border-subtle);box-shadow:var(--wb-shadow-popover);border-radius:16px;width:240px;padding:8px}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-dropdown.conversation-list-more-dropdown,:where(.yc-created-expert-source) .wb-popover.wb-dropdown.conversation-list-expert-dropdown{background:var(--wb-bg-card);border:.5px solid var(--wb-color-border-solid);box-shadow:var(--wb-shadow-popup-medium);border-radius:16px}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-dropdown.conversation-list-more-dropdown .wb-dropdown__list,:where(.yc-created-expert-source) .wb-popover.wb-dropdown.conversation-list-expert-dropdown .wb-dropdown__list{width:auto;min-width:144px}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-dropdown.conversation-list-more-dropdown .wb-dropdown__item,:where(.yc-created-expert-source) .wb-popover.wb-dropdown.conversation-list-expert-dropdown .wb-dropdown__item{white-space:nowrap}
|
||||
:where(.yc-created-expert-source) body.wb-mobile-layout .workspace-more-popover,:where(.yc-created-expert-source) body.wb-mobile-layout .wb-popover.task-filter-popover,:where(.yc-created-expert-source) body.wb-mobile-layout .wb-popover.wb-dropdown.conversation-list-more-dropdown,:where(.yc-created-expert-source) body.wb-mobile-layout .wb-popover.wb-dropdown.conversation-list-expert-dropdown,:where(.yc-created-expert-source) body.wb-mobile-layout .conversation-context-menu{box-sizing:border-box;max-width:calc(var(--wb-mobile-container-width,100vw) - var(--wb-spacing-5))}
|
||||
:where(.yc-created-expert-source) body.wb-mobile-layout .wb-popover.task-filter-popover{width:min(15rem, calc(var(--wb-mobile-container-width,100vw) - var(--wb-spacing-5)))}
|
||||
:where(.yc-created-expert-source) body.wb-mobile-layout .wb-popover.wb-dropdown.conversation-list-more-dropdown,:where(.yc-created-expert-source) body.wb-mobile-layout .wb-popover.wb-dropdown.conversation-list-expert-dropdown{right:calc(var(--wb-mobile-container-right,0px) + var(--wb-spacing-3));width:min(10rem, calc(var(--wb-mobile-container-width,100vw) - var(--wb-spacing-5)));overflow:hidden;left:auto!important}
|
||||
:where(.yc-created-expert-source) body.wb-mobile-layout .wb-popover.wb-dropdown.conversation-list-more-dropdown .wb-dropdown__list,:where(.yc-created-expert-source) body.wb-mobile-layout .wb-popover.wb-dropdown.conversation-list-expert-dropdown .wb-dropdown__list{width:auto;min-width:0;max-width:100%}
|
||||
:where(.yc-created-expert-source) body.wb-mobile-layout .wb-popover.wb-dropdown.conversation-list-more-dropdown .wb-dropdown__item,:where(.yc-created-expert-source) body.wb-mobile-layout .wb-popover.wb-dropdown.conversation-list-expert-dropdown .wb-dropdown__item{min-width:0}
|
||||
:where(.yc-created-expert-source) body.wb-mobile-layout .wb-popover.wb-dropdown.conversation-list-more-dropdown .wb-dropdown__label,:where(.yc-created-expert-source) body.wb-mobile-layout .wb-popover.wb-dropdown.conversation-list-expert-dropdown .wb-dropdown__label{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}
|
||||
:where(.yc-created-expert-source) .login-view__button.cb-button{width:180px;min-width:180px;height:38px;color:var(--wb-button-primary-fg);background-color:var(--wb-button-primary-bg);box-shadow:none;border:none;border-radius:999px;margin:0 auto;padding:0;font-size:14px;font-weight:500;line-height:30px}
|
||||
:where(.yc-created-expert-source) .login-view__button.cb-button:hover:not(:disabled),:where(.yc-created-expert-source) .login-view__button.cb-button:focus:not(:disabled){color:var(--wb-button-primary-fg);background-color:var(--wb-button-primary-bg-hover);filter:none}
|
||||
:where(.yc-created-expert-source) .login-view__button.cb-button:active:not(:disabled){color:var(--wb-button-primary-fg);background-color:var(--wb-button-primary-bg-active);filter:none}
|
||||
:where(.yc-created-expert-source) .login-view__button.cb-button:disabled{color:var(--wb-button-primary-fg-disabled);background-color:var(--wb-button-primary-bg-disabled);opacity:1}
|
||||
:where(.yc-created-expert-source) .ec-finance-risk-confirm-overlay[data-theme=dark] [style*=--cb-button-dark-background]{color:var(--wb-button-primary-fg)!important}
|
||||
:where(.yc-created-expert-source) .ec-finance-risk-confirm-overlay[data-theme=dark] [style*=--cb-button-dark-background]:hover:not(:disabled),:where(.yc-created-expert-source) .ec-finance-risk-confirm-overlay[data-theme=dark] [style*=--cb-button-dark-background]:active:not(:disabled){background-color:var(--wb-button-primary-bg-active)!important}
|
||||
:where(.yc-created-expert-source) .ec-my-panel{box-sizing:border-box;min-width:0;padding:20px var(--ec-content-gutter);flex-direction:column;flex:1;align-items:stretch;display:flex}
|
||||
:where(.yc-created-expert-source) .ec-my-sub-tabs{background:var(--wb-bg-tertiary);border-radius:12px;align-items:center;gap:4px;margin-bottom:24px;padding:4px;display:inline-flex}
|
||||
:where(.yc-created-expert-source) .ec-my-sub-tab{color:var(--wb-color-text-tertiary);cursor:pointer;letter-spacing:-.1px;background:0 0;border:none;border-radius:9px;padding:8px 22px;font-size:14px;font-weight:600;transition:all .25s cubic-bezier(.4,0,.2,1);position:relative}
|
||||
:where(.yc-created-expert-source) .ec-my-sub-tab:hover{color:var(--wb-color-text-secondary)}
|
||||
:where(.yc-created-expert-source) .ec-my-sub-tab.is-active{color:var(--wb-color-text-primary);background:var(--wb-bg-card);font-weight:700;box-shadow:0 1px 4px #00000014}
|
||||
:where(.yc-created-expert-source) .ec-my-recent-hint{color:var(--wb-color-text-tertiary);background:var(--wb-brand-accent-bg);border-radius:10px;align-items:center;gap:6px;margin-bottom:18px;padding:10px 16px;font-size:12px;font-weight:500;display:flex}
|
||||
:where(.yc-created-expert-source) .ec-my-recent-hint svg{width:14px;height:14px;color:var(--wb-brand-accent);flex-shrink:0}
|
||||
:where(.yc-created-expert-source) .ec-my-created-toolbar{justify-content:space-between;align-items:center;gap:var(--wb-spacing-4);margin-bottom:var(--wb-spacing-5);display:flex}
|
||||
:where(.yc-created-expert-source) .ec-my-created-tabs{min-width:0}
|
||||
:where(.yc-created-expert-source) .ec-my-created-tab-label{color:var(--wb-palette-black-50);font-family:PingFang SC;font-weight:var(--wb-font-weight-500)}
|
||||
:where(.yc-created-expert-source) .ec-my-created-tabs [role=tab][aria-selected=true] .ec-my-created-tab-label{color:var(--wb-palette-black-90);font-weight:var(--wb-font-weight-600)}
|
||||
:where(.yc-created-expert-source) .ec-my-created-tab-count{color:var(--wb-palette-black-50);font-family:PingFang SC;font-size:12px;font-weight:var(--wb-font-weight-400);background:#0000000f;border-radius:10px;flex-shrink:0;justify-content:center;align-items:center;padding:0 6px;font-style:normal;line-height:20px;text-decoration-line:none;display:flex}
|
||||
:where(.yc-created-expert-source) .ec-my-created-tabs [role=tab][aria-selected=true] .ec-my-created-tab-count{color:var(--wb-palette-black-90)}
|
||||
:where(.yc-created-expert-source) .expert-center-dark .ec-my-created-tab-label,:where(.yc-created-expert-source) .expert-center-dark .ec-my-created-tab-count{color:var(--wb-palette-white-50)}
|
||||
:where(.yc-created-expert-source) .expert-center-dark .ec-my-created-tabs [role=tab][aria-selected=true] .ec-my-created-tab-label,:where(.yc-created-expert-source) .expert-center-dark .ec-my-created-tabs [role=tab][aria-selected=true] .ec-my-created-tab-count{color:var(--wb-palette-white-90)}
|
||||
:where(.yc-created-expert-source) .expert-center-dark .ec-my-created-tab-count{background:#ffffff14}
|
||||
:where(.yc-created-expert-source) .ec-my-cloud-upload-all{height:28px;color:var(--wb-color-text-tertiary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex-shrink:0;justify-content:center;align-items:center;gap:4px;padding:0 6px;font-size:13px;font-weight:400;line-height:20px;transition:background .15s,color .15s;display:inline-flex}
|
||||
:where(.yc-created-expert-source) .ec-my-cloud-upload-all:hover:not(:disabled){background:var(--wb-bg-hover);color:var(--wb-color-text-primary)}
|
||||
:where(.yc-created-expert-source) .ec-my-cloud-upload-all:disabled{cursor:not-allowed;opacity:.6}
|
||||
:where(.yc-created-expert-source) .ec-my-cloud-upload-all__icon{flex-shrink:0;width:16px;height:16px}
|
||||
:where(.yc-created-expert-source) .ec-my-toolbar-right{align-items:center;gap:var(--wb-spacing-2,8px);min-width:0;margin-left:auto;display:flex}
|
||||
:where(.yc-created-expert-source) .ec-my-toolbar-right .ec-search-wrapper{margin-left:0}
|
||||
@media (width<=540px){:where(.yc-created-expert-source) .ec-my-toolbar-right{flex-basis:100%;width:100%}}
|
||||
:where(.yc-created-expert-source) .ec-my-mode-row{margin-bottom:var(--wb-spacing-3,12px);justify-content:flex-end;display:flex}
|
||||
:where(.yc-created-expert-source) .ec-purchased-card--expired .ec-cr-more-action{top:0;right:calc(var(--wb-spacing-12) + var(--wb-spacing-4));z-index:3;position:absolute}
|
||||
@media (width<=540px){:where(.yc-created-expert-source) .ec-my-created-toolbar{flex-wrap:wrap}}
|
||||
:where(.yc-created-expert-source) .ec-my-grid{gap:var(--wb-spacing-5);grid-template-columns:repeat(auto-fill,minmax(280px,1fr));align-content:start;display:grid}
|
||||
@media (width<=1279px){:where(.yc-created-expert-source) .ec-my-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
@media (width<=767px){:where(.yc-created-expert-source) .ec-my-grid{gap:var(--wb-spacing-5);grid-template-columns:repeat(2,1fr)}}
|
||||
@media (width<=575px){:where(.yc-created-expert-source) .ec-my-grid{grid-template-columns:1fr}}
|
||||
:where(.yc-created-expert-source) .ec-my-recent-empty{flex-direction:column;align-items:center;padding:15% 40px 60px;display:flex}
|
||||
:where(.yc-created-expert-source) .ec-my-recent-empty-icon{background:linear-gradient(145deg,#f0edff,#e5deff);border-radius:20px;justify-content:center;align-items:center;width:72px;height:72px;margin-bottom:16px;font-size:32px;display:flex;box-shadow:0 4px 16px #7c5cfc1a}
|
||||
:where(.yc-created-expert-source) .expert-center-dark .ec-my-recent-empty-icon{background:linear-gradient(145deg,#2d2a3e,#3a3550)}
|
||||
:where(.yc-created-expert-source) .ec-my-recent-empty-title{color:var(--wb-color-text-secondary);margin:0 0 4px;font-size:14px;font-weight:600}
|
||||
:where(.yc-created-expert-source) .ec-my-recent-empty-hint{color:var(--wb-color-text-tertiary);margin:0;font-size:12px;font-weight:400}
|
||||
:where(.yc-created-expert-source) .ec-cr-card{position:relative}
|
||||
:where(.yc-created-expert-source) .ec-cr-card:hover .ec-card-summon-btn--top:before,:where(.yc-created-expert-source) .ec-cr-card:focus-within .ec-card-summon-btn--top:before{background:linear-gradient(to right, transparent, color-mix(in srgb, var(--wb-palette-black-100) 5%, var(--wb-bg-card)))}
|
||||
:where(.yc-created-expert-source) .ec-cr-card:hover .ec-card-summon-btn--top,:where(.yc-created-expert-source) .ec-cr-card:focus-within .ec-card-summon-btn--top{opacity:1;pointer-events:auto}
|
||||
:where(.yc-created-expert-source) .ec-cr-card .ec-card-summon-btn--top{width:calc(var(--wb-spacing-12) + var(--wb-spacing-2));box-sizing:border-box}
|
||||
:where(.yc-created-expert-source) .ec-cr-cloud-icon{top:var(--wb-spacing-6);right:calc(var(--wb-spacing-12) + var(--wb-spacing-8) + var(--wb-spacing-8));z-index:3;width:20px;height:20px;color:var(--wb-color-text-primary);cursor:pointer;pointer-events:auto;background:0 0;border:0;border-radius:6px;justify-content:center;align-items:center;padding:2px;display:inline-flex;position:absolute}
|
||||
:where(.yc-created-expert-source) .ec-cr-cloud-icon.ec-cr-cloud-icon--top-action{position:static}
|
||||
:where(.yc-created-expert-source) .ec-cr-cloud-icon:hover:not(.is-loading):not(.is-disabled){background:var(--wb-bg-hover)}
|
||||
:where(.yc-created-expert-source) .ec-cr-cloud-icon.is-disabled{cursor:default}
|
||||
:where(.yc-created-expert-source) .ec-cr-cloud-icon.is-loading{cursor:not-allowed;opacity:.72}
|
||||
:where(.yc-created-expert-source) .ec-cr-cloud-icon__svg{width:16px;height:16px;display:block}
|
||||
:where(.yc-created-expert-source) .ec-cr-cloud-icon__spinner{border:2px solid;border-top-color:#0000;border-radius:50%;width:12px;height:12px;animation:.8s linear infinite ec-cloud-action-spin}
|
||||
:where(.yc-created-expert-source) .ec-cr-cloud-icon--local_ahead,:where(.yc-created-expert-source) .ec-cr-cloud-icon--local_only,:where(.yc-created-expert-source) .ec-cr-cloud-icon--checking,:where(.yc-created-expert-source) .ec-cr-cloud-icon--failed,:where(.yc-created-expert-source) .ec-cr-cloud-icon--invalid,:where(.yc-created-expert-source) .ec-cr-cloud-icon--cloud_ahead,:where(.yc-created-expert-source) .ec-cr-cloud-icon--cloud_only,:where(.yc-created-expert-source) .ec-cr-cloud-icon--both{color:var(--wb-color-text-primary)}
|
||||
:where(.yc-created-expert-source) .ec-cr-card:hover .ec-cr-more-btn--top,:where(.yc-created-expert-source) .ec-cr-card:focus-within .ec-cr-more-btn--top,:where(.yc-created-expert-source) .ec-cr-card .ec-cr-more-btn--top.is-visible{opacity:1;pointer-events:auto}
|
||||
:where(.yc-created-expert-source) .ec-cr-more-action{flex-shrink:0;position:relative}
|
||||
:where(.yc-created-expert-source) .ec-cr-more-btn{border-radius:var(--wb-radius-lg);cursor:pointer;width:24px;height:24px;color:var(--wb-color-text-secondary);opacity:0;pointer-events:none;background:0 0;border:none;flex-shrink:0;justify-content:center;align-items:center;padding:0;transition:opacity .15s,background .15s,color .15s;display:flex;position:relative}
|
||||
:where(.yc-created-expert-source) .ec-cr-more-btn:before{content:"";border-radius:inherit;background:0 0;transition:background .15s;position:absolute;inset:0}
|
||||
:where(.yc-created-expert-source) .ec-cr-more-btn svg{position:relative}
|
||||
:where(.yc-created-expert-source) .ec-cr-more-btn:hover,:where(.yc-created-expert-source) .ec-cr-more-btn:focus-visible,:where(.yc-created-expert-source) .ec-cr-more-btn.is-active{color:var(--wb-color-text-primary)}
|
||||
:where(.yc-created-expert-source) .ec-cr-more-btn:hover:before,:where(.yc-created-expert-source) .ec-cr-more-btn:focus-visible:before,:where(.yc-created-expert-source) .ec-cr-more-btn.is-active:before{background:var(--wb-bg-hover)}
|
||||
:where(.yc-created-expert-source) .ec-cr-status-badge{white-space:nowrap;border-radius:6px;flex-shrink:0;padding:2px 8px;font-size:10px;font-weight:600}
|
||||
:where(.yc-created-expert-source) .ec-cr-status-badge.is-unpublished{background:var(--wb-bg-tertiary);color:var(--wb-color-text-tertiary)}
|
||||
:where(.yc-created-expert-source) .ec-cr-status-badge.is-reviewing{background:var(--wb-alert-warning-bg);color:var(--wb-badge-warning-fg)}
|
||||
:where(.yc-created-expert-source) .ec-cr-status-badge.is-published{background:var(--wb-alert-success-bg);color:var(--wb-alert-success-text)}
|
||||
:where(.yc-created-expert-source) .ec-cr-status-badge.is-rejected{background:var(--wb-bg-error-soft);color:var(--wb-alert-error-text)}
|
||||
:where(.yc-created-expert-source) .ec-cr-status-badge.is-new-version{background:var(--wb-alert-info-bg);color:var(--wb-alert-info-text)}
|
||||
:where(.yc-created-expert-source) .ec-my-created-empty{justify-content:center;align-items:center;gap:var(--wb-spacing-4);flex-direction:column;flex:1;display:flex}
|
||||
:where(.yc-created-expert-source) .ec-my-created-empty-icon{background-color:var(--wb-palette-black-20);width:64px;height:64px;-webkit-mask-image:var(--ec-empty-created-icon);mask-image:var(--ec-empty-created-icon);flex-shrink:0;display:block;mask-position:50%;mask-size:contain;mask-repeat:no-repeat}
|
||||
:where(.yc-created-expert-source) .ec-my-created-empty-title{color:var(--wb-palette-black-30);font-family:PingFang SC;font-size:var(--wb-font-body-size);font-weight:var(--wb-font-body-strong-weight);line-height:var(--wb-font-body-strong-line-height);margin:0}
|
||||
:where(.yc-created-expert-source) .expert-center-dark .ec-my-created-empty-icon{background-color:var(--wb-palette-white-20)}
|
||||
:where(.yc-created-expert-source) .expert-center-dark .ec-my-created-empty-title{color:var(--wb-palette-white-30)}
|
||||
:where(.yc-created-expert-source) .ec-my-empty{color:var(--wb-color-text-tertiary);flex-direction:column;justify-content:center;align-items:center;padding:80px 40px;display:flex}
|
||||
:where(.yc-created-expert-source) .ec-my-empty .ec-empty-icon{width:64px;height:64px;color:var(--wb-border-default);margin-bottom:18px}
|
||||
:where(.yc-created-expert-source) .ec-my-empty-title{color:var(--wb-color-text-secondary);margin:0;font-size:15px;font-weight:600}
|
||||
:where(.yc-created-expert-source) .ec-my-empty-hint{color:var(--wb-color-text-tertiary);margin:8px 0 0;font-size:13px}
|
||||
:where(.yc-created-expert-source) ._card_1hdcm_2:hover,:where(.yc-created-expert-source) ._card_1hdcm_2.ec-cr-card--menu-open{background:var(--wb-bg-card-hover);border-color:var(--wb-border-default);box-shadow:var(--wb-shadow-card-soft);transform:translateY(0)}
|
||||
:where(.yc-created-expert-source) ._titleActions_1hdcm_384 .ec-cr-more-action{display:none}
|
||||
:where(.yc-created-expert-source) ._card_1hdcm_2:hover ._titleActions_1hdcm_384 .ec-cr-more-action,:where(.yc-created-expert-source) ._card_1hdcm_2:focus-within ._titleActions_1hdcm_384 .ec-cr-more-action,:where(.yc-created-expert-source) ._card_1hdcm_2.ec-cr-card--menu-open ._titleActions_1hdcm_384 .ec-cr-more-action{display:block}
|
||||
:where(.yc-created-expert-source) ._card_1hdcm_2:hover ._summonBtnTop_1hdcm_441,:where(.yc-created-expert-source) ._card_1hdcm_2:focus-within ._summonBtnTop_1hdcm_441,:where(.yc-created-expert-source) ._card_1hdcm_2.ec-cr-card--menu-open ._summonBtnTop_1hdcm_441{display:inline-flex}
|
||||
:where(.yc-created-expert-source) .expert-center-dark ._card_1hdcm_2:hover,:where(.yc-created-expert-source) .expert-center-dark ._card_1hdcm_2:focus-within,:where(.yc-created-expert-source) .expert-center-dark ._card_1hdcm_2.ec-cr-card--menu-open{background:var(--wb-bg-card-hover);border-color:#0000;border-color:var(--wb-border-subtle)}
|
||||
:where(.yc-created-expert-source) .ec-cr-card ._titleRow_1hdcm_104{box-sizing:border-box;width:100%}
|
||||
:where(.yc-created-expert-source) .ec-cr-card ._summonBtnTop_1hdcm_441{width:calc(var(--wb-spacing-12) + var(--wb-spacing-2));box-sizing:border-box}
|
||||
:where(.yc-created-expert-source) .ec-cr-card ._summonBtnTop_1hdcm_441:before{width:var(--wb-spacing-3)}
|
||||
:where(.yc-created-expert-source) .ec-delete-confirm-content{color:var(--wb-color-text-secondary);flex-direction:column;gap:12px;font-size:14px;line-height:1.6;display:flex}
|
||||
:where(.yc-created-expert-source) .ec-delete-confirm-cloud-option{border:1px solid var(--wb-border-default);background:color-mix(in srgb, var(--wb-bg-card) 74%, var(--wb-bg-hover));color:var(--wb-color-text-primary);cursor:pointer;border-radius:10px;align-items:flex-start;gap:8px;padding:10px 12px;display:flex}
|
||||
:where(.yc-created-expert-source) .ec-delete-confirm-cloud-option input{flex-shrink:0;margin:3px 0 0}
|
||||
:where(.yc-created-expert-source) .ec-delete-confirm-cloud-option:has(input:disabled){cursor:not-allowed;opacity:.7}
|
||||
:where(.yc-created-expert-source) .wb-popover._branchPopover_jdvlk_328{background:var(--wb-palette-black-75);width:max-content;max-width:min(320px,100vw - 32px);box-shadow:none;box-sizing:border-box;border:none;border-radius:8px;padding:4px 8px}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-cascader-popover{padding:var(--wb-spacing-2) 0}
|
||||
:where(.yc-created-expert-source) .wb-popover.cb-doc-sel-chip-tooltip{background:var(--wb-bg-primary);border:1px solid var(--wb-popover-border-color);color:var(--wb-color-text-primary);border-radius:16px;padding:0;display:block;overflow:hidden;box-shadow:0 6px 24px -4px #0000000a,0 4px 6px -8px #0000000a!important}
|
||||
:where(.yc-created-expert-source) .wb-popover.cb-doc-sel-chip-tooltip .cb-doc-sel-chip-card__title{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--wb-color-text-secondary);flex:1;font-size:12px;font-weight:400;line-height:20px;overflow:hidden}
|
||||
:where(.yc-created-expert-source) .wb-popover.cb-doc-sel-chip-tooltip .cb-doc-sel-chip-card__selection{text-overflow:ellipsis;white-space:nowrap;color:var(--wb-color-text-primary);font-size:14px;font-weight:400;line-height:22px;overflow:hidden}
|
||||
:where(.yc-created-expert-source) .tencent-docs-panel__settings-menu--with-admin .wb-dropdown__list>.wb-dropdown__item:first-child{margin-bottom:8px}
|
||||
:where(.yc-created-expert-source) .wb-home-schedule-context-menu .wb-dropdown__list{width:max-content;min-width:144px}
|
||||
:where(.yc-created-expert-source) .wb-home-schedule-context-menu .wb-dropdown__item{white-space:nowrap}
|
||||
:where(.yc-created-expert-source) .wb-home-schedule-context-menu .wb-dropdown__item--danger:not(.wb-dropdown__item--disabled) .wb-dropdown__label{color:var(--wb-color-text-error,#d54941)}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-select-popover.wb-home-schedule-recurrence-custom__unit-popover{overflow:visible;max-height:none!important}
|
||||
:where(.yc-created-expert-source) .wb-home-schedule-room-panel__occupant-pop.wb-popover{pointer-events:auto;--wb-popover-shadow:0 2px 8px 0 color-mix(in srgb, var(--wb-palette-black-100) 8%, transparent);padding:var(--wb-spacing-2) var(--wb-spacing-3);border-radius:var(--wb-radius-lg)}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-select-popover.wb-home-schedule-select-popover .wb-select__panel{padding-block:var(--wb-popover-padding)}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-select-popover.wb-home-schedule-select-popover .wb-select__search{padding-top:0}
|
||||
:where(.yc-created-expert-source) .wb-popover.wb-select-popover.wb-home-schedule-room-panel__select-pop{max-width:none;width:max-content!important}
|
||||
:where(.yc-created-expert-source) .wb-home-schedule-mobile-create__form:has(.wb-popover){z-index:1;position:relative}
|
||||
:where(.yc-created-expert-source) .wb-home-schedule-mobile-create__card:has(.wb-popover){z-index:2;position:relative}
|
||||
/* Host IDE semantic bridge, not a recovered declaration. */
|
||||
.yc-created-expert-source{--vscode-errorForeground:var(--wb-color-text-error-default);--vscode-errorBackground:var(--wb-button-danger-fg-on-solid)}
|
||||
@@ -0,0 +1,21 @@
|
||||
/** WorkBuddy main-content-core: preserve user text and non-text-only drafts;
|
||||
* replace only an empty input or the still-unedited expert default. */
|
||||
export interface ExpertInputDraft {
|
||||
text: string
|
||||
attachments: readonly unknown[]
|
||||
resources?: readonly unknown[]
|
||||
skills?: readonly unknown[]
|
||||
autoFilledPrompt?: string
|
||||
editingId?: string
|
||||
}
|
||||
export function expertInputUpdate<T extends ExpertInputDraft>(draft: T, prompt: string, explicit = false, conversation = false): T {
|
||||
if (!prompt && !explicit) return conversation ? clearExpertInputPrefill(draft) : draft
|
||||
const hasUserText = !!draft.text.trim() && draft.text !== draft.autoFilledPrompt
|
||||
const hasNonText = !!(draft.attachments.length || draft.resources?.length || draft.skills?.length)
|
||||
if (!explicit && (draft.editingId || hasUserText || (!draft.text.trim() && hasNonText))) return draft
|
||||
return { ...draft, text: prompt, autoFilledPrompt: prompt || undefined }
|
||||
}
|
||||
export function clearExpertInputPrefill<T extends ExpertInputDraft>(draft: T): T {
|
||||
if (!draft.autoFilledPrompt || draft.editingId || draft.text !== draft.autoFilledPrompt) return draft
|
||||
return { ...draft, text: '', autoFilledPrompt: undefined }
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"expert": {
|
||||
"panel": "_panel_14qob_17",
|
||||
"searchBox": "_searchBox_14qob_34",
|
||||
"list": "_list_14qob_39",
|
||||
"scrollList": "_scrollList_14qob_69",
|
||||
"searchList": "_searchList_14qob_70",
|
||||
"item": "_item_14qob_76",
|
||||
"selected": "_selected_14qob_87",
|
||||
"disabled": "_disabled_14qob_87",
|
||||
"avatar": "_avatar_14qob_99",
|
||||
"name": "_name_14qob_121",
|
||||
"actions": "_actions_14qob_137",
|
||||
"checkSlot": "_checkSlot_14qob_151",
|
||||
"pinButton": "_pinButton_14qob_161",
|
||||
"pinned": "_pinned_14qob_179",
|
||||
"pinIconRest": "_pinIconRest_14qob_189",
|
||||
"pinIconHover": "_pinIconHover_14qob_190",
|
||||
"divider": "_divider_14qob_215",
|
||||
"summonMore": "_summonMore_14qob_222",
|
||||
"summonMoreIcon": "_summonMoreIcon_14qob_236",
|
||||
"summonMoreLabel": "_summonMoreLabel_14qob_246",
|
||||
"empty": "_empty_14qob_258"
|
||||
},
|
||||
"base": {
|
||||
"panel": "_panel_1mrhu_14",
|
||||
"divider": "_divider_1mrhu_25",
|
||||
"list": "_list_1mrhu_31",
|
||||
"empty": "_empty_1mrhu_32",
|
||||
"body": "_body_1mrhu_49",
|
||||
"search": "_search_1mrhu_59",
|
||||
"searchIcon": "_searchIcon_1mrhu_71",
|
||||
"searchInput": "_searchInput_1mrhu_81",
|
||||
"searchClear": "_searchClear_1mrhu_98",
|
||||
"itemRow": "_itemRow_1mrhu_150",
|
||||
"itemRowSelected": "_itemRowSelected_1mrhu_168",
|
||||
"itemIcon": "_itemIcon_1mrhu_175",
|
||||
"itemLabel": "_itemLabel_1mrhu_185",
|
||||
"highlight": "_highlight_1mrhu_214",
|
||||
"actionGroup": "_actionGroup_1mrhu_224",
|
||||
"actionRow": "_actionRow_1mrhu_231",
|
||||
"actionIcon": "_actionIcon_1mrhu_257",
|
||||
"actionLabel": "_actionLabel_1mrhu_271",
|
||||
"actionSuffix": "_actionSuffix_1mrhu_279",
|
||||
"actionSuffixChevron": "_actionSuffixChevron_1mrhu_289"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/* Typed host positioning/accessibility/error adapters; recovered paint is in expert-picker-source.css. */
|
||||
.yc-expert-picker{max-width:calc(100vw - 16px);outline:none}
|
||||
.yc-expert-picker>div[role=menu]{max-height:inherit;max-width:100%;overflow:hidden}
|
||||
.yc-expert-picker [role=menuitem]:focus-visible{outline:1px solid var(--cb-popover-border);outline-offset:-1px;background:var(--cb-dropdown-item-hover-bg-color)}
|
||||
.yc-expert-picker-error{box-sizing:border-box;width:240px;max-width:100%;border-radius:8px;margin-top:4px;padding:8px 12px;background:var(--cb-dropdown-bg-color);color:var(--cb-error-color,#d54941);font-size:12px;line-height:18px;overflow-wrap:anywhere}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"version": "5.8.2",
|
||||
"rootSha256": "035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918",
|
||||
"evidence": [
|
||||
"ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb",
|
||||
"ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"file": "wb-input-add-B045GnIg.js",
|
||||
"sha256": "7842fa4321d5351bf6c822bbd0253e0d6d6e68cc9cfae3f2470b3d493c23daf4"
|
||||
},
|
||||
{
|
||||
"file": "contexts-BMeWSUHk.js",
|
||||
"sha256": "5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b"
|
||||
},
|
||||
{
|
||||
"file": "zh-cn-Dfzn8myY.js",
|
||||
"sha256": "05bdf17cc76e1c2547e84a2754d249405366eb5559149b1661837c9687e6efab"
|
||||
},
|
||||
{
|
||||
"file": "contexts-BkjwYSZT.css",
|
||||
"sha256": "50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380"
|
||||
},
|
||||
{
|
||||
"file": "wb-input-add-CDNucW11.css",
|
||||
"sha256": "07566131dac43d1854af1ecb973bf9736a63723a0786ba0d2d5287ed6f45f2f6"
|
||||
}
|
||||
],
|
||||
"bodies": [
|
||||
{
|
||||
"name": "CheckIcon$2",
|
||||
"start": 4196249,
|
||||
"end": 4196837,
|
||||
"sha256": "a3e200da3f151bcabc45f535385763364d3e1c330bc132e7ed73180e194fbddb"
|
||||
},
|
||||
{
|
||||
"name": "wV",
|
||||
"start": 4205837,
|
||||
"end": 4206556,
|
||||
"sha256": "c4416ff515e36f75b36c8d9700fec0fce1d8962f81b17543294bb1b736cc31ec"
|
||||
},
|
||||
{
|
||||
"name": "readString",
|
||||
"start": 31934,
|
||||
"end": 31995,
|
||||
"sha256": "5629144287b63d2510341dcdeaa034e4a4a789ea64ab8fe0ec2cc0454ba710a5"
|
||||
},
|
||||
{
|
||||
"name": "getExpertMenuDisplayName",
|
||||
"start": 32167,
|
||||
"end": 32274,
|
||||
"sha256": "04cbbbc9208ef33d98acc1c9af4e618ba779c9c088b70f9287e88411114e2f51"
|
||||
},
|
||||
{
|
||||
"name": "normalizeKeyword",
|
||||
"start": 38055,
|
||||
"end": 38114,
|
||||
"sha256": "2c776fb61293760e000167d2d290b944ea514a38fa423e56e30657547e0c845c"
|
||||
},
|
||||
{
|
||||
"name": "matchesKeyword",
|
||||
"start": 38114,
|
||||
"end": 38275,
|
||||
"sha256": "87715de9d17374c053db2fd290235645e41d313b9c04c1bc91db8e3e0f5522f5"
|
||||
},
|
||||
{
|
||||
"name": "SummonMoreIcon",
|
||||
"start": 46780,
|
||||
"end": 47191,
|
||||
"sha256": "bb8276dcbfa4b3d99d94682aa0a8776885a15a10730927cee438d95e175b9959"
|
||||
},
|
||||
{
|
||||
"name": "ExpertAvatar",
|
||||
"start": 50768,
|
||||
"end": 50901,
|
||||
"sha256": "c34b8c90ec8c7b889841cf3eecf46f16dac35545557f0fe6124e1e6d44a9ec27"
|
||||
},
|
||||
{
|
||||
"name": "ExpertRow",
|
||||
"start": 49899,
|
||||
"end": 50767,
|
||||
"sha256": "4ccbc5c4ceaae1b45a45063557cb5c20be0ea4466388be054392ee1be7f92711"
|
||||
},
|
||||
{
|
||||
"name": "ExpertsSubmenu",
|
||||
"start": 47192,
|
||||
"end": 49898,
|
||||
"sha256": "0883bbe4654144c155f45f0184511275dbfd27683c0e8e5a1d1336f80372b957"
|
||||
}
|
||||
],
|
||||
"adaptations": [
|
||||
"Local React/JSX singleton; source component/function bodies are preserved except binding substitutions.",
|
||||
"ExpertRow disabled adds the typed host item.disabled flag.",
|
||||
"Source cloud search/recommendation/pin hooks are replaced by the explicit local typed host; no original cloud service or fake expert is invoked.",
|
||||
"CSS declarations remain unchanged; selectors scoped to .yc-expert-picker.",
|
||||
"Explicit host adaptation: injection.reselectSelected opts out of the legacy selected-ID close-only branch; YCode enables it for current cr ordinary user reactivation while preserving selected styling. This is not literal legacy behavior. Basis: docs/专家使用逻辑-REA核对-20261016.md, src-DbE1_mgM.js ExpertsSubmenu [1574199,1579192), SHA 6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4; current cr onSelect does not short-circuit the selected ID. The typed host retains its existing async success-close/failure-keep-open behavior.",
|
||||
"Expert row and summon-more receive explicit tabindex/data attributes for direct host keyboard navigation; source parent has no Arrow-key row traversal."
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"file": "src/renderer/src/capabilities/expert-picker-source.mjs",
|
||||
"sha256": "85d132f3d4aaa69e0a0af05ed56a012e6d5dc9ec5e5c47ba7248f3dc168eeca5",
|
||||
"bytes": 8452
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/capabilities/expert-picker-source.d.mts",
|
||||
"sha256": "81548b0b6d32a68ac6335a5070819712658954799059af6b567a6ff71a7e1d95",
|
||||
"bytes": 635
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/capabilities/expert-picker-classes.json",
|
||||
"sha256": "08c703a43d89d7c097c9683760811dea16bee5f552a546de92ac7f3887e1e758",
|
||||
"bytes": 1674
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/capabilities/expert-picker-source.css",
|
||||
"sha256": "e6eb7670770628d49ad77754f11c910eaedb9e40e403c14884cd1bee986a2f53",
|
||||
"bytes": 10706
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { cloneElement, createContext, useContext, useMemo, useState, type ReactElement, type ReactNode } from 'react'
|
||||
import type { ExpertPickerItem } from './SourceExpertPicker'
|
||||
import { getExpertMenuDisplayName, matchesKeyword, normalizeKeyword } from './expert-picker-source.mjs'
|
||||
|
||||
export interface ExpertPickerMemory { pinned: string[]; recent: string[] }
|
||||
export interface ExpertPickerHost {
|
||||
items: readonly ExpertPickerItem[]
|
||||
memory: ExpertPickerMemory
|
||||
busy: boolean
|
||||
error?: string
|
||||
togglePin(item: ExpertPickerItem): void
|
||||
}
|
||||
export const ExpertPickerHostContext = createContext<ExpertPickerHost | null>(null)
|
||||
function useHost(): ExpertPickerHost { const host = useContext(ExpertPickerHostContext); if (!host) throw Error('Expert picker requires its typed host'); return host }
|
||||
export function classNames(...values: (string | false | undefined | Record<string, unknown>)[]): string { return values.flatMap(value => typeof value === 'string' ? [value] : value && typeof value === 'object' ? Object.keys(value).filter(key => value[key]) : []).join(' ') }
|
||||
export function usePinnedExperts(enabled: boolean): { pinnedExperts: ExpertPickerItem[]; isPinned(id: string): boolean; togglePinned(item: ExpertPickerItem): void } {
|
||||
const host = useHost(), known = new Map(host.items.map(item => [item.id, item]))
|
||||
return { pinnedExperts: enabled ? host.memory.pinned.flatMap(id => known.has(id) ? [known.get(id)!] : []) : [], isPinned: id => enabled && host.memory.pinned.includes(id), togglePinned: host.togglePin }
|
||||
}
|
||||
/** Source normal view: all pinned rows followed by at most five additional recent/suggested rows; scroll after ten. */
|
||||
export function useExpertsSuggestions(): { items: ExpertPickerItem[]; shouldScroll: boolean } {
|
||||
const host = useHost()
|
||||
return useMemo(() => {
|
||||
const known = new Map(host.items.map(item => [item.id, item])), pinned = host.memory.pinned.flatMap(id => known.has(id) ? [known.get(id)!] : []), seen = new Set(pinned.map(item => item.id)), recent = host.memory.recent.flatMap(id => known.has(id) ? [known.get(id)!] : []), additional: ExpertPickerItem[] = []
|
||||
for (const item of [...recent, ...host.items]) { if (seen.has(item.id)) continue; seen.add(item.id); additional.push(item); if (additional.length >= 5) break }
|
||||
const items = [...pinned, ...additional]
|
||||
return { items, shouldScroll: items.length > 10 }
|
||||
}, [host.items, host.memory])
|
||||
}
|
||||
/** Local host has the complete loaded registry pool, so the original 150ms network-preload timer is unnecessary. */
|
||||
export function useExpertSearch(keyword: string, enabled: boolean): { results: ExpertPickerItem[]; isLoading: boolean; errorReason: string | null; preload(): void } {
|
||||
const host = useHost(), needle = normalizeKeyword(keyword)
|
||||
return { results: enabled && needle ? host.items.filter(item => matchesKeyword(item, needle)) : [], isLoading: host.busy && !host.items.length, errorReason: host.error || null, preload: () => {} }
|
||||
}
|
||||
export function AvatarPrimitive({ src, className, children }: { src?: string; size?: number; alt?: string; className?: string; children?: ReactNode }): React.JSX.Element {
|
||||
const [failed, setFailed] = useState<string | null>(null)
|
||||
const safe = src && /^(?:https?:|blob:|data:image\/(?:png|jpeg|webp|gif|svg\+xml);)/i.test(src) ? src : undefined
|
||||
return <span className={className} style={{ background: 'var(--cb-text-secondary)' }}>{safe && failed !== safe ? <img src={safe} alt="" onError={() => setFailed(safe)} /> : Array.from(typeof children === 'string' ? children.trim() : '')[0]?.toUpperCase() ?? '?'}</span>
|
||||
}
|
||||
export function Tooltip({ children, content }: { children: ReactElement<{ title?: string }>; content: string }): React.JSX.Element { return cloneElement(children, { title: content }) }
|
||||
export function readExpertPickerMemory(key: string): ExpertPickerMemory {
|
||||
try { const value = JSON.parse(localStorage.getItem(key) ?? '{}'); const safe = (items: unknown): string[] => Array.isArray(items) ? [...new Set(items.filter((id): id is string => typeof id === 'string' && id.length > 0))].slice(0, 100) : []; return { pinned: safe(value?.pinned), recent: safe(value?.recent) } } catch { return { pinned: [], recent: [] } }
|
||||
}
|
||||
export { getExpertMenuDisplayName }
|
||||
@@ -0,0 +1,75 @@
|
||||
/* REA fixed _14qob_ expert menu + _1mrhu_ search/base declarations. */
|
||||
.yc-expert-picker ._panel_1mrhu_14{background:var(--cb-dropdown-bg-color);border:.5px solid var(--cb-popover-border);box-shadow:var(--cb-shadow-popover);box-sizing:border-box;border-radius:16px;flex-direction:column;gap:0;padding:8px;display:flex}
|
||||
.yc-expert-picker ._panel_1mrhu_14>:not(:first-child):not(._divider_1mrhu_25){margin-top:8px}
|
||||
.yc-expert-picker ._panel_1mrhu_14>._divider_1mrhu_25,.yc-expert-picker ._panel_1mrhu_14>._list_1mrhu_31+._divider_1mrhu_25,.yc-expert-picker ._panel_1mrhu_14>._empty_1mrhu_32+._divider_1mrhu_25,.yc-expert-picker ._panel_1mrhu_14>._divider_1mrhu_25+*{margin-top:0}
|
||||
.yc-expert-picker ._panel_1mrhu_14{user-select:none;z-index:40;overflow:hidden;outline:none!important}
|
||||
.yc-expert-picker ._panel_1mrhu_14:focus,.yc-expert-picker ._panel_1mrhu_14:focus-visible{outline:none!important}
|
||||
.yc-expert-picker ._body_1mrhu_49{flex-direction:column;flex:auto;min-height:0;display:flex}
|
||||
.yc-expert-picker ._search_1mrhu_59{background:var(--cb-search-input-bg);box-sizing:border-box;border-radius:8px;flex:0 0 32px;align-items:center;gap:8px;height:32px;padding:0 8px;display:flex}
|
||||
.yc-expert-picker ._searchIcon_1mrhu_71{width:16px;height:16px;color:var(--cb-text-secondary);flex:0 0 16px;justify-content:center;align-items:center;display:inline-flex}
|
||||
.yc-expert-picker ._searchInput_1mrhu_81{min-width:0;color:var(--cb-text-primary);caret-color:var(--cb-green-color);background:0 0;border:none;outline:none;flex:auto;padding:0;font-family:PingFang SC,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:14px;line-height:22px}
|
||||
.yc-expert-picker ._searchInput_1mrhu_81::placeholder{color:var(--cb-text-muted)}
|
||||
.yc-expert-picker ._searchClear_1mrhu_98{cursor:pointer;width:16px;height:16px;color:var(--cb-text-muted);background:0 0;border:none;flex:0 0 16px;justify-content:center;align-items:center;padding:0;display:inline-flex}
|
||||
.yc-expert-picker ._searchClear_1mrhu_98:hover{color:var(--cb-text-tertiary)}
|
||||
.yc-expert-picker ._list_1mrhu_31{scrollbar-width:thin;scrollbar-color:transparent transparent;flex-direction:column;flex:0 auto;gap:2px;min-height:0;display:flex;overflow-y:auto}
|
||||
.yc-expert-picker ._list_1mrhu_31::-webkit-scrollbar{width:4px}
|
||||
.yc-expert-picker ._list_1mrhu_31::-webkit-scrollbar-thumb{background:0 0;border-radius:12px;transition:background-color .2s}
|
||||
.yc-expert-picker ._list_1mrhu_31::-webkit-scrollbar-track{background:0 0}
|
||||
.yc-expert-picker ._panel_1mrhu_14:hover ._list_1mrhu_31{scrollbar-color:var(--cb-scrollbar-thumb) transparent}
|
||||
.yc-expert-picker ._panel_1mrhu_14:hover ._list_1mrhu_31::-webkit-scrollbar-thumb{background:var(--cb-scrollbar-thumb)}
|
||||
.yc-expert-picker ._panel_1mrhu_14:hover ._list_1mrhu_31::-webkit-scrollbar-thumb:hover{background:var(--cb-scrollbar-thumb-hover)}
|
||||
.yc-expert-picker ._itemRow_1mrhu_150{cursor:pointer;box-sizing:border-box;min-height:32px;color:var(--cb-text-primary);border-radius:8px;align-items:center;gap:8px;padding:4px 8px;font-family:PingFang SC,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:14px;line-height:22px;display:flex}
|
||||
.yc-expert-picker ._itemRow_1mrhu_150:hover{background:var(--cb-dropdown-item-hover-bg-color)}
|
||||
.yc-expert-picker ._itemRowSelected_1mrhu_168{background:var(--cb-list-item-selected-bg)}
|
||||
.yc-expert-picker ._itemRowSelected_1mrhu_168:hover{background:var(--cb-dropdown-item-hover-bg-color)}
|
||||
.yc-expert-picker ._itemIcon_1mrhu_175{width:16px;height:16px;color:var(--cb-text-secondary);flex:0 0 16px;justify-content:center;align-items:center;display:inline-flex}
|
||||
.yc-expert-picker ._itemLabel_1mrhu_185{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}
|
||||
.yc-expert-picker ._divider_1mrhu_25{background:var(--cb-popover-divider);flex:0 0 1px;height:1px}
|
||||
.yc-expert-picker ._empty_1mrhu_32{height:32px;color:var(--cb-text-muted);justify-content:center;align-items:center;padding:0 8px;font-family:PingFang SC,sans-serif;font-size:14px;line-height:22px;display:flex}
|
||||
.yc-expert-picker ._highlight_1mrhu_214{color:var(--cb-green-color);font-weight:inherit;background:0 0;padding:0}
|
||||
.yc-expert-picker ._actionGroup_1mrhu_224{flex-direction:column;flex:none;gap:2px;display:flex}
|
||||
.yc-expert-picker ._actionRow_1mrhu_231{width:100%;min-height:32px;color:var(--cb-text-primary);text-align:left;cursor:pointer;box-sizing:border-box;background:0 0;border:none;border-radius:8px;align-items:center;gap:8px;padding:4px 8px;font-family:PingFang SC,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:14px;line-height:22px;display:flex}
|
||||
.yc-expert-picker ._actionRow_1mrhu_231:hover:not(:disabled){background:var(--cb-dropdown-item-hover-bg-color)}
|
||||
.yc-expert-picker ._actionRow_1mrhu_231:disabled{cursor:not-allowed;opacity:.5}
|
||||
.yc-expert-picker ._actionIcon_1mrhu_257{width:16px;height:16px;color:var(--cb-text-secondary);flex:0 0 16px;justify-content:center;align-items:center;display:inline-flex}
|
||||
.yc-expert-picker ._actionIcon_1mrhu_257 svg{width:100%;height:100%}
|
||||
.yc-expert-picker ._actionLabel_1mrhu_271{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}
|
||||
.yc-expert-picker ._actionSuffix_1mrhu_279{color:var(--cb-text-tertiary);flex-shrink:0;align-items:center;gap:4px;font-size:12px;line-height:20px;display:inline-flex}
|
||||
.yc-expert-picker ._actionSuffixChevron_1mrhu_289{width:14px;height:14px;color:var(--cb-text-muted);justify-content:center;align-items:center;display:inline-flex}
|
||||
.yc-expert-picker ._panel_14qob_17{background:var(--cb-dropdown-bg-color);border:.5px solid var(--cb-popover-border);width:240px;box-shadow:var(--cb-shadow-popover);box-sizing:border-box;user-select:none;border-radius:16px;flex-direction:column;padding:8px;display:flex;outline:none!important}
|
||||
.yc-expert-picker ._panel_14qob_17:focus,.yc-expert-picker ._panel_14qob_17:focus-visible{outline:none!important}
|
||||
.yc-expert-picker ._searchBox_14qob_34{padding:0 0 8px}
|
||||
.yc-expert-picker ._list_14qob_39{scrollbar-width:thin;scrollbar-color:transparent transparent;flex-direction:column;gap:2px;min-height:0;display:flex}
|
||||
.yc-expert-picker ._list_14qob_39::-webkit-scrollbar{width:4px}
|
||||
.yc-expert-picker ._list_14qob_39::-webkit-scrollbar-thumb{background:0 0;border-radius:12px;transition:background-color .2s}
|
||||
.yc-expert-picker ._list_14qob_39::-webkit-scrollbar-track{background:0 0}
|
||||
.yc-expert-picker ._panel_14qob_17:hover ._list_14qob_39{scrollbar-color:var(--cb-scrollbar-thumb) transparent}
|
||||
.yc-expert-picker ._panel_14qob_17:hover ._list_14qob_39::-webkit-scrollbar-thumb{background:var(--cb-scrollbar-thumb)}
|
||||
.yc-expert-picker ._panel_14qob_17:hover ._list_14qob_39::-webkit-scrollbar-thumb:hover{background:var(--cb-scrollbar-thumb-hover)}
|
||||
.yc-expert-picker ._scrollList_14qob_69,.yc-expert-picker ._searchList_14qob_70{max-height:338px;overflow-y:auto}
|
||||
.yc-expert-picker ._item_14qob_76{cursor:pointer;box-sizing:border-box;border-radius:8px;flex-shrink:0;align-items:center;gap:8px;height:32px;padding:0 8px;display:flex}
|
||||
.yc-expert-picker ._item_14qob_76:hover:not(._selected_14qob_87):not(._disabled_14qob_87){background:var(--cb-dropdown-item-hover-bg-color)}
|
||||
.yc-expert-picker ._item_14qob_76._selected_14qob_87{background:var(--cb-list-item-selected-bg)}
|
||||
.yc-expert-picker ._item_14qob_76._disabled_14qob_87{cursor:not-allowed;opacity:.5}
|
||||
.yc-expert-picker ._avatar_14qob_99{width:16px;height:16px;color:var(--wb-text-on-primary);border-radius:6px;flex:0 0 16px;justify-content:center;align-items:center;font-family:PingFang SC,sans-serif;font-size:9px;font-weight:600;line-height:1;display:inline-flex;overflow:hidden}
|
||||
.yc-expert-picker ._avatar_14qob_99 img{object-fit:cover;width:100%;height:100%;display:block}
|
||||
.yc-expert-picker ._name_14qob_121{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--cb-text-primary);flex:auto;font-family:PingFang SC,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:14px;line-height:22px;overflow:hidden}
|
||||
.yc-expert-picker ._item_14qob_76._selected_14qob_87 ._name_14qob_121{font-weight:600}
|
||||
.yc-expert-picker ._actions_14qob_137{flex:none;justify-content:flex-end;align-items:center;gap:4px;min-width:18px;display:inline-flex}
|
||||
.yc-expert-picker ._checkSlot_14qob_151{width:16px;height:16px;color:var(--cb-text-primary);flex:0 0 16px;justify-content:center;align-items:center;display:inline-flex}
|
||||
.yc-expert-picker ._pinButton_14qob_161{width:16px;height:16px;color:var(--cb-text-tertiary);cursor:pointer;opacity:0;background:0 0;border:none;flex:0 0 16px;justify-content:center;align-items:center;padding:0;transition:opacity .15s,color .15s;display:inline-flex}
|
||||
.yc-expert-picker ._pinButton_14qob_161:hover,.yc-expert-picker ._pinButton_14qob_161:focus-visible{color:var(--cb-text-secondary)}
|
||||
.yc-expert-picker ._pinButton_14qob_161._pinned_14qob_179{color:var(--cb-text-secondary);opacity:1}
|
||||
.yc-expert-picker ._item_14qob_76:hover ._pinButton_14qob_161,.yc-expert-picker ._pinButton_14qob_161:focus-visible{opacity:1}
|
||||
.yc-expert-picker ._pinIconRest_14qob_189,.yc-expert-picker ._pinIconHover_14qob_190{justify-content:center;align-items:center;width:16px;height:16px;display:inline-flex}
|
||||
.yc-expert-picker ._pinIconRest_14qob_189>svg,.yc-expert-picker ._pinIconHover_14qob_190>svg{width:16px;height:16px}
|
||||
.yc-expert-picker ._pinIconHover_14qob_190,.yc-expert-picker ._item_14qob_76:hover ._pinButton_14qob_161._pinned_14qob_179 ._pinIconRest_14qob_189{display:none}
|
||||
.yc-expert-picker ._item_14qob_76:hover ._pinButton_14qob_161._pinned_14qob_179 ._pinIconHover_14qob_190{display:inline-flex}
|
||||
.yc-expert-picker ._divider_14qob_215{background:var(--cb-popover-divider);height:1px;margin:8px 0}
|
||||
.yc-expert-picker ._summonMore_14qob_222{cursor:pointer;box-sizing:border-box;border-radius:8px;align-items:center;gap:8px;height:32px;padding:0 8px;display:flex}
|
||||
.yc-expert-picker ._summonMore_14qob_222:hover{background:var(--cb-dropdown-item-hover-bg-color)}
|
||||
.yc-expert-picker ._summonMoreIcon_14qob_236{width:16px;height:16px;color:var(--cb-text-secondary);flex:0 0 16px;justify-content:center;align-items:center;display:inline-flex}
|
||||
.yc-expert-picker ._summonMoreLabel_14qob_246{text-overflow:ellipsis;white-space:nowrap;color:var(--cb-text-primary);flex:auto;font-family:PingFang SC,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:14px;line-height:22px;overflow:hidden}
|
||||
.yc-expert-picker ._empty_14qob_258{height:32px;color:var(--cb-text-muted);justify-content:center;align-items:center;padding:0 8px;font-family:PingFang SC,sans-serif;font-size:14px;line-height:22px;display:flex}
|
||||
.yc-expert-picker .teams-container.is-mobile ._panel_14qob_17{max-width:100%}
|
||||
.yc-expert-picker .teams-container.is-mobile ._item_14qob_76{min-width:0}
|
||||
@@ -0,0 +1,3 @@
|
||||
import type {ComponentType} from 'react';import type {ExpertPickerItem} from './SourceExpertPicker';
|
||||
export const ExpertsSubmenu:ComponentType<{injection:{selectedExpertId?:string|null;reselectSelected?:boolean;expertScope:string;recentExperts:readonly ExpertPickerItem[];loadFailed?:boolean;onSelect?:(item:ExpertPickerItem)=>unknown;onSummonSearchResult?:(item:ExpertPickerItem)=>unknown;onSummonMore?():void};onCloseMenu():void}>;
|
||||
export function getExpertMenuDisplayName(item:ExpertPickerItem):string;export function normalizeKeyword(value:string):string;export function matchesKeyword(item:ExpertPickerItem,query:string):boolean;
|
||||
@@ -0,0 +1,17 @@
|
||||
import * as React from 'react';import * as JSX from 'react/jsx-runtime';
|
||||
import {usePinnedExperts,useExpertsSuggestions,useExpertSearch,AvatarPrimitive,Tooltip,classNames} from './expert-picker-runtime';
|
||||
import {PinIcon,UnpinIcon,SearchIcon,ClearFilledIcon} from './SkillSourceIcons';
|
||||
const no=React,SV=React,U=JSX,CV=JSX,V={"panel":"_panel_14qob_17","searchBox":"_searchBox_14qob_34","list":"_list_14qob_39","scrollList":"_scrollList_14qob_69","searchList":"_searchList_14qob_70","item":"_item_14qob_76","selected":"_selected_14qob_87","disabled":"_disabled_14qob_87","avatar":"_avatar_14qob_99","name":"_name_14qob_121","actions":"_actions_14qob_137","checkSlot":"_checkSlot_14qob_151","pinButton":"_pinButton_14qob_161","pinned":"_pinned_14qob_179","pinIconRest":"_pinIconRest_14qob_189","pinIconHover":"_pinIconHover_14qob_190","divider":"_divider_14qob_215","summonMore":"_summonMore_14qob_222","summonMoreIcon":"_summonMoreIcon_14qob_236","summonMoreLabel":"_summonMoreLabel_14qob_246","empty":"_empty_14qob_258"},_V={"panel":"_panel_1mrhu_14","divider":"_divider_1mrhu_25","list":"_list_1mrhu_31","empty":"_empty_1mrhu_32","body":"_body_1mrhu_49","search":"_search_1mrhu_59","searchIcon":"_searchIcon_1mrhu_71","searchInput":"_searchInput_1mrhu_81","searchClear":"_searchClear_1mrhu_98","itemRow":"_itemRow_1mrhu_150","itemRowSelected":"_itemRowSelected_1mrhu_168","itemIcon":"_itemIcon_1mrhu_175","itemLabel":"_itemLabel_1mrhu_185","highlight":"_highlight_1mrhu_214","actionGroup":"_actionGroup_1mrhu_224","actionRow":"_actionRow_1mrhu_231","actionIcon":"_actionIcon_1mrhu_257","actionLabel":"_actionLabel_1mrhu_271","actionSuffix":"_actionSuffix_1mrhu_279","actionSuffixChevron":"_actionSuffixChevron_1mrhu_289"},to={default:classNames},j=()=>key=>({"input.wbAdd.experts.summonMore":"召唤更多专家","input.wbAdd.experts.empty":"暂无可用专家","input.wbAdd.experts.loadFailed":"加载失败,请稍后重试","input.wbAdd.experts.noResult":"未找到专家","input.wbAdd.experts.pin":"置顶","input.wbAdd.experts.searchClear":"清空搜索","input.wbAdd.experts.searching":"搜索中…","input.wbAdd.experts.searchUnavailable":"搜索暂不可用,请稍后重试","input.wbAdd.experts.searchPlaceholder":"搜索专家","input.wbAdd.experts.unpin":"取消置顶"})[key]??key,oe=AvatarPrimitive,C=Tooltip,we=PinIcon,Vt=UnpinIcon,Tt=value=>value;
|
||||
const CheckIcon$2=e=> JSX.jsx(`svg`,{width:`12`,height:`12`,viewBox:`0 0 12 12`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,...e,children: JSX.jsx(`path`,{fill:`currentColor`,fillRule:`evenodd`,clipRule:`evenodd`,d:`M4.57915 9.38193C4.58418 9.38745 4.58937 9.39287 4.59471 9.39821C4.78509 9.58859 5.09375 9.58859 5.28413 9.39821L10.5874 4.09491C10.7778 3.90453 10.7778 3.59586 10.5874 3.40548C10.3971 3.2151 10.0884 3.2151 9.89801 3.40548L4.93978 8.36371L2.10223 5.52616C1.91185 5.33578 1.60318 5.33578 1.4128 5.52616C1.22242 5.71654 1.22242 6.02521 1.41281 6.21559L4.57915 9.38193Z`})});
|
||||
const yt=CheckIcon$2,SearchIcon$1=SearchIcon;
|
||||
const wV=(0,SV.forwardRef)(({value:e,onChange:t,placeholder:n,clearAriaLabel:r=`Clear search`,onCompositionStart:i,onCompositionEnd:a},o)=>{let s=(0,SV.useCallback)(n=>{n.key===`Escape`&&e&&(n.stopPropagation(),t(``))},[e,t]),c=(0,SV.useCallback)(()=>{t(``)},[t]);return JSX.jsxs(`div`,{className:_V.search,children:[ JSX.jsx(`span`,{className:_V.searchIcon,"aria-hidden":`true`,children: JSX.jsx(SearchIcon$1,{})}), JSX.jsx(`input`,{ref:o,type:`text`,className:_V.searchInput,placeholder:n,value:e,onChange:e=>t(e.target.value),onKeyDown:s,onCompositionStart:i,onCompositionEnd:a}),e&& JSX.jsx(`button`,{type:`button`,className:_V.searchClear,"aria-label":r,onClick:c,children: JSX.jsx(ClearFilledIcon,{})})]})});
|
||||
const Pt=wV;
|
||||
function readString(e){return typeof e==`string`?e.trim():``};
|
||||
function getExpertMenuDisplayName(e){return readString(e.profession)||readString(e.name)||readString(e.id)};
|
||||
function normalizeKeyword(e){return e.trim().toLowerCase()};
|
||||
function matchesKeyword(e,t){return t?(typeof e.searchText==`string`&&e.searchText.trim()?e.searchText:getExpertMenuDisplayName(e).toLowerCase()).includes(t):!1};
|
||||
const SummonMoreIcon=()=> JSX.jsxs(`svg`,{width:`16`,height:`16`,viewBox:`0 0 16 16`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,"aria-hidden":`true`,children:[ JSX.jsx(`path`,{d:`M5 4.5h6.5V11`,stroke:`currentColor`,strokeWidth:`1.4`,strokeLinecap:`round`,strokeLinejoin:`round`}), JSX.jsx(`path`,{d:`M11.5 4.5L4.5 11.5`,stroke:`currentColor`,strokeWidth:`1.4`,strokeLinecap:`round`,strokeLinejoin:`round`})]});
|
||||
const ExpertAvatar=({expert:e})=> JSX.jsx(oe,{size:16,src:Tt(e.avatarUrl),alt:``,className:V.avatar,children:getExpertMenuDisplayName(e)});
|
||||
const ExpertRow=({expert:e,selected:t,disabled:n,pinned:r,onClick:i,onPinToggle:a,pinLabel:o})=>{let s=getExpertMenuDisplayName(e);return JSX.jsxs(`div`,{className:(0,to.default)(V.item,{[V.selected]:t,[V.disabled]:n}),role:`menuitem`,tabIndex:n?-1:0,"data-expert-id":e.id,"aria-disabled":n,"aria-checked":t,onClick:()=>{n||i()},children:[ JSX.jsx(ExpertAvatar,{expert:e}), JSX.jsx(`span`,{className:V.name,children:s}), JSX.jsx(`span`,{className:V.actions,children:t? JSX.jsx(`span`,{className:V.checkSlot,"aria-hidden":`true`,children: JSX.jsx(yt,{})}):a&& JSX.jsx(C,{content:o,children: JSX.jsxs(`button`,{type:`button`,className:(0,to.default)(V.pinButton,{[V.pinned]:r}),"aria-label":o,onClick:a,children:[ JSX.jsx(`span`,{className:V.pinIconRest,"aria-hidden":`true`,children: JSX.jsx(we,{})}), JSX.jsx(`span`,{className:V.pinIconHover,"aria-hidden":`true`,children: JSX.jsx(Vt,{})})]})})})]})};
|
||||
const ExpertsSubmenu=({injection:e,onCloseMenu:t})=>{let n=j(),r=e?.selectedExpertId??null,i=e?.expertScope??`default`,a=!e?.disableRecommended,[o,s]=(0,no.useState)(``),[c,l]=(0,no.useState)(``),u=(0,no.useRef)(!1),d=c.trim(),f=a&&d.length>0,handleKeywordChange=e=>{s(e),u.current||l(e)},handleCompositionStart=()=>{u.current=!0},handleCompositionEnd=e=>{u.current=!1,l(e.currentTarget.value)},{pinnedExperts:p,isPinned:m,togglePinned:h}=usePinnedExperts(a,e?.recentExperts??[],i),{items:g,shouldScroll:_}=useExpertsSuggestions(e?.recentExperts,e?.disableRecommended,p,i),{results:v,isLoading:y,errorReason:b,preload:x}=useExpertSearch(c,a,g,i),handleClick=async(n,i)=>{if(i===`search`){if(!e?.onSummonSearchResult)return;await Promise.resolve(e.onSummonSearchResult(n))!==!1&&t?.();return}if(r===n.id&&!e?.reselectSelected){t?.();return}e?.onSelect?.(n),t?.()},handlePinToggle=(e,t)=>{t.preventDefault(),t.stopPropagation(),h(e)},handleSummonMore=()=>{e?.onSummonMore?.(),t?.()},renderRow=(t,i)=> JSX.jsx(ExpertRow,{expert:t,selected:r===t.id&&i===`normal`,disabled:!!t.disabled||(i===`search`?!e?.onSummonSearchResult:!e?.onSelect),pinned:m(t.id),onClick:()=>{handleClick(t,i).catch(()=>void 0)},onPinToggle:a?e=>handlePinToggle(t,e):void 0,pinLabel:m(t.id)?n(`input.wbAdd.experts.unpin`):n(`input.wbAdd.experts.pin`)},`${i}:${t.id}`);return JSX.jsxs(`div`,{className:V.panel,role:`menu`,children:[a&& JSX.jsx(`div`,{className:V.searchBox,onFocus:x,children: JSX.jsx(Pt,{value:o,onChange:handleKeywordChange,onCompositionStart:handleCompositionStart,onCompositionEnd:handleCompositionEnd,placeholder:n(`input.wbAdd.experts.searchPlaceholder`),clearAriaLabel:n(`input.wbAdd.experts.searchClear`)})}),f?y&&v.length===0? JSX.jsx(`div`,{className:V.empty,children:n(`input.wbAdd.experts.searching`)}):b&&v.length===0? JSX.jsx(`div`,{className:V.empty,children:n(`input.wbAdd.experts.searchUnavailable`)}):v.length===0? JSX.jsx(`div`,{className:V.empty,children:n(`input.wbAdd.experts.noResult`)}): JSX.jsx(`div`,{className:(0,to.default)(V.list,V.searchList),children:v.map(e=>renderRow(e,`search`))}):g.length===0? JSX.jsx(`div`,{className:V.empty,children:e?.loadFailed?n(`input.wbAdd.experts.loadFailed`):n(`input.wbAdd.experts.empty`)}): JSX.jsx(`div`,{className:(0,to.default)(V.list,{[V.scrollList]:_}),children:g.map(e=>renderRow(e,`normal`))}),!f&&e?.onSummonMore&& JSX.jsxs(U.Fragment,{children:[ JSX.jsx(`div`,{className:V.divider,role:`separator`}), JSX.jsxs(`div`,{className:V.summonMore,role:`menuitem`,tabIndex:0,"data-expert-manage":true,onClick:handleSummonMore,children:[ JSX.jsx(`span`,{className:V.summonMoreIcon,children: JSX.jsx(SummonMoreIcon,{})}), JSX.jsx(`span`,{className:V.summonMoreLabel,children:n(`input.wbAdd.experts.summonMore`)})]})]})]})};
|
||||
export {ExpertsSubmenu,ExpertRow,getExpertMenuDisplayName,normalizeKeyword,matchesKeyword};
|
||||
@@ -0,0 +1,926 @@
|
||||
/* REA recovered declarations; generated by recover-capability-center.mjs. */
|
||||
:where(.yc-capability-center) .expert-center-light,:where(.yc-capability-center) .expert-center-light{--ec-bg-primary:#fff;--ec-bg-secondary:#fafafa;--ec-bg-tertiary:#f5f5f5;--ec-bg-hover:#f0f0f0;--ec-bg-active:#f5f5f5;--ec-text-primary:#1a1a1a;--ec-text-secondary:#666;--ec-text-tertiary:#999;--ec-text-placeholder:#ccc;--ec-text-desc:var(--wb-color-text-secondary);--ec-text-usage:var(--wb-color-text-tertiary);--ec-text-use-count:var(--wb-color-text-secondary);--ec-text-category-tab:var(--wb-color-text-secondary);--ec-text-contrast:var(--wb-button-primary-fg);--ec-text-soft:var(--wb-color-text-secondary);--ec-text-strong:var(--wb-color-text-primary);--ec-text-muted:var(--wb-text-muted);--ec-expert-card-bg:#fff;--ec-expert-card-hover-bg:#f0f0f0;--ec-expert-card-hover-border:#00000014;--ec-expert-card-avatar-bg:#a6d5da;--ec-expert-card-shadow:0 16px 32px #00000008;--ec-expert-card-desc-color:#000000b3;--ec-card-tag-bg:#0000000a;--ec-card-tag-border:transparent;--ec-card-tag-color:#000000b3;--ec-card-tag-paid-bg:#fceed0;--ec-card-tag-paid-color:#000000b3;--ec-filter-border:transparent;--ec-filter-text:var(--ec-text-category-tab);--ec-filter-text-hover:var(--ec-text-secondary);--ec-filter-text-active:var(--ec-text-primary);--ec-primary:#1f1f1f;--ec-primary-light:#f5f5f5;--ec-primary-hover:#000;--ec-primary-shadow:#0000001f;--ec-primary-bg:#f5f2ff;--ec-border:#e8e8e8;--ec-border-light:var(--wb-border-default);--ec-border-hover:var(--wb-border-strong);--ec-border-subtle:var(--wb-border-default);--ec-card-bg:#fff;--ec-modal-bg:var(--wb-bg-popover);--ec-card-border-static:#f0f0f0;--ec-card-shadow:0 1px 3px #0000000f;--ec-card-shadow-hover:0 12px 32px #0000001f;--ec-card-border-hover:var(--wb-border-strong);--ec-shadow-sm:var(--wb-shadow-dialog-md);--ec-shadow-lg:var(--wb-shadow-dialog-md);--ec-btn-gradient:linear-gradient(135deg, #2f2f2f, #111);--ec-btn-text:#fff;--ec-btn-bg:var(--wb-button-primary-bg);--ec-btn-bg-hover:var(--wb-button-primary-bg-hover);--ec-surface-muted:var(--wb-bg-secondary);--ec-surface-subtle:var(--wb-bg-primary);--ec-surface-hover:var(--wb-bg-hover);--ec-modal-tag-chip-bg:#00000005;--ec-modal-tag-chip-color:#0000009e;--ec-modal-tag-chip-border:#0000000f;--ec-modal-quick-prompt-bg:#00000005;--ec-modal-quick-prompt-bg-hover:#0000000a;--ec-modal-quick-prompt-color:#0000009e;--ec-modal-quick-prompt-border:#0000000f;--ec-success:#10b981;--ec-warning:#f59e0b;--ec-error:#ff4d4f;--ec-avatar-fallback-bg-1:#6366f1;--ec-avatar-fallback-bg-2:#8b5cf6;--ec-avatar-fallback-bg-3:#ec4899;--ec-avatar-fallback-bg-4:#f59e0b;--ec-avatar-fallback-bg-5:#10b981;--ec-avatar-fallback-bg-6:#3b82f6;--ec-avatar-fallback-bg-7:#ef4444;--ec-avatar-fallback-bg-8:#14b8a6;--ec-operational-tag-bg:#f7d18f;--ec-operational-tag-color:#3d3d3d;--ec-operational-tag-border:transparent;--ec-operational-tag-icon-color:#3d3d3d;--ec-operational-tag-check-color:#f7d18f}
|
||||
:where(.yc-capability-center) .expert-center-dark{--ec-bg-primary:var(--content-bg,var(--wb-bg-primary));--ec-bg-secondary:var(--sidebar-bg,var(--wb-bg-secondary));--ec-bg-tertiary:var(--filter-bg,#ffffff0a);--ec-bg-hover:var(--sidebar-hover,#ffffff0a);--ec-bg-active:var(--sidebar-active,#ffffff12);--ec-text-primary:var(--text-primary,var(--wb-color-text-primary));--ec-text-secondary:var(--text-secondary,var(--wb-color-text-secondary));--ec-text-tertiary:var(--text-muted,var(--wb-color-text-tertiary));--ec-text-placeholder:var(--text-dim,#fff6);--ec-text-desc:var(--text-secondary,var(--wb-color-text-secondary));--ec-text-usage:var(--text-muted,var(--wb-color-text-tertiary));--ec-text-use-count:var(--text-dim,#fff6);--ec-text-category-tab:var(--text-muted,#ffffff94);--ec-text-contrast:var(--wb-button-primary-fg);--ec-text-soft:var(--text-secondary,#ffffffb8);--ec-text-strong:var(--wb-color-text-primary);--ec-text-muted:var(--text-muted,#ffffff94);--ec-expert-card-bg:var(--ec-card-bg);--ec-expert-card-hover-bg:var(--card-hover-bg,#2c2c2c);--ec-expert-card-hover-border:transparent;--ec-expert-card-avatar-bg:var(--ec-surface-muted);--ec-expert-card-shadow:0 16px 32px -8px #00000008;--ec-expert-card-desc-color:var(--ec-text-desc);--ec-card-tag-bg:#0000000a;--ec-card-tag-border:transparent;--ec-card-tag-color:var(--text-secondary,#ffffffb3);--ec-card-tag-paid-bg:#fceed0;--ec-card-tag-paid-color:#000000b3;--ec-filter-border:var(--filter-border,#ffffff1a);--ec-filter-text:var(--text-muted,#ffffff8f);--ec-filter-text-hover:var(--text-secondary,#ffffffbd);--ec-filter-text-active:var(--text-primary,#fffffff0);--ec-primary:#f5f5f5;--ec-primary-light:#ffffff1f;--ec-primary-hover:#fff;--ec-primary-shadow:#ffffff1f;--ec-primary-bg:#ffffff14;--ec-border:var(--card-border,#ffffff14);--ec-border-light:var(--card-border,#ffffff14);--ec-border-hover:var(--card-hover-border,#ffffff1f);--ec-border-subtle:var(--sidebar-border,#ffffff14);--ec-card-bg:#ffffff0d;--ec-modal-bg:var(--wb-bg-popover);--ec-card-border-static:var(--card-border,#ffffff14);--ec-card-shadow:none;--ec-card-shadow-hover:var(--card-hover-shadow,none);--ec-card-border-hover:var(--card-hover-border,#ffffff24);--ec-shadow-sm:none;--ec-shadow-lg:none;--ec-btn-gradient:linear-gradient(135deg, #3f434d, #323742);--ec-btn-text:#fff;--ec-btn-bg:var(--wb-button-primary-bg);--ec-btn-bg-hover:var(--wb-button-primary-bg-hover);--ec-btn-color:var(--wb-button-primary-fg);--ec-surface-muted:var(--sidebar-bg,#262626);--ec-surface-subtle:var(--filter-bg,#242424);--ec-surface-hover:var(--sidebar-hover,#2c2c2c);--ec-modal-tag-chip-bg:var(--filter-bg,#ffffff0f);--ec-modal-tag-chip-color:var(--text-secondary,#ffffffb8);--ec-modal-tag-chip-border:var(--card-border,#ffffff14);--ec-modal-quick-prompt-bg:var(--card-bg,#1f2024);--ec-modal-quick-prompt-bg-hover:var(--sidebar-active,#ffffff1a);--ec-modal-quick-prompt-color:var(--text-secondary,#ffffffb8);--ec-modal-quick-prompt-border:var(--card-border,#ffffff14);--ec-success:#a6a6a6;--ec-warning:#d0d0d0;--ec-error:#ec5558;--ec-avatar-fallback-bg-1:#4f5667;--ec-avatar-fallback-bg-2:#5b526d;--ec-avatar-fallback-bg-3:#6a4f65;--ec-avatar-fallback-bg-4:#6b5b48;--ec-avatar-fallback-bg-5:#4f665c;--ec-avatar-fallback-bg-6:#4b5f73;--ec-avatar-fallback-bg-7:#674f52;--ec-avatar-fallback-bg-8:#4f6667;--ec-operational-tag-bg:#f7d18f;--ec-operational-tag-color:#3d3d3d;--ec-operational-tag-border:transparent;--ec-operational-tag-icon-color:#d0d0d0;--ec-operational-tag-check-color:#242424}
|
||||
:where(.yc-capability-center) .vscode-high-contrast .expert-center-dark{--ec-border:#6fc3df;--ec-text-primary:#fff}
|
||||
:where(.yc-capability-center) .vscode-high-contrast-light .expert-center-light{--ec-border:#0f4a85;--ec-text-primary:#000}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-modal-card{border:1px solid color-mix(in srgb, var(--wb-color-text-tertiary) 10%, transparent)}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-modal-name,:where(.yc-capability-center) .expert-center-dark .ec-modal-use-count,:where(.yc-capability-center) .expert-center-dark .ec-modal-section-title,:where(.yc-capability-center) .expert-center-dark .ec-modal-member-name{color:color-mix(in srgb, var(--wb-color-text-tertiary) 78%, white 22%)}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-modal-member-role{color:var(--wb-color-text-primary)}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-modal-team-member{border-bottom-color:color-mix(in srgb, var(--wb-color-text-tertiary) 9%, transparent)}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-badge-team{background:color-mix(in srgb, var(--wb-color-text-tertiary) 10%, transparent);color:var(--wb-color-text-tertiary);border:1px solid color-mix(in srgb, var(--wb-color-text-tertiary) 14%, transparent)}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-modal-category-badge,:where(.yc-capability-center) .expert-center-dark .ec-modal-skill-tag{background:color-mix(in srgb, var(--wb-color-text-tertiary) 9%, transparent);color:color-mix(in srgb, var(--wb-color-text-tertiary) 82%, white 18%);border-color:color-mix(in srgb, var(--wb-color-text-tertiary) 12%, transparent)}
|
||||
:where(.yc-capability-center) .expert-center-page{--ec-page-gutter:24px;background:var(--wb-home-bg-secondary);height:100%;color:var(--wb-color-text-primary);flex-direction:column;display:flex;position:relative;overflow:hidden}
|
||||
:where(.yc-capability-center) .expert-center-page.is-inactive{opacity:.96}
|
||||
:where(.yc-capability-center) .ec-header{padding:7px var(--ec-page-gutter) 0;flex-wrap:wrap;flex-shrink:0;justify-content:space-between;align-items:flex-start;gap:16px 24px;display:flex}
|
||||
:where(.yc-capability-center) .ec-header-left{flex:1;min-width:fit-content}
|
||||
:where(.yc-capability-center) .ec-title-tabs{align-items:baseline;gap:20px;display:flex}
|
||||
:where(.yc-capability-center) .ec-title{color:var(--wb-color-text-tertiary);cursor:pointer;user-select:none;margin:0;font-size:18px;font-weight:600;line-height:1.33;transition:color .2s}
|
||||
:where(.yc-capability-center) .ec-title:hover{color:var(--wb-color-text-secondary)}
|
||||
:where(.yc-capability-center) .ec-title-active{color:var(--wb-color-text-primary);cursor:default}
|
||||
:where(.yc-capability-center) .ec-title-item{white-space:nowrap;color:var(--wb-color-text-tertiary);cursor:pointer;user-select:none;padding-right:4px;font-size:18px;font-weight:600;line-height:1.33;transition:color .2s;position:relative}
|
||||
:where(.yc-capability-center) .ec-title-item:hover{color:var(--wb-color-text-secondary)}
|
||||
:where(.yc-capability-center) .ec-subtitle{color:var(--wb-color-text-tertiary);margin:4px 0 0;font-size:13px;line-height:1.5}
|
||||
:where(.yc-capability-center) .ec-title-disabled{color:var(--wb-color-text-placeholder);cursor:not-allowed;user-select:none}
|
||||
:where(.yc-capability-center) .ec-coming-soon-badge{vertical-align:super;color:var(--wb-color-text-tertiary);background:var(--wb-bg-tertiary);border:1px solid var(--wb-border-default);white-space:nowrap;letter-spacing:0;border-radius:4px;align-items:center;margin-left:4px;padding:1px 5px;font-size:9px;font-weight:500;line-height:14px;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-main-scroll{--ec-main-scrollbar-width:4px;min-height:0;padding:0 calc(var(--ec-page-gutter) - var(--ec-main-scrollbar-width)) 0 var(--ec-page-gutter);scrollbar-width:thin;scrollbar-color:var(--wb-border-default) transparent;flex:1;overflow:hidden auto}
|
||||
:where(.yc-capability-center) .ec-main-scroll::-webkit-scrollbar{width:4px}
|
||||
:where(.yc-capability-center) .ec-main-scroll::-webkit-scrollbar-track{background:0 0}
|
||||
:where(.yc-capability-center) .ec-main-scroll::-webkit-scrollbar-thumb{background:var(--wb-border-default);border-radius:2px}
|
||||
:where(.yc-capability-center) .ec-expert-section{margin-bottom:28px}
|
||||
:where(.yc-capability-center) .ec-main-scroll--split{min-height:0;padding:0 var(--ec-page-gutter);flex-direction:column;flex:1;display:flex;overflow:hidden}
|
||||
:where(.yc-capability-center) .ec-main-scroll.ec-expert-scroll{padding:0 0 28px}
|
||||
:where(.yc-capability-center) .ec-section-header{justify-content:space-between;align-items:center;margin-bottom:12px;display:flex}
|
||||
:where(.yc-capability-center) .ec-section-title{color:var(--wb-color-text-primary);align-items:center;gap:6px;margin:0;padding-left:2px;font-size:20px;font-weight:600;line-height:28px;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-new-badge{color:var(--wb-button-primary-fg);white-space:nowrap;background:linear-gradient(135deg,#ff6b6b 0%,#ee5a24 100%);border-radius:4px;justify-content:center;align-items:center;padding:2px 6px;font-size:10px;font-weight:600;line-height:14px;display:inline-flex;position:relative;top:-1px}
|
||||
:where(.yc-capability-center) .ec-team-empty{border:1px dashed var(--wb-border-default);background:var(--wb-bg-secondary);border-radius:12px;justify-content:center;align-items:center;padding:24px 20px;display:flex}
|
||||
:where(.yc-capability-center) .ec-team-empty p{color:var(--wb-color-text-tertiary);margin:0;font-size:13px}
|
||||
:where(.yc-capability-center) .ec-page-error{height:100%;color:var(--wb-color-text-tertiary);flex-direction:column;justify-content:center;align-items:center;gap:16px;padding:60px 20px;display:flex}
|
||||
:where(.yc-capability-center) .ec-page-error__msg{text-align:center;margin:0;font-size:14px}
|
||||
:where(.yc-capability-center) .ec-list-error{color:var(--wb-color-text-tertiary);flex-direction:column;justify-content:center;align-items:center;gap:16px;padding:60px 20px;display:flex}
|
||||
:where(.yc-capability-center) .ec-list-error__msg{text-align:center;margin:0;font-size:14px}
|
||||
:where(.yc-capability-center) .ec-grid-loading{flex:1;justify-content:center;align-items:center;min-height:200px;display:flex}
|
||||
:where(.yc-capability-center) .ec-grid-spinner{border:2px solid var(--wb-bg-tertiary);border-top-color:var(--wb-brand-accent);border-radius:50%;width:28px;height:28px;animation:.8s linear infinite ec-spin}
|
||||
:where(.yc-capability-center) .ec-load-more-spinner{justify-content:center;align-items:center;padding:12px 0;display:flex}
|
||||
:where(.yc-capability-center) .ec-grid-spinner--sm{width:16px;height:16px}
|
||||
:where(.yc-capability-center) .ec-load-more-sentinel{height:0;margin:0;padding:0}
|
||||
@keyframes ec-spin{to{transform:rotate(360deg)}}
|
||||
:where(.yc-capability-center) .ec-expert-tag{border:1px solid var(--wb-border-hover);background:var(--wb-bg-tertiary);max-width:240px;color:var(--wb-brand-accent);border-radius:6px;align-items:center;gap:6px;padding:4px 10px 4px 6px;display:inline-flex;position:relative}
|
||||
:where(.yc-capability-center) .ec-expert-tag-avatar{background:var(--wb-bg-primary);border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;width:20px;height:20px;font-size:12px;font-weight:600;display:inline-flex;overflow:hidden}
|
||||
:where(.yc-capability-center) .ec-expert-tag-avatar img{object-fit:cover;width:100%;height:100%}
|
||||
:where(.yc-capability-center) .ec-expert-tag-name{white-space:nowrap;text-overflow:ellipsis;font-size:13px;font-weight:500;overflow:hidden}
|
||||
:where(.yc-capability-center) .ec-expert-tag-close{cursor:pointer;opacity:0;background:var(--wb-bg-tertiary);width:16px;height:16px;color:var(--wb-button-primary-fg);border:none;border-radius:50%;justify-content:center;align-items:center;padding:0;transition:opacity .15s,transform .15s,background .15s;display:flex;position:absolute;top:-6px;right:-6px;transform:scale(.75)}
|
||||
:where(.yc-capability-center) .ec-expert-tag-close.is-visible{opacity:1;transform:scale(1)}
|
||||
:where(.yc-capability-center) .ec-expert-tag-close:hover{background:var(--wb-status-error)}
|
||||
:where(.yc-capability-center) .ec-expert-tag-close svg{width:10px;height:10px}
|
||||
:where(.yc-capability-center) .ec-search-highlight{background:var(--wb-bg-active);color:var(--wb-color-text-solid);border-radius:2px;padding:0 1px}
|
||||
@media (width<=767px){:where(.yc-capability-center) .expert-center-page{--ec-page-gutter:20px}:where(.yc-capability-center) .ec-header{padding-top:7px}:where(.yc-capability-center) .ec-subtitle{margin-bottom:16px}:where(.yc-capability-center) .ec-main-scroll,:where(.yc-capability-center) .ec-main-scroll--split{padding:12px var(--ec-page-gutter) 24px}}
|
||||
:where(.yc-capability-center) .ec-leaderboard-panel{scrollbar-width:none;-ms-overflow-style:none;background:linear-gradient(#fff 0%,#f7f7f7 40%,#fff 100%);flex-direction:column;flex-shrink:0;width:280px;min-width:280px;display:none;overflow-y:auto;box-shadow:-1px 0 8px #00000014}
|
||||
:where(.yc-capability-center) .ec-leaderboard-panel::-webkit-scrollbar{display:none}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-leaderboard-panel{background:linear-gradient(#252526 0%,#222325 40%,#1e1e1e 100%);box-shadow:-1px 0 8px #00000038}
|
||||
:where(.yc-capability-center) .ec-lb-header{background:linear-gradient(135deg,#f5f0ff,#fef8e8);border-radius:14px 14px 0 0;align-items:center;gap:8px;margin:12px 12px 0;padding:24px 20px 10px;display:flex}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-lb-header{background:linear-gradient(135deg,#2d2a3e,#3a3530)}
|
||||
:where(.yc-capability-center) .ec-lb-trophy-icon{width:24px;height:24px;color:var(--wb-status-warning-muted);filter:drop-shadow(0 1px 3px #f59e0b4d);flex-shrink:0}
|
||||
:where(.yc-capability-center) .ec-lb-header-title{color:var(--wb-color-text-primary);letter-spacing:.5px;margin:0;font-size:16px;font-weight:700}
|
||||
:where(.yc-capability-center) .ec-lb-subtitle{color:var(--wb-tab-active-fg);border-bottom:2px solid var(--wb-tab-active-border);background:linear-gradient(135deg,#f5f0ff,#fef8e8);border-radius:0 0 14px 14px;margin:0 12px;padding:6px 20px 14px;font-size:12px}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-lb-subtitle{border-bottom-color:var(--wb-tab-active-border);background:linear-gradient(135deg,#2d2a3e,#3a3530)}
|
||||
:where(.yc-capability-center) .ec-lb-list{flex-direction:column;gap:4px;padding:8px 12px 20px;display:flex;position:relative}
|
||||
:where(.yc-capability-center) .ec-lb-loading-overlay{opacity:1;pointer-events:none;background:linear-gradient(#fdfbffd9 0%,#f8f5ffd9 40%,#ffffffd9 100%);justify-content:center;align-items:center;transition:opacity .22s;display:flex;position:absolute;inset:0}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-lb-loading-overlay{background:linear-gradient(#252526c7 0%,#2a2a3ec7 40%,#1e1e1ec7 100%)}
|
||||
:where(.yc-capability-center) .ec-lb-loading-overlay.is-fade-out{opacity:0}
|
||||
:where(.yc-capability-center) .ec-lb-loading{color:var(--wb-color-text-secondary);font-size:13px;line-height:20px}
|
||||
:where(.yc-capability-center) .ec-lb-item{cursor:pointer;border-radius:12px;align-items:center;gap:12px;padding:10px 12px;transition:all .2s;display:flex;position:relative;overflow:hidden}
|
||||
:where(.yc-capability-center) .ec-lb-rank{flex-shrink:0;justify-content:center;align-items:center;min-width:40px;font-family:SF Mono,Menlo,Monaco,Consolas,monospace;font-size:12px;font-weight:700;display:flex}
|
||||
:where(.yc-capability-center) .ec-lb-avatar-wrapper{flex-shrink:0;position:relative}
|
||||
:where(.yc-capability-center) .ec-lb-crown{z-index:2;filter:drop-shadow(0 1px 2px #00000026);font-size:16px;line-height:1;position:absolute;top:-10px;left:50%;transform:translate(-50%)}
|
||||
:where(.yc-capability-center) .ec-lb-avatar{border:2px solid var(--wb-card-selected-border);background:var(--wb-card-selected-bg);border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;width:36px;height:36px;font-size:18px;display:flex;overflow:hidden}
|
||||
:where(.yc-capability-center) .ec-lb-avatar img{object-fit:cover;width:100%;height:100%}
|
||||
:where(.yc-capability-center) .ec-lb-avatar-fallback{color:var(--wb-brand-accent);font-size:14px;font-weight:600}
|
||||
:where(.yc-capability-center) .ec-lb-info{flex:1;min-width:0}
|
||||
:where(.yc-capability-center) .ec-lb-name{color:var(--wb-color-text-primary);white-space:nowrap;text-overflow:ellipsis;font-size:13px;font-weight:500;overflow:hidden}
|
||||
:where(.yc-capability-center) .ec-lb-title{color:var(--wb-color-text-tertiary);white-space:nowrap;text-overflow:ellipsis;margin-top:2px;font-size:11px;overflow:hidden}
|
||||
:where(.yc-capability-center) .ec-lb-hover-overlay{opacity:0;pointer-events:none;z-index:5;background:#0000008c;border-radius:12px;justify-content:center;align-items:center;transition:opacity .2s;display:flex;position:absolute;inset:0}
|
||||
:where(.yc-capability-center) .ec-lb-hover-overlay.is-visible{opacity:1;pointer-events:auto}
|
||||
:where(.yc-capability-center) .ec-lb-summon-btn{background:var(--wb-button-primary-bg);color:var(--wb-button-primary-fg);cursor:pointer;white-space:nowrap;letter-spacing:.5px;border:none;border-radius:20px;padding:8px 20px;font-size:13px;font-weight:500;transition:all .2s;box-shadow:0 4px 16px #00000038}
|
||||
:where(.yc-capability-center) .ec-lb-summon-btn:hover{transform:scale(1.08);box-shadow:0 6px 20px #00000052}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-1{border:1px solid var(--wb-border-warning);background:linear-gradient(135deg,#fffbeb,#fef3c7);padding:14px 12px}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-1 .ec-lb-rank{color:var(--wb-alert-warning-text);font-size:14px;font-weight:800}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-1 .ec-lb-avatar{border-color:var(--wb-status-warning-muted);background:var(--wb-bg-primary);box-shadow:0 2px 8px #f59e0b40}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-1 .ec-lb-name{color:var(--wb-alert-warning-text);font-size:14px;font-weight:600}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-1 .ec-lb-title{color:var(--wb-badge-warning-fg)}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-lb-item.rank-1{background:linear-gradient(135deg,#3d3520,#4a3d20);border-color:#7a6530}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-lb-item.rank-1 .ec-lb-name{color:#fde68a}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-lb-item.rank-1 .ec-lb-title{color:#fcd34d}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-2{background:linear-gradient(135deg,#f8fafc,#f1f5f9);border:1px solid #e2e8f0;padding:12px}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-2 .ec-lb-rank{color:#64748b;font-size:13px;font-weight:800}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-2 .ec-lb-avatar{background:#fff;border-color:#94a3b8}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-2 .ec-lb-name{color:#334155;font-weight:600}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-2 .ec-lb-title{color:#64748b}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-lb-item.rank-2{background:linear-gradient(135deg,#2d2d32,#36363c);border-color:#4a4a55}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-lb-item.rank-2 .ec-lb-name{color:#e2e8f0}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-lb-item.rank-2 .ec-lb-title{color:#94a3b8}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-3{background:linear-gradient(135deg,#fdf8f3,#fef0e2);border:1px solid #fcd9b6;padding:12px}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-3 .ec-lb-rank{color:#c2702a;font-size:13px;font-weight:800}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-3 .ec-lb-avatar{background:#fff;border-color:#e8a76a}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-3 .ec-lb-name{color:#7c4a1e;font-weight:600}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-3 .ec-lb-title{color:#b45309}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-lb-item.rank-3{background:linear-gradient(135deg,#3a3025,#453528);border-color:#6a5030}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-lb-item.rank-3 .ec-lb-name{color:#fcd9b6}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-lb-item.rank-3 .ec-lb-title{color:#e8a76a}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-normal{padding:8px 12px}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-normal .ec-lb-rank{color:#9ca3af;font-size:12px}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-normal .ec-lb-avatar{background:#f9fafb;border-color:#e5e7eb}
|
||||
:where(.yc-capability-center) .ec-lb-item.rank-normal .ec-lb-name{color:#666;font-weight:400}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-lb-item.rank-normal .ec-lb-avatar{background:#2d2d32;border-color:#4a4a55}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-lb-item.rank-normal .ec-lb-name{color:#a0a0a0}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-lb-item.rank-normal .ec-lb-title{color:gray}
|
||||
:where(.yc-capability-center) .ec-page-layout{flex:1;min-height:0;display:flex;overflow:hidden}
|
||||
:where(.yc-capability-center) .ec-topbar{height:var(--wb-titlebar-height);padding:0 var(--ec-page-gutter);-webkit-app-region:drag;flex-shrink:0;align-items:center;display:flex}
|
||||
:where(.yc-capability-center) .ec-topbar__left{-webkit-app-region:no-drag;flex-shrink:0;align-items:center;gap:4px;display:flex}
|
||||
:where(.yc-capability-center) .ec-topbar__right{-webkit-app-region:no-drag;flex-shrink:1;align-items:center;gap:12px;min-width:0;margin-left:auto;display:flex}
|
||||
:where(.yc-capability-center) .ec-topbar__right>.wb-button{flex:none}
|
||||
:where(.yc-capability-center) .ec-topbar-action-btn.wb-button--medium:not(.wb-button--icon-only){padding:0 16px}
|
||||
:where(.yc-capability-center) .ec-topbar__right--my-experts{flex:0 160px}
|
||||
:where(.yc-capability-center) .ec-topbar--subpage .ec-subpage-topbar-nav{min-width:0;color:var(--wb-color-text-primary);cursor:pointer;text-align:left;-webkit-app-region:no-drag;background:0 0;border:none;align-items:center;gap:12px;padding:0;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-topbar--subpage .ec-subpage-topbar-nav:hover{opacity:.8}
|
||||
:where(.yc-capability-center) .ec-topbar--subpage .ec-subpage-topbar-back{flex-shrink:0;justify-content:center;align-items:center;width:20px;height:20px;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-topbar--subpage .ec-subpage-topbar-title{color:inherit;white-space:nowrap;align-items:center;gap:4px;margin:0;font-size:18px;font-weight:600;line-height:1.4;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-main-content{flex-direction:column;flex:1;min-width:0;display:flex;position:relative;overflow:hidden}
|
||||
@media (width<=1199px){:where(.yc-capability-center) .ec-leaderboard-panel{width:260px;min-width:260px}}
|
||||
@media (width<=991px){:where(.yc-capability-center) .ec-leaderboard-panel{width:240px;min-width:240px}}
|
||||
@media (width<=767px){:where(.yc-capability-center) .ec-leaderboard-panel{width:220px;min-width:220px}:where(.yc-capability-center) .ec-lb-header{margin:8px 8px 0;padding:16px 14px 8px}:where(.yc-capability-center) .ec-lb-subtitle{margin:0 8px;padding:4px 14px 10px;font-size:11px}:where(.yc-capability-center) .ec-lb-list{gap:2px;padding:6px 8px 16px}:where(.yc-capability-center) .ec-lb-item{gap:8px;padding:8px 10px}:where(.yc-capability-center) .ec-lb-rank{min-width:32px;font-size:11px}:where(.yc-capability-center) .ec-lb-avatar{width:32px;height:32px}:where(.yc-capability-center) .ec-lb-name{font-size:12px}:where(.yc-capability-center) .ec-lb-title{font-size:10px}}
|
||||
:where(.yc-capability-center) .ec-modal-overlay{z-index:1000;background:var(--wb-color-mask-color);justify-content:center;align-items:center;padding:20px;animation:.15s ec-overlay-in;display:flex;position:fixed;inset:0}
|
||||
@keyframes ec-overlay-in{0%{opacity:0}to{opacity:1}}
|
||||
:where(.yc-capability-center) .ec-modal-card{background:var(--wb-bg-primary);width:520px;max-width:90vw;max-height:85vh;box-shadow:var(--wb-shadow-dialog-md);opacity:0;border-radius:16px;flex-direction:column;transition:opacity .12s ease-out;animation:.2s forwards ec-modal-in;display:flex;position:relative;overflow:hidden}
|
||||
@keyframes ec-modal-in{0%{opacity:0;transform:scale(.95)translateY(8px)}to{opacity:1;transform:scale(1)translateY(0)}}
|
||||
:where(.yc-capability-center) .ec-modal-body{scrollbar-width:thin;scrollbar-color:var(--wb-border-default) transparent;flex:1;min-height:0;padding:0 20px;overflow-y:auto}
|
||||
:where(.yc-capability-center) .ec-modal-body::-webkit-scrollbar{width:4px}
|
||||
:where(.yc-capability-center) .ec-modal-body::-webkit-scrollbar-thumb{background:var(--wb-border-default);border-radius:999px}
|
||||
:where(.yc-capability-center) .ec-modal-close{border-radius:var(--wb-radius-md);width:24px;height:24px;color:var(--wb-text-muted);cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;transition:background .15s,color .15s;display:flex;position:absolute;top:18px;right:18px}
|
||||
:where(.yc-capability-center) .ec-modal-close:hover{background:var(--wb-bg-hover);color:var(--wb-color-text-primary)}
|
||||
:where(.yc-capability-center) .ec-modal-top{flex-shrink:0;align-items:flex-start;gap:12px;margin-bottom:14px;padding:22px 52px 0 20px;display:flex}
|
||||
:where(.yc-capability-center) .ec-modal-avatar{border-radius:12px;flex-shrink:0;justify-content:center;align-items:center;width:76px;height:76px;display:flex;overflow:hidden}
|
||||
:where(.yc-capability-center) .ec-modal-avatar img{object-fit:cover;width:100%;height:100%;display:block}
|
||||
:where(.yc-capability-center) .ec-modal-avatar-fallback{width:100%;height:100%;color:var(--wb-button-primary-fg);background:linear-gradient(135deg, var(--wb-avatar-bg-fallback-6), var(--wb-avatar-bg-fallback-2));border-radius:10px;justify-content:center;align-items:center;font-size:20px;font-weight:700;display:flex}
|
||||
:where(.yc-capability-center) .ec-modal-info{flex:1;min-width:0}
|
||||
:where(.yc-capability-center) .ec-modal-title-row{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;display:flex}
|
||||
:where(.yc-capability-center) .ec-modal-role{color:var(--wb-color-text-primary);letter-spacing:-.02em;align-items:center;gap:8px;margin:0;font-size:18px;font-weight:700;line-height:1.3;display:flex!important}
|
||||
:where(.yc-capability-center) .ec-modal-meta{flex-wrap:wrap;align-items:center;gap:6px;min-width:0;margin-top:4px;display:flex}
|
||||
:where(.yc-capability-center) .ec-modal-name{color:var(--wb-color-text-secondary);white-space:nowrap;flex-shrink:0;font-size:12px;font-weight:500;line-height:18px}
|
||||
:where(.yc-capability-center) .ec-modal-type-badge{border-radius:4px;align-items:center;height:20px;padding:0 6px;font-size:11px;font-weight:600;line-height:20px;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-badge-team{color:#4f46e5;background:#5865f214;border:1px solid #5865f229}
|
||||
:where(.yc-capability-center) .ec-badge-expert{background:color-mix(in srgb, var(--wb-bg-tertiary) 88%, transparent);color:var(--wb-color-text-secondary);border:1px solid var(--wb-border-subtle)}
|
||||
:where(.yc-capability-center) .ec-modal-category-badge{background:var(--wb-bg-secondary);color:var(--wb-color-text-secondary);border-radius:4px;align-items:center;padding:2px 8px;font-size:11px;font-weight:500;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-modal-use-count-row{align-items:center;min-width:0;margin-top:4px;display:flex}
|
||||
:where(.yc-capability-center) .ec-modal-use-count{color:var(--wb-color-text-tertiary);align-items:center;font-size:11px;font-weight:500;line-height:20px;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-modal-content,:where(.yc-capability-center) .ec-modal-tags-section{margin-bottom:18px}
|
||||
:where(.yc-capability-center) .ec-modal-tags-list{flex-wrap:wrap;gap:8px;display:flex}
|
||||
:where(.yc-capability-center) .ec-modal-tag-chip{background:var(--wb-bg-hover);color:var(--wb-color-text-secondary);border:1px solid var(--wb-border-weak);box-sizing:border-box;white-space:nowrap;border-radius:6px;align-items:center;gap:5px;height:30px;padding:0 12px;font-size:12px;line-height:1;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-modal-quick-prompts{margin-bottom:16px}
|
||||
:where(.yc-capability-center) .ec-modal-quick-prompts-list{flex-direction:column;gap:10px;display:flex}
|
||||
:where(.yc-capability-center) .ec-modal-quick-prompt-btn{text-align:left;width:100%;color:var(--wb-color-text-secondary);cursor:pointer;background:var(--wb-bg-hover);border-style:solid;border-width:1px;border-color:var(--wb-border-weak);border-image:initial;border-radius:8px;justify-content:space-between;align-items:center;gap:12px;padding:10px 14px;font-size:13px;line-height:1.5;transition:background .12s;display:flex}
|
||||
:where(.yc-capability-center) .ec-modal-quick-prompt-btn:hover{background:var(--wb-bg-active)}
|
||||
:where(.yc-capability-center) .ec-modal-quick-prompt-text{min-width:0;font-size:inherit;line-height:inherit;color:inherit;flex:1}
|
||||
:where(.yc-capability-center) .ec-modal-quick-prompt-arrow{width:18px;height:18px;color:var(--wb-color-text-tertiary);flex-shrink:0;justify-content:center;align-items:center;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-modal-section-title{color:var(--wb-color-text-tertiary);margin-bottom:8px;font-size:12px;font-weight:600}
|
||||
:where(.yc-capability-center) .ec-modal-desc{color:var(--wb-color-text-secondary);margin:0;font-size:13px;line-height:1.6}
|
||||
:where(.yc-capability-center) .ec-modal-team{margin-bottom:18px}
|
||||
:where(.yc-capability-center) .ec-modal-team-list{flex-direction:column;gap:0;display:flex}
|
||||
:where(.yc-capability-center) .ec-modal-skills{margin-bottom:18px}
|
||||
:where(.yc-capability-center) .ec-modal-skills-list{flex-wrap:wrap;gap:8px;display:flex}
|
||||
:where(.yc-capability-center) .ec-modal-skill-tag{height:30px;color:var(--wb-color-text-secondary);background:var(--wb-bg-secondary);border:1px solid var(--wb-border-default);white-space:nowrap;box-sizing:border-box;border-radius:6px;align-items:center;gap:5px;padding:0 12px;font-size:12px;font-weight:500;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-modal-skill-icon{font-size:13px;line-height:1}
|
||||
:where(.yc-capability-center) .ec-modal-team-member{border-bottom:1px solid #00000014;align-items:center;gap:10px;padding:8px 0;display:flex}
|
||||
:where(.yc-capability-center) .ec-modal-team-member:last-child{border-bottom:none}
|
||||
:where(.yc-capability-center) .ec-modal-member-avatar{background:var(--wb-bg-secondary);border-radius:6px;flex-shrink:0;justify-content:center;align-items:center;width:28px;height:28px;display:flex;overflow:hidden}
|
||||
:where(.yc-capability-center) .ec-modal-member-avatar img{object-fit:cover;width:100%;height:100%;display:block}
|
||||
:where(.yc-capability-center) .ec-modal-member-avatar-placeholder{width:100%;height:100%;color:var(--wb-button-primary-fg);justify-content:center;align-items:center;font-size:12px;font-weight:600;display:flex}
|
||||
:where(.yc-capability-center) .ec-modal-member-info{flex-flow:row;flex:1;align-items:center;gap:6px;min-width:0;display:flex;overflow:hidden}
|
||||
:where(.yc-capability-center) .ec-modal-member-name{color:var(--wb-color-text-tertiary);white-space:nowrap;text-overflow:ellipsis;flex:0 auto;min-width:0;font-size:11px;overflow:hidden}
|
||||
:where(.yc-capability-center) .ec-modal-member-role{color:var(--wb-color-text-primary);white-space:nowrap;flex-shrink:0;font-size:13px;font-weight:600}
|
||||
:where(.yc-capability-center) .ec-modal-member-skeleton-avatar{width:100%;height:100%}
|
||||
:where(.yc-capability-center) .ec-modal-member-skeleton-role{border-radius:4px;width:72px;height:13px;display:inline-block}
|
||||
:where(.yc-capability-center) .ec-modal-member-skeleton-name{border-radius:4px;width:48px;height:11px;display:inline-block}
|
||||
:where(.yc-capability-center) .ec-modal-leader-badge{background:var(--wb-bg-warning-soft-strong);color:var(--wb-badge-warning-fg);white-space:nowrap;border-radius:20px;flex-shrink:0;justify-content:flex-start;align-items:center;gap:3px;padding:1px 6px;font-size:10px;font-weight:600;display:inline-flex}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-modal-leader-badge{background:var(--wb-palette-white-20);color:var(--wb-palette-white-80)}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-modal-leader-badge .ec-modal-leader-badge-icon{filter:grayscale()invert();opacity:.8}
|
||||
:where(.yc-capability-center) .ec-modal-footer{border-top:1px solid var(--wb-border-default);background:var(--wb-bg-primary);flex-shrink:0;justify-content:center;padding:16px 20px 20px;display:flex}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-modal-footer{border-top:1px solid color-mix(in srgb, var(--wb-color-text-tertiary) 10%, transparent)}
|
||||
:where(.yc-capability-center) .ec-modal-summon-btn{background:var(--wb-button-primary-bg);width:100%;height:42px;color:var(--wb-button-primary-fg);cursor:pointer;letter-spacing:.2px;border:none;border-radius:8px;padding:0 24px;font-size:14px;font-weight:600;transition:background .15s,opacity .15s,transform .15s}
|
||||
:where(.yc-capability-center) .ec-modal-summon-btn:hover{background:var(--wb-button-primary-bg-hover);opacity:.96}
|
||||
:where(.yc-capability-center) .ec-modal-summon-btn:active{transform:translateY(0)}
|
||||
:where(.yc-capability-center) .ec-finance-risk-dialog-title{align-items:center;font-weight:600;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-confirm-dialog-content{gap:var(--wb-spacing-5);flex-direction:column;display:flex}
|
||||
:where(.yc-capability-center) .ec-confirm-dialog-checkbox-group{flex-direction:column;gap:8px;display:flex}
|
||||
:where(.yc-capability-center) .ec-confirm-dialog-checkbox{color:inherit;cursor:pointer;align-items:center;gap:8px;font-size:12px;line-height:1.5;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-confirm-dialog-checkbox input{accent-color:currentColor;margin:0}
|
||||
:where(.yc-capability-center) .ec-expert-usage-dialog-overlay[data-appearance=workbuddy]{backdrop-filter:blur(var(--wb-spacing-4));background:var(--wb-modal-overlay-mask-color)!important}
|
||||
:where(.yc-capability-center) .ec-finance-risk-confirm-overlay[data-theme=dark] [style*=--cb-button-dark-background]{color:var(--wb-button-primary-fg)!important}
|
||||
:where(.yc-capability-center) .ec-finance-risk-confirm-overlay[data-theme=dark] [style*=--cb-button-dark-background]:hover:not(:disabled),:where(.yc-capability-center) .ec-finance-risk-confirm-overlay[data-theme=dark] [style*=--cb-button-dark-background]:active:not(:disabled){background-color:var(--wb-button-primary-bg-active)!important}
|
||||
:where(.yc-capability-center) .ec-my-panel{box-sizing:border-box;min-width:0;padding:20px var(--ec-content-gutter);flex-direction:column;flex:1;align-items:stretch;display:flex}
|
||||
:where(.yc-capability-center) .ec-my-sub-tabs{background:var(--wb-bg-tertiary);border-radius:12px;align-items:center;gap:4px;margin-bottom:24px;padding:4px;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-my-sub-tab{color:var(--wb-color-text-tertiary);cursor:pointer;letter-spacing:-.1px;background:0 0;border:none;border-radius:9px;padding:8px 22px;font-size:14px;font-weight:600;transition:all .25s cubic-bezier(.4,0,.2,1);position:relative}
|
||||
:where(.yc-capability-center) .ec-my-sub-tab:hover{color:var(--wb-color-text-secondary)}
|
||||
:where(.yc-capability-center) .ec-my-sub-tab.is-active{color:var(--wb-color-text-primary);background:var(--wb-bg-card);font-weight:700;box-shadow:0 1px 4px #00000014}
|
||||
:where(.yc-capability-center) .ec-my-recent-hint{color:var(--wb-color-text-tertiary);background:var(--wb-brand-accent-bg);border-radius:10px;align-items:center;gap:6px;margin-bottom:18px;padding:10px 16px;font-size:12px;font-weight:500;display:flex}
|
||||
:where(.yc-capability-center) .ec-my-recent-hint svg{width:14px;height:14px;color:var(--wb-brand-accent);flex-shrink:0}
|
||||
:where(.yc-capability-center) .ec-my-created-toolbar{justify-content:space-between;align-items:center;gap:var(--wb-spacing-4);margin-bottom:var(--wb-spacing-5);display:flex}
|
||||
:where(.yc-capability-center) .ec-list-tabs{flex-shrink:0;align-items:center;gap:20px;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-list-tab{color:var(--wb-text-weak);white-space:nowrap;cursor:pointer;background:0 0;border:none;align-items:center;gap:6px;padding:0;font-size:20px;font-weight:600;line-height:28px;transition:opacity .15s;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-list-tab:hover,:where(.yc-capability-center) .ec-list-tab.is-active{color:var(--wb-color-text-primary)}
|
||||
:where(.yc-capability-center) .ec-my-created-tabs{min-width:0}
|
||||
:where(.yc-capability-center) .ec-my-created-tab-label{color:var(--wb-palette-black-50);font-family:PingFang SC;font-weight:var(--wb-font-weight-500)}
|
||||
:where(.yc-capability-center) .ec-my-created-tabs [role=tab][aria-selected=true] .ec-my-created-tab-label{color:var(--wb-palette-black-90);font-weight:var(--wb-font-weight-600)}
|
||||
:where(.yc-capability-center) .ec-my-created-tab-count{color:var(--wb-palette-black-50);font-family:PingFang SC;font-size:12px;font-weight:var(--wb-font-weight-400);background:#0000000f;border-radius:10px;flex-shrink:0;justify-content:center;align-items:center;padding:0 6px;font-style:normal;line-height:20px;text-decoration-line:none;display:flex}
|
||||
:where(.yc-capability-center) .ec-my-created-tabs [role=tab][aria-selected=true] .ec-my-created-tab-count{color:var(--wb-palette-black-90)}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-my-created-tab-label,:where(.yc-capability-center) .expert-center-dark .ec-my-created-tab-count{color:var(--wb-palette-white-50)}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-my-created-tabs [role=tab][aria-selected=true] .ec-my-created-tab-label,:where(.yc-capability-center) .expert-center-dark .ec-my-created-tabs [role=tab][aria-selected=true] .ec-my-created-tab-count{color:var(--wb-palette-white-90)}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-my-created-tab-count{background:#ffffff14}
|
||||
:where(.yc-capability-center) .ec-my-cloud-upload-all{height:28px;color:var(--wb-color-text-tertiary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex-shrink:0;justify-content:center;align-items:center;gap:4px;padding:0 6px;font-size:13px;font-weight:400;line-height:20px;transition:background .15s,color .15s;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-my-cloud-upload-all:hover:not(:disabled){background:var(--wb-bg-hover);color:var(--wb-color-text-primary)}
|
||||
:where(.yc-capability-center) .ec-my-cloud-upload-all:disabled{cursor:not-allowed;opacity:.6}
|
||||
:where(.yc-capability-center) .ec-my-cloud-upload-all__icon{flex-shrink:0;width:16px;height:16px}
|
||||
:where(.yc-capability-center) .ec-my-toolbar-right{align-items:center;gap:var(--wb-spacing-2,8px);min-width:0;margin-left:auto;display:flex}
|
||||
:where(.yc-capability-center) .ec-my-toolbar-right .ec-search-wrapper{margin-left:0}
|
||||
@media (width<=540px){:where(.yc-capability-center) .ec-my-toolbar-right{flex-basis:100%;width:100%}}
|
||||
:where(.yc-capability-center) .ec-my-mode-row{margin-bottom:var(--wb-spacing-3,12px);justify-content:flex-end;display:flex}
|
||||
:where(.yc-capability-center) .ec-purchased-card--expired .ec-card-avatar-sq{opacity:.35}
|
||||
:where(.yc-capability-center) .ec-purchased-card--expired .ec-cr-more-action{top:0;right:calc(var(--wb-spacing-12) + var(--wb-spacing-4));z-index:3;position:absolute}
|
||||
:where(.yc-capability-center) .ec-purchased-empty{flex-direction:column;align-items:center;padding-top:130px;display:flex}
|
||||
:where(.yc-capability-center) .ec-purchased-empty--error{gap:var(--wb-spacing-4,12px);padding-top:var(--wb-spacing-12,64px)}
|
||||
:where(.yc-capability-center) .ec-purchased-empty-icon{color:var(--wb-color-text-primary);margin-bottom:var(--wb-spacing-5,16px)}
|
||||
:where(.yc-capability-center) .ec-purchased-empty-copy{align-items:center;gap:var(--wb-spacing-3,8px);margin-bottom:var(--wb-spacing-5,16px);flex-direction:column;display:flex}
|
||||
:where(.yc-capability-center) .ec-purchased-empty-text{color:var(--wb-color-text-primary);margin:0;font-size:14px;font-weight:500}
|
||||
:where(.yc-capability-center) .ec-purchased-empty-hint{color:var(--wb-color-text-tertiary);margin:0;font-size:12px;font-weight:400}
|
||||
@media (width<=540px){:where(.yc-capability-center) .ec-my-created-toolbar{flex-wrap:wrap}}
|
||||
:where(.yc-capability-center) .ec-my-grid{gap:var(--wb-spacing-5);grid-template-columns:repeat(auto-fill,minmax(280px,1fr));align-content:start;display:grid}
|
||||
@media (width<=1279px){:where(.yc-capability-center) .ec-my-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
@media (width<=767px){:where(.yc-capability-center) .ec-my-grid{gap:var(--wb-spacing-5);grid-template-columns:repeat(2,1fr)}}
|
||||
@media (width<=575px){:where(.yc-capability-center) .ec-my-grid{grid-template-columns:1fr}}
|
||||
:where(.yc-capability-center) .ec-my-recent-empty{flex-direction:column;align-items:center;padding:15% 40px 60px;display:flex}
|
||||
:where(.yc-capability-center) .ec-my-recent-empty-icon{background:linear-gradient(145deg,#f0edff,#e5deff);border-radius:20px;justify-content:center;align-items:center;width:72px;height:72px;margin-bottom:16px;font-size:32px;display:flex;box-shadow:0 4px 16px #7c5cfc1a}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-my-recent-empty-icon{background:linear-gradient(145deg,#2d2a3e,#3a3550)}
|
||||
:where(.yc-capability-center) .ec-my-recent-empty-title{color:var(--wb-color-text-secondary);margin:0 0 4px;font-size:14px;font-weight:600}
|
||||
:where(.yc-capability-center) .ec-my-recent-empty-hint{color:var(--wb-color-text-tertiary);margin:0;font-size:12px;font-weight:400}
|
||||
:where(.yc-capability-center) .ec-cr-card{position:relative}
|
||||
:where(.yc-capability-center) .ec-card-summon-btn{background:var(--wb-button-primary-bg);height:24px;color:var(--wb-button-primary-fg);white-space:nowrap;box-shadow:none;cursor:pointer;border:none;border-radius:8px;outline:none;flex-shrink:0;justify-content:center;align-items:center;padding:0 10px;font-size:12px;font-weight:500;line-height:24px;transition:background .15s,color .15s;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-card-summon-btn--top{z-index:2;opacity:0;pointer-events:none;transition:opacity .15s,background .15s,color .15s;position:absolute;top:16px;right:20px}
|
||||
:where(.yc-capability-center) .ec-card-summon-btn--top:before{content:"";background:linear-gradient(to right, transparent, var(--wb-bg-card));pointer-events:none;width:40px;height:100%;position:absolute;top:0;right:100%}
|
||||
:where(.yc-capability-center) .ec-cr-card:hover .ec-card-summon-btn--top:before,:where(.yc-capability-center) .ec-cr-card:focus-within .ec-card-summon-btn--top:before{background:linear-gradient(to right, transparent, color-mix(in srgb, var(--wb-palette-black-100) 5%, var(--wb-bg-card)))}
|
||||
:where(.yc-capability-center) .ec-cr-card:hover .ec-card-summon-btn--top,:where(.yc-capability-center) .ec-cr-card:focus-within .ec-card-summon-btn--top{opacity:1;pointer-events:auto}
|
||||
:where(.yc-capability-center) .ec-card-summon-btn:hover{background:var(--wb-button-primary-bg-hover);color:var(--wb-button-primary-fg);border-color:#0000}
|
||||
:where(.yc-capability-center) .ec-card-summon-btn:focus,:where(.yc-capability-center) .ec-card-summon-btn:focus-visible,:where(.yc-capability-center) .ec-card-summon-btn:active{background:var(--wb-button-primary-bg-hover);color:var(--wb-button-primary-fg);box-shadow:none;border-color:#0000;outline:none;transform:none}
|
||||
:where(.yc-capability-center) .ec-cr-card .ec-card-summon-btn--top{width:calc(var(--wb-spacing-12) + var(--wb-spacing-2));box-sizing:border-box}
|
||||
:where(.yc-capability-center) .ec-cr-cloud-icon{top:var(--wb-spacing-6);right:calc(var(--wb-spacing-12) + var(--wb-spacing-8) + var(--wb-spacing-8));z-index:3;width:20px;height:20px;color:var(--wb-color-text-primary);cursor:pointer;pointer-events:auto;background:0 0;border:0;border-radius:6px;justify-content:center;align-items:center;padding:2px;display:inline-flex;position:absolute}
|
||||
:where(.yc-capability-center) .ec-cr-cloud-icon.ec-cr-cloud-icon--top-action{position:static}
|
||||
:where(.yc-capability-center) .ec-cr-cloud-icon:hover:not(.is-loading):not(.is-disabled){background:var(--wb-bg-hover)}
|
||||
:where(.yc-capability-center) .ec-cr-cloud-icon.is-disabled{cursor:default}
|
||||
:where(.yc-capability-center) .ec-cr-cloud-icon.is-loading{cursor:not-allowed;opacity:.72}
|
||||
:where(.yc-capability-center) .ec-cr-cloud-icon__svg{width:16px;height:16px;display:block}
|
||||
:where(.yc-capability-center) .ec-cr-cloud-icon__spinner{border:2px solid;border-top-color:#0000;border-radius:50%;width:12px;height:12px;animation:.8s linear infinite ec-cloud-action-spin}
|
||||
@keyframes ec-cloud-action-spin{to{transform:rotate(360deg)}}
|
||||
:where(.yc-capability-center) .ec-cr-cloud-icon--local_ahead,:where(.yc-capability-center) .ec-cr-cloud-icon--local_only,:where(.yc-capability-center) .ec-cr-cloud-icon--checking,:where(.yc-capability-center) .ec-cr-cloud-icon--failed,:where(.yc-capability-center) .ec-cr-cloud-icon--invalid,:where(.yc-capability-center) .ec-cr-cloud-icon--cloud_ahead,:where(.yc-capability-center) .ec-cr-cloud-icon--cloud_only,:where(.yc-capability-center) .ec-cr-cloud-icon--both{color:var(--wb-color-text-primary)}
|
||||
:where(.yc-capability-center) .ec-cr-card:hover .ec-cr-more-btn--top,:where(.yc-capability-center) .ec-cr-card:focus-within .ec-cr-more-btn--top,:where(.yc-capability-center) .ec-cr-card .ec-cr-more-btn--top.is-visible{opacity:1;pointer-events:auto}
|
||||
:where(.yc-capability-center) .ec-cr-more-action{flex-shrink:0;position:relative}
|
||||
:where(.yc-capability-center) .ec-cr-more-btn{border-radius:var(--wb-radius-lg);cursor:pointer;width:24px;height:24px;color:var(--wb-color-text-secondary);opacity:0;pointer-events:none;background:0 0;border:none;flex-shrink:0;justify-content:center;align-items:center;padding:0;transition:opacity .15s,background .15s,color .15s;display:flex;position:relative}
|
||||
:where(.yc-capability-center) .ec-cr-more-btn:before{content:"";border-radius:inherit;background:0 0;transition:background .15s;position:absolute;inset:0}
|
||||
:where(.yc-capability-center) .ec-cr-more-btn svg{position:relative}
|
||||
:where(.yc-capability-center) .ec-cr-more-btn:hover,:where(.yc-capability-center) .ec-cr-more-btn:focus-visible,:where(.yc-capability-center) .ec-cr-more-btn.is-active{color:var(--wb-color-text-primary)}
|
||||
:where(.yc-capability-center) .ec-cr-more-btn:hover:before,:where(.yc-capability-center) .ec-cr-more-btn:focus-visible:before,:where(.yc-capability-center) .ec-cr-more-btn.is-active:before{background:var(--wb-bg-hover)}
|
||||
:where(.yc-capability-center) .ec-share-modal-card .ec-share-body{padding:0 20px 16px}
|
||||
:where(.yc-capability-center) .ec-share-modal-card .ec-share-footer{flex-direction:column;justify-content:center;align-items:stretch;gap:8px}
|
||||
:where(.yc-capability-center) .ec-share-modal-card .ec-share-copy-btn{justify-content:center}
|
||||
:where(.yc-capability-center) .ec-share-footer-tip{color:var(--wb-color-text-secondary);text-align:center;justify-content:center;align-items:center;gap:6px;font-size:12px;line-height:1.5;display:flex}
|
||||
:where(.yc-capability-center) .ec-share-footer-tip svg{width:13px;height:13px;color:var(--wb-status-warning-muted);flex-shrink:0}
|
||||
:where(.yc-capability-center) .ec-share-modal{background:var(--wb-bg-card);border-radius:16px;flex-direction:column;width:min(540px,100vw - 48px);max-height:min(680px,100vh - 48px);display:flex;position:relative;overflow:hidden;box-shadow:0 24px 80px #0000001f,0 8px 32px #00000014}
|
||||
:where(.yc-capability-center) .ec-share-title{color:var(--wb-color-text-primary);flex-shrink:0;margin:0;padding:24px 48px 0 24px;font-size:16px;font-weight:600;line-height:1.4}
|
||||
:where(.yc-capability-center) .ec-share-title-name{color:var(--wb-brand-accent)}
|
||||
:where(.yc-capability-center) .ec-share-warning{background:var(--wb-alert-warning-bg);color:var(--wb-badge-gold-fg);border-radius:10px;flex-shrink:0;align-items:center;gap:8px;margin:16px 24px 0;padding:10px 14px;font-size:12px;line-height:1.5;display:flex}
|
||||
:where(.yc-capability-center) .ec-share-warning svg{color:var(--wb-badge-gold);flex-shrink:0}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-share-warning{color:var(--wb-badge-gold-bg);background:#d4a0171f}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-share-warning svg{color:var(--wb-badge-gold-bg)}
|
||||
:where(.yc-capability-center) .ec-share-body{flex-direction:column;flex:1;gap:12px;min-height:0;padding:16px 24px;display:flex;overflow-y:auto}
|
||||
:where(.yc-capability-center) .ec-share-section{border:1px solid var(--wb-border-default);border-radius:12px;overflow:hidden}
|
||||
:where(.yc-capability-center) .ec-share-section-header{cursor:pointer;width:100%;color:var(--wb-color-text-primary);text-align:left;background:0 0;border:none;align-items:center;gap:8px;padding:12px 14px;font-size:13px;font-weight:600;transition:background .15s;display:flex}
|
||||
:where(.yc-capability-center) .ec-share-section-header:hover{background:var(--wb-brand-accent-bg)}
|
||||
:where(.yc-capability-center) .ec-share-section-icon{color:var(--wb-color-text-tertiary);align-items:center;display:flex}
|
||||
:where(.yc-capability-center) .ec-share-section-label{flex:1}
|
||||
:where(.yc-capability-center) .ec-share-section-toggle{color:var(--wb-color-text-tertiary);align-items:center;display:flex}
|
||||
:where(.yc-capability-center) .ec-share-prompt-content{color:var(--wb-color-text-secondary);white-space:pre-wrap;word-break:break-word;border-top:1px solid var(--wb-border-default);background:color-mix(in srgb, var(--wb-bg-card) 50%, transparent);max-height:200px;margin-top:0;padding:12px 14px 14px;font-size:13px;line-height:1.7;overflow-y:auto}
|
||||
:where(.yc-capability-center) .ec-share-skills-list{border-top:1px solid var(--wb-border-default);flex-wrap:wrap;gap:8px;padding:12px 14px 14px;display:flex}
|
||||
:where(.yc-capability-center) .ec-share-skill-chip{border:1.5px solid var(--wb-brand-accent);color:var(--wb-brand-accent);white-space:nowrap;background:0 0;border-radius:20px;align-items:center;gap:6px;padding:6px 14px;font-size:12px;font-weight:500;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-share-skill-chip svg{width:13px;height:13px}
|
||||
:where(.yc-capability-center) .ec-share-footer{flex-shrink:0;justify-content:flex-end;padding:16px 24px 20px;display:flex}
|
||||
:where(.yc-capability-center) .ec-share-copy-btn{background:var(--wb-brand-accent);color:var(--wb-button-primary-fg);cursor:pointer;border:none;border-radius:10px;align-items:center;gap:8px;padding:10px 28px;font-size:13px;font-weight:600;transition:opacity .15s;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-share-copy-btn:hover{opacity:.9}
|
||||
:where(.yc-capability-center) .ec-share-copy-btn:disabled{opacity:.7;cursor:not-allowed}
|
||||
:where(.yc-capability-center) .ec-share-copy-btn svg{flex-shrink:0}
|
||||
:where(.yc-capability-center) .ec-share-spinner{border:2px solid #fff6;border-top-color:var(--wb-border-default);border-radius:50%;flex-shrink:0;width:14px;height:14px;animation:.8s linear infinite ec-share-spinner-rotate}
|
||||
@keyframes ec-share-spinner-rotate{to{transform:rotate(360deg)}}
|
||||
:where(.yc-capability-center) .expert-import-modal-overlay{background:color-mix(in srgb, var(--wb-bg-secondary) 78%, transparent)}
|
||||
:where(.yc-capability-center) .expert-import-modal-shell{flex-direction:column;align-items:center;gap:28px;max-width:90vw;max-height:calc(100vh - 40px);display:flex}
|
||||
:where(.yc-capability-center) .expert-import-modal-heading{color:var(--wb-color-text-primary);text-align:center;text-shadow:0 1px 2px var(--wb-bg-primary);font-size:20px;font-weight:600;line-height:1.4}
|
||||
:where(.yc-capability-center) .expert-share-prompt-preview,:where(.yc-capability-center) .expert-import-prompt-preview{cursor:default}
|
||||
:where(.yc-capability-center) .expert-share-prompt-preview:hover,:where(.yc-capability-center) .expert-import-prompt-preview:hover{background:var(--wb-bg-hover)}
|
||||
:where(.yc-capability-center) .expert-import-modal-card .ec-modal-close:disabled,:where(.yc-capability-center) .expert-import-modal-card .ec-modal-summon-btn:disabled{cursor:not-allowed;opacity:.7}
|
||||
:where(.yc-capability-center) .expert-import-loading-card{justify-content:center;width:360px;min-height:160px}
|
||||
:where(.yc-capability-center) .expert-import-loading-content{flex-direction:column;align-items:center;gap:14px;padding:24px;display:flex}
|
||||
:where(.yc-capability-center) .expert-import-loading-spinner{border:2px solid color-mix(in srgb, var(--wb-text-muted) 24%, transparent);border-top-color:var(--wb-color-text-primary);border-radius:50%;width:22px;height:22px;animation:.8s linear infinite ec-share-spinner-rotate}
|
||||
:where(.yc-capability-center) .expert-import-loading-text{color:var(--wb-color-text-secondary);text-align:center;font-size:13px;line-height:1.5}
|
||||
:where(.yc-capability-center) .expert-import-security-card{background:var(--wb-bg-card);border-radius:16px;width:min(560px,100vw - 48px);padding:28px 32px 30px;position:relative;box-shadow:0 24px 80px #0000002e,0 8px 32px #0000001a}
|
||||
:where(.yc-capability-center) .expert-import-security-close{width:28px;height:28px;color:var(--wb-color-text-tertiary);cursor:pointer;background:0 0;border:none;border-radius:8px;justify-content:center;align-items:center;padding:0;display:inline-flex;position:absolute;top:20px;right:20px}
|
||||
:where(.yc-capability-center) .expert-import-security-close:hover{background:var(--wb-bg-secondary);color:var(--wb-color-text-primary)}
|
||||
:where(.yc-capability-center) .expert-import-security-title{color:var(--wb-color-text-primary);margin:4px 40px 24px 0;font-size:20px;font-weight:600;line-height:1.4}
|
||||
:where(.yc-capability-center) .expert-import-security-status-box{border:1px dashed var(--wb-border-default);background:color-mix(in srgb, var(--wb-bg-card) 70%, transparent);border-radius:14px;flex-direction:column;justify-content:center;align-items:center;min-height:132px;margin-bottom:22px;display:flex}
|
||||
:where(.yc-capability-center) .expert-import-security-hourglass{margin-bottom:10px;font-size:30px;line-height:1}
|
||||
:where(.yc-capability-center) .expert-import-security-status-text{color:var(--wb-color-text-primary);font-size:15px;font-weight:600;line-height:1.5}
|
||||
:where(.yc-capability-center) .expert-import-security-desc{color:var(--wb-color-text-secondary);margin-bottom:18px;font-size:14px;line-height:1.7}
|
||||
:where(.yc-capability-center) .expert-import-security-checkbox{color:var(--wb-color-text-secondary);cursor:pointer;align-items:center;gap:8px;margin-bottom:22px;font-size:14px;line-height:1.5;display:inline-flex}
|
||||
:where(.yc-capability-center) .expert-import-security-checkbox input{cursor:pointer;width:18px;height:18px;margin:0}
|
||||
:where(.yc-capability-center) .expert-import-security-footer{flex-direction:column;justify-content:center;align-items:center;gap:8px;display:flex}
|
||||
:where(.yc-capability-center) .expert-import-security-skip-btn{border:1px solid var(--wb-border-default);background:var(--wb-bg-card);min-width:176px;height:40px;color:var(--wb-color-text-primary);cursor:pointer;border-radius:10px;padding:0 20px;font-size:14px;font-weight:600}
|
||||
:where(.yc-capability-center) .expert-import-security-skip-btn:hover{border-color:var(--wb-brand-accent);color:var(--wb-brand-accent)}
|
||||
:where(.yc-capability-center) .expert-import-security-skip-tip{color:var(--wb-color-text-tertiary);text-align:center;font-size:12px;line-height:1.5}
|
||||
:where(.yc-capability-center) .expert-import-prompt-preview{cursor:default;pointer-events:none}
|
||||
:where(.yc-capability-center) .expert-import-dialog-detail{flex-direction:column;gap:12px;display:flex}
|
||||
:where(.yc-capability-center) .expert-import-dialog-prompt{border:1px solid var(--wb-border-default);background:var(--wb-bg-card);border-radius:12px;overflow:hidden}
|
||||
:where(.yc-capability-center) .expert-import-dialog-section-title{color:var(--wb-color-text-primary);border-bottom:1px solid var(--wb-border-default);padding:10px 12px;font-size:13px;font-weight:600}
|
||||
:where(.yc-capability-center) .expert-import-dialog-prompt-content{white-space:pre-wrap;word-break:break-word;max-height:180px;color:var(--wb-color-text-secondary);padding:12px;font-size:13px;line-height:1.7;overflow-y:auto}
|
||||
:where(.yc-capability-center) .expert-import-hook-risk-trigger.wb-button{height:calc(var(--wb-spacing-9) - var(--wb-spacing-1));min-height:calc(var(--wb-spacing-9) - var(--wb-spacing-1));padding:var(--wb-spacing-3) var(--wb-spacing-4);border:calc(var(--wb-spacing-1) / 4) solid var(--wb-color-border-solid);border-radius:var(--wb-radius-lg);background:var(--wb-color-bg-secondary-hoveractive,var(--wb-bg-secondary));color:var(--wb-color-text-primary);text-align:left;flex-shrink:0;justify-content:space-between;align-items:center;display:flex}
|
||||
:where(.yc-capability-center) .expert-import-hook-risk-trigger.wb-button:hover{background:var(--wb-color-bg-secondary-hoveractive,var(--wb-bg-secondary))}
|
||||
:where(.yc-capability-center) .expert-import-hook-risk-trigger .wb-button__content{white-space:normal;flex:1}
|
||||
:where(.yc-capability-center) .expert-import-hook-risk-trigger-icon{color:var(--wb-status-error)}
|
||||
:where(.yc-capability-center) .expert-import-hook-risk-details{gap:var(--wb-spacing-5);max-height:calc(var(--wb-spacing-12) * 3 + var(--wb-spacing-7) + var(--wb-spacing-2));flex-direction:column;display:flex;overflow-y:auto}
|
||||
:where(.yc-capability-center) .expert-import-hook-risk-detail-item{gap:var(--wb-spacing-2);flex-direction:column;display:flex}
|
||||
:where(.yc-capability-center) .expert-import-hook-risk-detail-title{align-items:center;gap:var(--wb-spacing-3);color:var(--wb-color-text-primary);font-size:var(--wb-font-body-size);line-height:var(--wb-font-body-line-height);font-weight:var(--wb-font-body-strong-weight);display:flex}
|
||||
:where(.yc-capability-center) .expert-import-hook-risk-detail-title svg{flex-shrink:0}
|
||||
:where(.yc-capability-center) .expert-import-hook-risk-detail-description{color:var(--wb-color-text-secondary);font-size:var(--wb-font-body-size);line-height:var(--wb-font-body-line-height);overflow-wrap:anywhere}
|
||||
:where(.yc-capability-center) .expert-import-hook-risk-detail-description code{padding:var(--wb-spacing-1) var(--wb-spacing-2);border-radius:var(--wb-radius-sm);background:var(--wb-bg-secondary);color:var(--wb-color-text-primary);font-family:var(--wb-font-mono);font-size:var(--wb-font-caption-size);line-height:var(--wb-font-caption-line-height);white-space:pre-wrap}
|
||||
:where(.yc-capability-center) .expert-import-dialog-error{background:var(--wb-bg-error-soft);color:var(--wb-alert-error-text);border-radius:8px;padding:10px 12px;font-size:12px;line-height:1.5}
|
||||
:where(.yc-capability-center) .expert-center-dark .expert-import-dialog-error{color:var(--wb-status-error);background:#f871711a}
|
||||
:where(.yc-capability-center) .ec-cr-status-badge{white-space:nowrap;border-radius:6px;flex-shrink:0;padding:2px 8px;font-size:10px;font-weight:600}
|
||||
:where(.yc-capability-center) .ec-cr-status-badge.is-unpublished{background:var(--wb-bg-tertiary);color:var(--wb-color-text-tertiary)}
|
||||
:where(.yc-capability-center) .ec-cr-status-badge.is-reviewing{background:var(--wb-alert-warning-bg);color:var(--wb-badge-warning-fg)}
|
||||
:where(.yc-capability-center) .ec-cr-status-badge.is-published{background:var(--wb-alert-success-bg);color:var(--wb-alert-success-text)}
|
||||
:where(.yc-capability-center) .ec-cr-status-badge.is-rejected{background:var(--wb-bg-error-soft);color:var(--wb-alert-error-text)}
|
||||
:where(.yc-capability-center) .ec-cr-status-badge.is-new-version{background:var(--wb-alert-info-bg);color:var(--wb-alert-info-text)}
|
||||
:where(.yc-capability-center) .ec-my-created-empty{justify-content:center;align-items:center;gap:var(--wb-spacing-4);flex-direction:column;flex:1;display:flex}
|
||||
:where(.yc-capability-center) .ec-my-created-empty-icon{background-color:var(--wb-palette-black-20);width:64px;height:64px;-webkit-mask-image:var(--ec-empty-created-icon);mask-image:var(--ec-empty-created-icon);flex-shrink:0;display:block;mask-position:50%;mask-size:contain;mask-repeat:no-repeat}
|
||||
:where(.yc-capability-center) .ec-my-created-empty-title{color:var(--wb-palette-black-30);font-family:PingFang SC;font-size:var(--wb-font-body-size);font-weight:var(--wb-font-body-strong-weight);line-height:var(--wb-font-body-strong-line-height);margin:0}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-my-created-empty-icon{background-color:var(--wb-palette-white-20)}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-my-created-empty-title{color:var(--wb-palette-white-30)}
|
||||
:where(.yc-capability-center) .ec-my-empty{color:var(--wb-color-text-tertiary);flex-direction:column;justify-content:center;align-items:center;padding:80px 40px;display:flex}
|
||||
:where(.yc-capability-center) .ec-my-empty .ec-empty-icon{width:64px;height:64px;color:var(--wb-border-default);margin-bottom:18px}
|
||||
:where(.yc-capability-center) .ec-my-empty-title{color:var(--wb-color-text-secondary);margin:0;font-size:15px;font-weight:600}
|
||||
:where(.yc-capability-center) .ec-my-empty-hint{color:var(--wb-color-text-tertiary);margin:8px 0 0;font-size:13px}
|
||||
@media (width<=1279px){:where(.yc-capability-center) .ec-card-grid{grid-template-columns:repeat(3,1fr)}}
|
||||
@media (width<=991px){:where(.yc-capability-center) .ec-card-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
@media (width<=767px){:where(.yc-capability-center) .expert-center-page{--ec-page-gutter:20px;--ec-content-gutter:20px}:where(.yc-capability-center) .ec-page-header{padding:16px var(--ec-content-gutter) 12px;gap:12px}:where(.yc-capability-center) .ec-main-tab{font-size:16px}:where(.yc-capability-center) .ec-recruit-banner{min-height:100px}:where(.yc-capability-center) .ec-recruit-title{font-size:16px}:where(.yc-capability-center) .ec-card-grid{grid-template-columns:repeat(2,1fr);gap:12px}}
|
||||
@media (width<=575px){:where(.yc-capability-center) .ec-card-grid{grid-template-columns:1fr}}
|
||||
:where(.yc-capability-center) .ec-detail-mask{background:var(--wb-color-mask-color);z-index:9999;opacity:0;visibility:hidden;justify-content:center;align-items:center;transition:opacity .3s,visibility .3s;display:flex;position:fixed;inset:0}
|
||||
:where(.yc-capability-center) .ec-detail-mask.is-visible{opacity:1;visibility:visible}
|
||||
:where(.yc-capability-center) .ec-detail-modal{background:var(--wb-bg-card);border-radius:16px;width:520px;max-height:80vh;transition:transform .3s;position:relative;overflow-y:auto;transform:translateY(20px)scale(.96);box-shadow:0 24px 80px #0000002e}
|
||||
:where(.yc-capability-center) .ec-detail-mask.is-visible .ec-detail-modal{transform:translateY(0)scale(1)}
|
||||
:where(.yc-capability-center) .ec-detail-modal::-webkit-scrollbar{width:4px}
|
||||
:where(.yc-capability-center) .ec-detail-modal::-webkit-scrollbar-thumb{background:var(--wb-border-default);border-radius:2px}
|
||||
:where(.yc-capability-center) .ec-detail-close{cursor:pointer;z-index:2;background:#0000000f;border:none;border-radius:50%;justify-content:center;align-items:center;width:32px;height:32px;transition:background .2s;display:flex;position:absolute;top:16px;right:16px}
|
||||
:where(.yc-capability-center) .ec-detail-close:hover{background:#0000001f}
|
||||
:where(.yc-capability-center) .ec-detail-close svg{width:16px;height:16px;stroke:var(--wb-color-text-secondary)}
|
||||
:where(.yc-capability-center) .ec-detail-header{text-align:center;padding:32px 32px 0}
|
||||
:where(.yc-capability-center) .ec-detail-avatar{object-fit:cover;border:3px solid var(--wb-tab-active-border);border-radius:50%;width:72px;height:72px;margin:0 auto 12px;display:block}
|
||||
:where(.yc-capability-center) .ec-detail-name{color:var(--wb-color-text-primary);margin:0 0 2px;font-size:18px;font-weight:700}
|
||||
:where(.yc-capability-center) .ec-detail-title{color:var(--wb-color-text-tertiary);margin:0 0 10px;font-size:13px}
|
||||
:where(.yc-capability-center) .ec-detail-desc{color:var(--wb-color-text-secondary);margin:0 0 20px;padding:0 8px;font-size:13px;line-height:1.6}
|
||||
:where(.yc-capability-center) .ec-detail-meta{border-top:1px solid var(--wb-border-default);border-bottom:1px solid var(--wb-border-default);justify-content:center;gap:0;margin:0 24px;padding:16px 24px;display:flex}
|
||||
:where(.yc-capability-center) .ec-detail-meta-item{text-align:center;flex:1;padding:0 8px}
|
||||
:where(.yc-capability-center) .ec-detail-meta-item:not(:last-child){border-right:1px solid var(--wb-border-default)}
|
||||
:where(.yc-capability-center) .ec-detail-meta-val{color:var(--wb-color-text-primary);margin-bottom:2px;font-size:14px;font-weight:700}
|
||||
:where(.yc-capability-center) .ec-detail-meta-label{color:var(--wb-color-text-tertiary);font-size:11px}
|
||||
:where(.yc-capability-center) .ec-detail-section{padding:20px 32px 0}
|
||||
:where(.yc-capability-center) .ec-detail-section h3{color:var(--wb-color-text-primary);margin:0 0 12px;font-size:15px;font-weight:700}
|
||||
:where(.yc-capability-center) .ec-detail-abilities{margin:0 0 20px;padding:0;list-style:none}
|
||||
:where(.yc-capability-center) .ec-detail-abilities li{color:var(--wb-color-text-primary);padding:6px 0 6px 20px;font-size:13px;line-height:1.7;position:relative}
|
||||
:where(.yc-capability-center) .ec-detail-abilities li:before{content:"";background:var(--wb-brand-accent);border-radius:50%;width:6px;height:6px;position:absolute;top:13px;left:0}
|
||||
:where(.yc-capability-center) .ec-detail-abilities-empty{color:var(--wb-color-text-tertiary);font-style:italic}
|
||||
:where(.yc-capability-center) .ec-detail-abilities-empty:before{display:none}
|
||||
:where(.yc-capability-center) .ec-detail-skills{padding:0 32px 16px}
|
||||
:where(.yc-capability-center) .ec-detail-skills h3{color:var(--wb-color-text-primary);margin:0 0 12px;font-size:15px;font-weight:700}
|
||||
:where(.yc-capability-center) .ec-detail-skills-list{flex-wrap:wrap;gap:8px;display:flex}
|
||||
:where(.yc-capability-center) .ec-detail-skill-tag{color:var(--wb-brand-accent);background:linear-gradient(135deg,#f5f2ff,#ede5ff);border:1px solid #7c5cfc1f;border-radius:20px;align-items:center;gap:4px;padding:6px 14px;font-size:12px;font-weight:600;transition:background .2s,border-color .2s;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-detail-skill-tag:hover{background:linear-gradient(135deg,#ede5ff,#e0d4ff);border-color:#7c5cfc40}
|
||||
:where(.yc-capability-center) .ec-detail-skill-tag svg{flex-shrink:0;width:12px;height:12px}
|
||||
:where(.yc-capability-center) .ec-detail-skills-empty{color:var(--wb-color-text-tertiary);font-size:12px;font-style:italic}
|
||||
:where(.yc-capability-center) .ec-detail-footer{background:var(--wb-bg-card);border-top:1px solid var(--wb-border-default);padding:16px 32px 24px;position:sticky;bottom:0}
|
||||
:where(.yc-capability-center) .ec-detail-summon{width:100%;color:var(--wb-button-primary-fg);background:linear-gradient(135deg, var(--wb-brand-accent), var(--wb-brand-accent));cursor:pointer;letter-spacing:.5px;border:none;border-radius:10px;padding:13px 0;font-size:15px;font-weight:700;transition:transform .2s,box-shadow .2s;box-shadow:0 4px 14px #7c5cfc4d}
|
||||
:where(.yc-capability-center) .ec-detail-summon:hover{transform:translateY(-1px);box-shadow:0 4px 14px #7c5cfc4d,0 6px 20px #7c5cfc40}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-detail-modal{box-shadow:0 24px 80px #0006}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-detail-close{background:#ffffff1a}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-detail-close:hover{background:#ffffff2e}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-detail-close svg{stroke:var(--wb-color-text-tertiary)}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-detail-avatar{border-color:var(--wb-accent-purple-light)}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-detail-desc{color:var(--wb-color-text-tertiary)}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-detail-abilities li{color:var(--wb-color-text-disabled)}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-detail-skill-tag{background:linear-gradient(135deg,#2d2a3e,#3a3550);border-color:#7c5cfc33}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-detail-skill-tag:hover{background:linear-gradient(135deg,#3a3550,#4a4065)}
|
||||
:where(.yc-capability-center) .expert-center-light.theme-switching .wb-button,:where(.yc-capability-center) body.theme-switching .wb-button{transition:none}
|
||||
:where(.yc-capability-center) .teams-container.is-mobile .expert-center-page{--ec-page-gutter:var(--wb-spacing-5);--ec-content-gutter:var(--wb-spacing-5)}
|
||||
:where(.yc-capability-center) .teams-container.is-mobile .ec-topbar{-webkit-overflow-scrolling:touch;scrollbar-width:none;flex-wrap:nowrap;overflow:auto hidden}
|
||||
:where(.yc-capability-center) .teams-container.is-mobile .ec-topbar::-webkit-scrollbar{display:none}
|
||||
:where(.yc-capability-center) .teams-container.is-mobile .ec-topbar__left,:where(.yc-capability-center) .teams-container.is-mobile .ec-topbar__right{flex-shrink:0}
|
||||
:where(.yc-capability-center) ._wrap_zzafx_1{margin-bottom:var(--wb-spacing-5);min-width:0;position:relative;overflow:hidden}
|
||||
:where(.yc-capability-center) ._wrap_zzafx_1:before,:where(.yc-capability-center) ._wrap_zzafx_1:after{content:"";z-index:1;width:var(--wb-spacing-10);pointer-events:none;opacity:0;height:100%;transition:opacity .2s;position:absolute;top:0}
|
||||
:where(.yc-capability-center) ._wrap_zzafx_1:before{background:linear-gradient(to left, transparent 0%, var(--wb-home-bg-secondary) 100%);left:0}
|
||||
:where(.yc-capability-center) ._wrap_zzafx_1:after{background:linear-gradient(to right, transparent 0%, var(--wb-home-bg-secondary) 100%);right:0}
|
||||
:where(.yc-capability-center) ._wrap_zzafx_1._maskedStart_zzafx_27:before,:where(.yc-capability-center) ._wrap_zzafx_1._maskedEnd_zzafx_28:after{opacity:1}
|
||||
:where(.yc-capability-center) .ec-main-scroll--split ._wrap_zzafx_1{flex-shrink:0}
|
||||
:where(.yc-capability-center) ._tabs_zzafx_35{align-items:center;gap:var(--wb-spacing-2);cursor:grab;user-select:none;scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;flex-wrap:nowrap;width:100%;min-width:0;padding:0 0 2px;display:flex;overflow:auto hidden}
|
||||
:where(.yc-capability-center) ._tabs_zzafx_35::-webkit-scrollbar{display:none}
|
||||
:where(.yc-capability-center) ._tabs_zzafx_35._dragging_zzafx_54,:where(.yc-capability-center) ._tabs_zzafx_35._dragging_zzafx_54 *{cursor:grabbing}
|
||||
:where(.yc-capability-center) ._next_zzafx_58{z-index:2;border:1px solid var(--wb-border-subtle);background:var(--wb-bg-primary);width:20px;height:20px;color:var(--wb-color-text-tertiary);box-shadow:var(--wb-shadow-popup-medium);cursor:pointer;border-radius:999px;justify-content:center;align-items:center;padding:0;transition:color .12s,border-color .12s;display:inline-flex;position:absolute;top:50%;right:0;transform:translateY(-50%)}
|
||||
:where(.yc-capability-center) ._next_zzafx_58:hover{color:var(--wb-color-text-primary);border-color:var(--wb-border-default)}
|
||||
:where(.yc-capability-center) ._nextIcon_zzafx_82{border-top:1.5px solid;border-right:1.5px solid;width:6px;height:6px;transform:translate(-1px)rotate(45deg)}
|
||||
:where(.yc-capability-center) ._prev_zzafx_89{left:0;right:auto}
|
||||
:where(.yc-capability-center) ._prevIcon_zzafx_93{transform:translate(1px)rotate(-135deg)}
|
||||
:where(.yc-capability-center) ._tab_zzafx_35{color:var(--wb-color-text-tertiary);font-size:var(--wb-font-body-size);font-weight:500;line-height:var(--wb-font-body-line-height);white-space:nowrap;cursor:pointer;padding:0 var(--wb-spacing-4);height:var(--wb-spacing-8);border-radius:var(--wb-radius-lg);background:0 0;border:none;flex-shrink:0;align-items:center;transition:background .15s,color .15s;display:inline-flex}
|
||||
:where(.yc-capability-center) ._tab_zzafx_35:hover{background:var(--wb-bg-hover);color:var(--wb-color-text-secondary)}
|
||||
:where(.yc-capability-center) ._tab_zzafx_35._active_zzafx_117{background:var(--wb-bg-inset-strong);color:var(--wb-color-text-primary);font-weight:600}
|
||||
:where(.yc-capability-center) .expert-center-dark ._tab_zzafx_35:hover{color:var(--ec-text-primary);background:#212121}
|
||||
:where(.yc-capability-center) .expert-center-dark ._tab_zzafx_35._active_zzafx_117{background:#242424}
|
||||
:where(.yc-capability-center) .expert-center-light.theme-switching ._tab_zzafx_35,:where(.yc-capability-center) body.theme-switching ._tab_zzafx_35{transition:none}
|
||||
@media (width<=767px){:where(.yc-capability-center) ._tabs_zzafx_35{padding:0 var(--ec-page-gutter)}:where(.yc-capability-center) ._tab_zzafx_35{padding:4px 10px;font-size:12px}}
|
||||
:where(.yc-capability-center) .teams-container.is-mobile ._wrap_zzafx_1{overflow-x:auto}
|
||||
:where(.yc-capability-center) ._root_pmhio_1{color:var(--wb-color-text-tertiary);flex-direction:column;justify-content:center;align-items:center;padding:60px 20px;display:flex}
|
||||
:where(.yc-capability-center) ._icon_pmhio_9{width:64px;height:64px;color:var(--wb-border-default);margin-bottom:14px}
|
||||
:where(.yc-capability-center) ._message_pmhio_15{margin:0;font-size:14px}
|
||||
:where(.yc-capability-center) .ec-card-avatar-sq{width:var(--wb-size-9);height:var(--wb-size-9);border-radius:var(--wb-radius-lg);background:var(--wb-bg-tertiary);flex-shrink:0;justify-content:center;align-items:center;display:flex;position:relative;overflow:hidden}
|
||||
:where(.yc-capability-center) .ec-card-avatar-sq--large{width:var(--wb-size-12);height:var(--wb-size-12);border-radius:var(--wb-radius-2xl);-electron-corner-smoothing:100%}
|
||||
:where(.yc-capability-center) .ec-card-avatar-sq--large img,:where(.yc-capability-center) .ec-card-avatar-sq--large .ec-avatar-sq-fallback{border-radius:inherit;-electron-corner-smoothing:100%}
|
||||
@supports (corner-shape:squircle){:where(.yc-capability-center) .ec-card-avatar-sq--large,:where(.yc-capability-center) .ec-card-avatar-sq--large img,:where(.yc-capability-center) .ec-card-avatar-sq--large .ec-avatar-sq-fallback{corner-shape:squircle}}
|
||||
:where(.yc-capability-center) .ec-card-avatar-sq img{object-fit:cover;width:100%;height:100%;display:block;position:absolute;inset:0}
|
||||
:where(.yc-capability-center) .ec-card-avatar-sq img.is-pending{visibility:hidden}
|
||||
:where(.yc-capability-center) .ec-avatar-sq-fallback{width:100%;height:100%;color:var(--wb-button-primary-fg);border-radius:inherit;justify-content:center;align-items:center;font-size:14px;font-weight:600;display:flex}
|
||||
:where(.yc-capability-center) ._card_1hdcm_2{background:var(--wb-bg-card);border:1px solid var(--wb-border-card-static);cursor:pointer;min-height:154px;box-shadow:var(--wb-shadow-card-soft);box-sizing:border-box;opacity:0;animation-delay:var(--delay,0s);border-radius:16px;flex-direction:column;gap:12px;padding:16px 20px;transition:box-shadow .2s,transform .2s,background .2s,border-color .2s;animation:.25s forwards _fade-in-up_1hdcm_1;display:flex;position:relative;overflow:hidden;transform:translateY(4px)}
|
||||
:where(.yc-capability-center) ._card_1hdcm_2:focus-visible{box-shadow:0 0 0 2px var(--wb-border-default), var(--wb-shadow-card-soft)}
|
||||
:where(.yc-capability-center) ._card_1hdcm_2:hover,:where(.yc-capability-center) ._card_1hdcm_2.ec-cr-card--menu-open{background:var(--wb-bg-card-hover);border-color:var(--wb-border-default);box-shadow:var(--wb-shadow-card-soft);transform:translateY(0)}
|
||||
:where(.yc-capability-center) ._card_1hdcm_2._skeleton_1hdcm_33{cursor:default;pointer-events:none;opacity:1;animation:none;transform:none;box-shadow:0 16px 32px -8px #00000008}
|
||||
:where(.yc-capability-center) ._card_1hdcm_2._selectable_1hdcm_41{cursor:pointer;transition:border-color .15s,box-shadow .15s,background .15s}
|
||||
:where(.yc-capability-center) ._card_1hdcm_2._selectable_1hdcm_41:hover:not(._selected_1hdcm_45){border-color:var(--wb-color-border-primary)}
|
||||
:where(.yc-capability-center) ._card_1hdcm_2._selected_1hdcm_45,:where(.yc-capability-center) ._card_1hdcm_2._selected_1hdcm_45:hover{border-color:var(--wb-button-primary-bg);box-shadow:0 0 0 1px var(--wb-button-primary-bg);background:var(--wb-bg-tertiary);transform:none}
|
||||
:where(.yc-capability-center) ._card_1hdcm_2._disabled_1hdcm_55{opacity:.5;cursor:not-allowed;pointer-events:none}
|
||||
:where(.yc-capability-center) ._centerCard_1hdcm_60{--wb-expert-center-card-height:156px;--wb-expert-center-card-shadow:0 4px 12px color-mix(in srgb, var(--wb-palette-black-100) 3%, transparent);height:var(--wb-expert-center-card-height);min-height:var(--wb-expert-center-card-height);align-self:stretch;align-items:flex-start;gap:var(--wb-spacing-4);padding:var(--wb-spacing-6) var(--wb-spacing-6) var(--wb-spacing-5);background:var(--wb-color-bg-secondary-default);border:calc(var(--wb-spacing-1) / 4) solid var(--wb-color-border-primary);box-shadow:var(--wb-expert-center-card-shadow);flex:1 0 0}
|
||||
:where(.yc-capability-center) ._centerCard_1hdcm_60:hover{background:var(--wb-color-bg-secondary-hover-active);border-color:var(--wb-color-border-primary);box-shadow:var(--wb-expert-center-card-shadow)}
|
||||
:where(.yc-capability-center) ._centerCard_1hdcm_60._skeleton_1hdcm_33{box-shadow:var(--wb-expert-center-card-shadow)}
|
||||
:where(.yc-capability-center) ._centerCard_1hdcm_60:focus-visible{border-color:var(--wb-color-border-primary);box-shadow:0 0 0 2px var(--wb-border-default), var(--wb-expert-center-card-shadow)}
|
||||
:where(.yc-capability-center) ._centerCard_1hdcm_60 ._main_1hdcm_91{gap:var(--wb-spacing-4);flex:1;width:100%;min-height:0}
|
||||
:where(.yc-capability-center) ._centerCard_1hdcm_60 ._head_1hdcm_97{align-items:flex-start;gap:var(--wb-spacing-4)}
|
||||
:where(.yc-capability-center) ._centerCard_1hdcm_60 ._body_1hdcm_101{gap:var(--wb-spacing-2)}
|
||||
:where(.yc-capability-center) ._centerCard_1hdcm_60 ._titleRow_1hdcm_104{gap:calc(var(--wb-spacing-2) + var(--wb-spacing-1));box-sizing:border-box;width:100%}
|
||||
:where(.yc-capability-center) ._centerCard_1hdcm_60 ._role_1hdcm_109{color:var(--wb-color-text-primary);font-family:var(--wb-font-family,"PingFang SC");font-size:var(--wb-font-spec-body-lg-size);line-height:var(--wb-font-spec-body-lg-line-height);font-weight:var(--wb-font-weight-500);font-style:normal;text-decoration-line:none}
|
||||
:where(.yc-capability-center) ._centerCard_1hdcm_60 ._tags_1hdcm_118{gap:var(--wb-spacing-2)}
|
||||
:where(.yc-capability-center) ._centerCard_1hdcm_60 ._tag_1hdcm_73{height:var(--wb-spacing-7);padding:0 var(--wb-spacing-3);border-radius:var(--wb-radius-sm);background:var(--wb-palette-gray-2);color:var(--wb-color-text-tertiary);font-family:var(--wb-font-family,"PingFang SC");font-size:var(--wb-font-body-size);font-weight:var(--wb-font-weight-400);line-height:var(--wb-font-line-height-5)}
|
||||
:where(.yc-capability-center) ._centerCard_1hdcm_60 ._tagPaid_1hdcm_74{background:var(--ec-card-tag-paid-bg,#fceed0);color:var(--ec-card-tag-paid-color,#000000b3)}
|
||||
:where(.yc-capability-center) ._centerCard_1hdcm_60:hover ._tag_1hdcm_73,:where(.yc-capability-center) ._centerCard_1hdcm_60:focus-within ._tag_1hdcm_73{background:var(--wb-palette-gray-4)}
|
||||
:where(.yc-capability-center) ._centerCard_1hdcm_60:hover ._tagPaid_1hdcm_74,:where(.yc-capability-center) ._centerCard_1hdcm_60:focus-within ._tagPaid_1hdcm_74{background:var(--ec-card-tag-paid-bg,#fceed0)}
|
||||
:where(.yc-capability-center) ._centerCard_1hdcm_60 ._topActions_1hdcm_144{top:var(--wb-spacing-6);right:var(--wb-spacing-6)}
|
||||
:where(.yc-capability-center) ._centerCard_1hdcm_60 ._metadata_1hdcm_148{width:100%}
|
||||
:where(.yc-capability-center) ._centerCardWithoutMetadata_1hdcm_151{height:auto;min-height:auto;padding-bottom:var(--wb-spacing-6)}
|
||||
@keyframes _fade-in-up_1hdcm_1{to{opacity:1;transform:translateY(0)}}
|
||||
:where(.yc-capability-center) ._main_1hdcm_91{cursor:pointer;border-radius:8px;flex-direction:column;gap:12px;display:flex}
|
||||
:where(.yc-capability-center) ._main_1hdcm_91:focus-visible{box-shadow:0 0 0 2px var(--wb-border-default)}
|
||||
:where(.yc-capability-center) ._head_1hdcm_97{align-items:center;gap:12px;width:100%;display:flex}
|
||||
:where(.yc-capability-center) ._body_1hdcm_101{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}
|
||||
:where(.yc-capability-center) ._titleRow_1hdcm_104{justify-content:flex-start;align-items:center;gap:4px;min-width:0;display:flex}
|
||||
:where(.yc-capability-center) ._tags_1hdcm_118{flex-wrap:nowrap;align-items:center;gap:4px;min-width:0;display:flex;overflow:hidden}
|
||||
:where(.yc-capability-center) ._tag_1hdcm_73{background:var(--wb-palette-gray-2);height:20px;color:var(--wb-color-text-secondary);white-space:nowrap;border:1px solid #0000;border-radius:4px;flex-shrink:0;align-items:center;padding:0 8px;font-size:11px;font-weight:400;line-height:20px;display:inline-flex}
|
||||
:where(.yc-capability-center) ._tagPaid_1hdcm_74{background:var(--ec-card-tag-paid-bg,#fceed0);color:var(--ec-card-tag-paid-color,#000000b3)}
|
||||
:where(.yc-capability-center) .ec-purchased-card--expired ._role_1hdcm_109,:where(.yc-capability-center) .ec-purchased-card--expired ._subtitle_1hdcm_225,:where(.yc-capability-center) .ec-purchased-card--expired ._desc_1hdcm_226,:where(.yc-capability-center) .ec-purchased-card--expired ._tag_1hdcm_73:not(:first-child){opacity:.5}
|
||||
:where(.yc-capability-center) ._subtitle_1hdcm_225{color:var(--wb-color-text-primary);text-overflow:ellipsis;white-space:nowrap;font-size:11px;font-weight:400;line-height:16px;overflow:hidden}
|
||||
:where(.yc-capability-center) ._roleWrap_1hdcm_239{align-items:center;min-width:0;max-width:100%;display:inline-flex}
|
||||
:where(.yc-capability-center) ._role_1hdcm_109{min-width:0;color:var(--wb-color-text-primary);letter-spacing:0;text-overflow:ellipsis;white-space:nowrap;flex:0 auto;margin-bottom:0;font-size:14px;font-weight:500;line-height:20px;overflow:hidden}
|
||||
:where(.yc-capability-center) ._importSecurityBadge_1hdcm_258{border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;width:16px;height:16px;margin-left:2px;display:inline-flex}
|
||||
:where(.yc-capability-center) ._importSecurityBadge_1hdcm_258._safe_1hdcm_268{color:var(--wb-status-success,#34a853);background:color-mix(in srgb, var(--wb-status-success) 18%, transparent)}
|
||||
:where(.yc-capability-center) ._importSecurityBadge_1hdcm_258._low_1hdcm_272{color:var(--wb-status-info,#4f86ff);background:color-mix(in srgb, var(--wb-status-info) 18%, transparent)}
|
||||
:where(.yc-capability-center) ._importSecurityBadge_1hdcm_258._medium_1hdcm_276{color:var(--wb-status-warning,#f5a623);background:color-mix(in srgb, var(--wb-status-warning) 20%, transparent)}
|
||||
:where(.yc-capability-center) ._importSecurityIcon_1hdcm_280{width:14px;height:14px}
|
||||
:where(.yc-capability-center) ._operationalTag_1hdcm_284{background:var(--ec-operational-tag-bg,#f7d18f);max-width:110px;height:18px;color:var(--ec-operational-tag-color,#3d3d3d);font-size:10px;font-weight:var(--wb-font-weight-500);letter-spacing:0;white-space:nowrap;box-sizing:border-box;pointer-events:none;border:none;border-radius:4px;flex-shrink:0;justify-content:center;align-items:center;gap:4px;padding:0 4px 0 0;line-height:18px;display:inline-flex;overflow:hidden}
|
||||
:where(.yc-capability-center) ._operationalTagIconBackground_1hdcm_306{background:#3d3d3d;flex:0 0 18px;justify-content:center;align-items:center;width:18px;height:18px;display:inline-flex}
|
||||
:where(.yc-capability-center) ._operationalTagIcon_1hdcm_306{object-fit:contain;flex-shrink:0;width:11.388px;height:12.25px;display:block}
|
||||
:where(.yc-capability-center) ._operationalTagText_1hdcm_322{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}
|
||||
:where(.yc-capability-center) ._desc_1hdcm_226{width:100%;color:var(--wb-color-text-secondary);line-clamp:2;-webkit-line-clamp:2;-webkit-box-orient:vertical;min-height:36px;font-size:11px;font-weight:400;line-height:18px;display:-webkit-box;overflow:hidden}
|
||||
:where(.yc-capability-center) ._centerDescription_1hdcm_341{color:var(--wb-color-text-secondary);font-size:var(--wb-font-body-size);font-weight:var(--wb-font-body-weight);line-height:var(--wb-font-body-line-height);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}
|
||||
:where(.yc-capability-center) ._metadata_1hdcm_148{min-width:0;min-height:var(--wb-font-body-line-height);justify-content:space-between;align-items:center;gap:var(--wb-spacing-3);color:var(--wb-color-text-tertiary);font-size:var(--wb-font-body-size);font-weight:var(--wb-font-body-weight);line-height:var(--wb-font-body-line-height);display:flex}
|
||||
:where(.yc-capability-center) ._usage_1hdcm_362{align-items:center;gap:var(--wb-spacing-2);flex-shrink:0;display:inline-flex}
|
||||
:where(.yc-capability-center) ._usageIcon_1hdcm_368{width:var(--wb-font-body-size);height:var(--wb-font-body-size);background:currentColor;flex-shrink:0;mask:url("data:image/svg+xml,%3c?xml%20version='1.0'%20encoding='utf-8'%20?%3e%3csvg%20xmlns='http://www.w3.org/2000/svg'%20xmlns:xlink='http://www.w3.org/1999/xlink'%20width='16'%20height='16'%3e%3cclipPath%20id='clip_0'%3e%3crect%20width='16'%20height='16'/%3e%3c/clipPath%3e%3cg%20clip-path='url(%23clip_0)'%3e%3cpath%20fill='%23000'%20transform='matrix(1%200%200%201%201%20-0.749512)'%20d='M8.5821%205.6358Q8.5782%205.6639%208.5705%205.7201Q8.503%206.2088%208.5048%206.3468Q8.505%206.3625%208.5056%206.3752Q8.5182%206.3775%208.5337%206.3799Q8.6701%206.4004%209.1635%206.4Q9.2202%206.4%209.2486%206.4L11.2827%206.4Q11.4084%206.4%2011.6448%206.3982Q12.4721%206.3919%2012.7885%206.4466Q13.5075%206.5709%2013.761%207.112Q14.0144%207.6531%2013.6495%208.285Q13.489%208.563%2012.9545%209.1946Q12.8018%209.3751%2012.7213%209.4716L8.5533%2014.4732Q8.4211%2014.6319%208.1737%2014.934Q7.4209%2015.8535%207.0953%2016.1442Q6.3479%2016.8116%205.6832%2016.5183Q5.0185%2016.225%205.0077%2015.223Q5.0031%2014.7866%205.1748%2013.6107Q5.2312%2013.2243%205.2593%2013.0197L5.4179%2011.8642Q5.4219%2011.8349%205.4295%2011.7799Q5.497%2011.2912%205.4952%2011.1532Q5.495%2011.1375%205.4944%2011.1248Q5.4819%2011.1225%205.4663%2011.1201Q5.3299%2011.0996%204.8365%2011.1Q4.7798%2011.1%204.7514%2011.1L2.7173%2011.1Q2.5916%2011.1%202.3552%2011.1018Q1.5279%2011.1081%201.2115%2011.0534Q0.4925%2010.9291%200.239%2010.388Q-0.0144%209.847%200.3505%209.215Q0.511%208.937%201.0455%208.3054Q1.1982%208.1249%201.2787%208.0284L5.4467%203.0268Q5.5789%202.8681%205.8263%202.566Q6.5791%201.6465%206.9047%201.3558Q7.6521%200.6884%208.3168%200.9817Q8.9815%201.275%208.9923%202.277Q8.9969%202.7134%208.8252%203.8893Q8.7688%204.2756%208.7407%204.4803L8.5821%205.6358ZM7.3932%205.4726L7.5518%204.3171Q7.5806%204.1074%207.6378%203.7159Q7.796%202.6327%207.7923%202.2899Q7.7917%202.2289%207.7882%202.1795Q7.7495%202.2103%207.704%202.2509Q7.4483%202.4792%206.7547%203.3262Q6.5041%203.6324%206.3685%203.795L2.2005%208.7966Q2.1171%208.8967%201.9615%209.0806Q1.4965%209.6301%201.3897%209.8151Q1.3752%209.8401%201.3645%209.8609Q1.3873%209.866%201.4158%209.8709Q1.6263%209.9073%202.3461%209.9018Q2.5872%209.9%202.7173%209.9L4.7514%209.9Q4.7789%209.9%204.8357%209.9Q5.4192%209.8995%205.6449%209.9335Q6.1372%2010.0076%206.4129%2010.3237Q6.6886%2010.6399%206.6951%2011.1377Q6.698%2011.3659%206.6183%2011.9439Q6.6106%2011.9996%206.6068%2012.0274L6.4482%2013.1829Q6.4194%2013.3926%206.3622%2013.7841Q6.204%2014.8673%206.2077%2015.2101Q6.2083%2015.2711%206.2118%2015.3205Q6.2505%2015.2897%206.296%2015.2491Q6.5517%2015.0208%207.2453%2014.1738Q7.496%2013.8676%207.6315%2013.705L11.7995%208.7034Q11.8829%208.6033%2012.0385%208.4194Q12.5035%207.8699%2012.6103%207.6849Q12.6248%207.6599%2012.6355%207.6391Q12.6127%207.634%2012.5842%207.6291Q12.3737%207.5927%2011.6539%207.5982Q11.4129%207.6%2011.2827%207.6L9.2486%207.6Q9.2211%207.6%209.1643%207.6Q8.5808%207.6005%208.3551%207.5665Q7.8628%207.4924%207.5871%207.1763Q7.3114%206.8601%207.3049%206.3623Q7.302%206.1342%207.3817%205.5561Q7.3894%205.5004%207.3932%205.4726Z'%20fill-rule='evenodd'/%3e%3c/g%3e%3c/svg%3e") 50%/contain no-repeat}
|
||||
:where(.yc-capability-center) ._footer_1hdcm_376{justify-content:flex-end;align-items:center;min-height:20px;display:flex}
|
||||
:where(.yc-capability-center) ._titleActions_1hdcm_384{align-items:center;gap:var(--wb-spacing-2);flex-shrink:0;margin-left:auto;display:flex}
|
||||
:where(.yc-capability-center) ._titleActions_1hdcm_384 .ec-cr-more-action{display:none}
|
||||
:where(.yc-capability-center) ._card_1hdcm_2:hover ._titleActions_1hdcm_384 .ec-cr-more-action,:where(.yc-capability-center) ._card_1hdcm_2:focus-within ._titleActions_1hdcm_384 .ec-cr-more-action,:where(.yc-capability-center) ._card_1hdcm_2.ec-cr-card--menu-open ._titleActions_1hdcm_384 .ec-cr-more-action{display:block}
|
||||
:where(.yc-capability-center) ._topActions_1hdcm_144{top:var(--wb-spacing-5);right:var(--wb-spacing-6);z-index:2;align-items:center;gap:var(--wb-spacing-2);display:flex;position:absolute}
|
||||
:where(.yc-capability-center) ._summonBtn_1hdcm_408{background:var(--wb-button-primary-bg);height:24px;color:var(--wb-button-primary-fg);white-space:nowrap;box-shadow:none;cursor:pointer;border:none;border-radius:8px;outline:none;flex-shrink:0;justify-content:center;align-items:center;padding:0 10px;font-size:12px;font-weight:500;line-height:24px;transition:background .15s,color .15s;display:inline-flex}
|
||||
:where(.yc-capability-center) ._summonBtn_1hdcm_408:hover,:where(.yc-capability-center) ._summonBtn_1hdcm_408:focus,:where(.yc-capability-center) ._summonBtn_1hdcm_408:focus-visible,:where(.yc-capability-center) ._summonBtn_1hdcm_408:active{background:var(--wb-button-primary-bg-hover);color:var(--wb-button-primary-fg);box-shadow:none;border-color:#0000;outline:none;transform:none}
|
||||
:where(.yc-capability-center) ._summonBtnTop_1hdcm_441{margin-left:auto;display:none}
|
||||
:where(.yc-capability-center) ._card_1hdcm_2:hover ._summonBtnTop_1hdcm_441,:where(.yc-capability-center) ._card_1hdcm_2:focus-within ._summonBtnTop_1hdcm_441,:where(.yc-capability-center) ._card_1hdcm_2.ec-cr-card--menu-open ._summonBtnTop_1hdcm_441{display:inline-flex}
|
||||
:where(.yc-capability-center) ._selectedBadge_1hdcm_450{background:var(--wb-button-primary-bg);width:22px;height:22px;color:var(--wb-button-primary-fg);border-radius:50%;justify-content:center;align-items:center;display:flex;position:absolute;top:8px;right:8px}
|
||||
:where(.yc-capability-center) .expert-center-dark ._card_1hdcm_2{border-color:#0000}
|
||||
:where(.yc-capability-center) .expert-center-dark ._card_1hdcm_2:hover,:where(.yc-capability-center) .expert-center-dark ._card_1hdcm_2:focus-within,:where(.yc-capability-center) .expert-center-dark ._card_1hdcm_2.ec-cr-card--menu-open{background:var(--wb-bg-card-hover);border-color:#0000;border-color:var(--wb-border-subtle)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._centerCard_1hdcm_60{background:var(--wb-bg-card);border-color:var(--wb-color-border-primary)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._centerCard_1hdcm_60 ._tag_1hdcm_73,:where(.yc-capability-center) .expert-center-dark ._centerCard_1hdcm_60:hover ._tag_1hdcm_73,:where(.yc-capability-center) .expert-center-dark ._centerCard_1hdcm_60:focus-within ._tag_1hdcm_73{background:var(--wb-bg-hover)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._centerCard_1hdcm_60:hover,:where(.yc-capability-center) .expert-center-dark ._centerCard_1hdcm_60:focus-within{border-color:var(--wb-color-border-primary)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._role_1hdcm_109,:where(.yc-capability-center) .expert-center-dark ._subtitle_1hdcm_225{color:var(--ec-text-strong)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._desc_1hdcm_226{color:var(--ec-text-desc)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._tag_1hdcm_73{background:var(--wb-bg-hover)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._centerCard_1hdcm_60 .ec-card-avatar-sq,:where(.yc-capability-center) .expert-center-dark ._centerCard_1hdcm_60 ._tag_1hdcm_73,:where(.yc-capability-center) .expert-center-dark ._centerCard_1hdcm_60 ._operationalTag_1hdcm_284{opacity:.85}
|
||||
:where(.yc-capability-center) .expert-center-light.theme-switching ._card_1hdcm_2,:where(.yc-capability-center) body.theme-switching ._card_1hdcm_2{transition:none}
|
||||
:where(.yc-capability-center) .ec-cr-card ._titleRow_1hdcm_104{box-sizing:border-box;width:100%}
|
||||
:where(.yc-capability-center) .ec-cr-card ._summonBtnTop_1hdcm_441{width:calc(var(--wb-spacing-12) + var(--wb-spacing-2));box-sizing:border-box}
|
||||
:where(.yc-capability-center) .ec-cr-card ._summonBtnTop_1hdcm_441:before{width:var(--wb-spacing-3)}
|
||||
:where(.yc-capability-center) ._block_is4an_1{background:linear-gradient(90deg, color-mix(in srgb, var(--wb-border-default) 65%, transparent) 25%, color-mix(in srgb, var(--wb-bg-secondary) 85%, white) 50%, color-mix(in srgb, var(--wb-border-default) 65%, transparent) 75%);background-size:200% 100%;border-radius:6px;animation:1.5s ease-in-out infinite _skeleton-shimmer_is4an_1}
|
||||
@keyframes _skeleton-shimmer_is4an_1{0%{background-position:-200% 0}to{background-position:200% 0}}
|
||||
:where(.yc-capability-center) ._avatar_1iatb_1{width:var(--wb-size-9);height:var(--wb-size-9);border-radius:var(--wb-radius-lg);flex-shrink:0}
|
||||
:where(.yc-capability-center) ._avatarLarge_1iatb_7{width:var(--wb-size-12);height:var(--wb-size-12);border-radius:var(--wb-radius-2xl);-electron-corner-smoothing:100%;flex-shrink:0}
|
||||
@supports (corner-shape:squircle){:where(.yc-capability-center) ._avatarLarge_1iatb_7{corner-shape:squircle}}
|
||||
:where(.yc-capability-center) ._role_1iatb_19{width:72%;max-width:160px;height:14px;margin-bottom:4px}
|
||||
:where(.yc-capability-center) ._descShort_1iatb_25{width:58%;height:12px}
|
||||
:where(.yc-capability-center) ._roleLarge_1iatb_29{width:72%;max-width:160px;height:var(--wb-font-spec-body-lg-line-height)}
|
||||
:where(.yc-capability-center) ._descLarge_1iatb_34{width:86%;height:var(--wb-font-body-line-height)}
|
||||
:where(.yc-capability-center) ._desc_1iatb_25{flex-direction:column;gap:8px;padding:0 20px;display:flex}
|
||||
:where(.yc-capability-center) ._descLine_1iatb_44{width:100%;height:14px}
|
||||
:where(.yc-capability-center) ._descLineShort_1iatb_48{width:60%}
|
||||
:where(.yc-capability-center) ._usage_1iatb_51{border-radius:6px;width:84px;height:12px}
|
||||
:where(.yc-capability-center) ._tags_1iatb_56,:where(.yc-capability-center) ._metadata_1iatb_57{align-items:center;display:flex}
|
||||
:where(.yc-capability-center) ._tags_1iatb_56{gap:var(--wb-spacing-2)}
|
||||
:where(.yc-capability-center) ._metadata_1iatb_57{width:100%;min-height:var(--wb-font-body-line-height);justify-content:space-between;gap:var(--wb-spacing-3)}
|
||||
:where(.yc-capability-center) ._tag_1iatb_56{width:var(--wb-spacing-10);height:var(--wb-spacing-7);border-radius:var(--wb-radius-sm)}
|
||||
:where(.yc-capability-center) ._tagShort_1iatb_75{width:var(--wb-spacing-8)}
|
||||
:where(.yc-capability-center) ._author_1iatb_78{width:var(--wb-spacing-12);height:var(--wb-font-body-size);border-radius:var(--wb-radius-md)}
|
||||
:where(.yc-capability-center) ._content_1tpa4_1{min-width:0}
|
||||
:where(.yc-capability-center) ._animated_1tpa4_4{will-change:opacity, transform;animation:.22s _grid-content-in_1tpa4_1}
|
||||
@keyframes _grid-content-in_1tpa4_1{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
|
||||
:where(.yc-capability-center) ._grid_1tpa4_18{gap:var(--wb-spacing-5);scrollbar-width:thin;scrollbar-color:var(--wb-border-default) transparent;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));align-content:start;display:grid}
|
||||
@media (width<=1279px){:where(.yc-capability-center) ._grid_1tpa4_18{grid-template-columns:repeat(2,1fr)}}
|
||||
@media (width<=767px){:where(.yc-capability-center) ._grid_1tpa4_18{gap:var(--wb-spacing-5);grid-template-columns:repeat(2,1fr)}}
|
||||
@media (width<=575px){:where(.yc-capability-center) ._grid_1tpa4_18{grid-template-columns:1fr}}
|
||||
:where(.yc-capability-center) ._grid_1tpa4_18::-webkit-scrollbar{width:4px;height:4px}
|
||||
:where(.yc-capability-center) ._grid_1tpa4_18::-webkit-scrollbar-track{background:0 0;border-radius:2px}
|
||||
:where(.yc-capability-center) ._grid_1tpa4_18::-webkit-scrollbar-thumb{background:var(--wb-border-default);border-radius:2px;transition:background .2s}
|
||||
:where(.yc-capability-center) ._grid_1tpa4_18::-webkit-scrollbar-thumb:hover{background:var(--wb-bg-tertiary)}
|
||||
:where(.yc-capability-center) ._grid_1tpa4_18::-webkit-scrollbar-corner{background:0 0}
|
||||
:where(.yc-capability-center) ._grid_1tpa4_18._skeleton_1tpa4_61{pointer-events:none}
|
||||
:where(.yc-capability-center) ._expandWrap_1tpa4_64{justify-content:center;margin-top:24px;display:flex}
|
||||
:where(.yc-capability-center) ._expandBtn_1tpa4_69{height:auto;color:var(--wb-color-text-tertiary);cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;gap:4px;padding:0;font-size:12px;font-weight:500;line-height:1;transition:color .15s,opacity .15s;display:inline-flex}
|
||||
:where(.yc-capability-center) ._expandBtn_1tpa4_69:hover{color:var(--wb-color-text-primary)}
|
||||
:where(.yc-capability-center) ._expandBtn_1tpa4_69:focus-visible{color:var(--wb-color-text-primary);outline:none}
|
||||
:where(.yc-capability-center) ._expandIcon_1tpa4_92{color:currentColor;justify-content:center;align-items:center;transition:transform .15s;display:inline-flex}
|
||||
:where(.yc-capability-center) ._expandIcon_1tpa4_92._expanded_1tpa4_99{transform:rotate(180deg)}
|
||||
:where(.yc-capability-center) ._searchHint_1tpa4_102{color:var(--wb-color-text-tertiary);padding:0 0 12px;font-size:13px}
|
||||
:where(.yc-capability-center) ._keyword_1tpa4_107{color:var(--wb-brand-accent);font-weight:500}
|
||||
:where(.yc-capability-center) ._count_1tpa4_111{color:var(--wb-color-text-primary);font-weight:600}
|
||||
:where(.yc-capability-center) ._root_mbn4e_1{flex:0 180px;width:180px;min-width:120px;position:relative}
|
||||
:where(.yc-capability-center) ._root_mbn4e_1 .wb-input-wrapper{background:var(--wb-bg-primary);border-color:var(--wb-border-default)}
|
||||
:where(.yc-capability-center) ._root_mbn4e_1 .wb-input-wrapper:hover:not(.wb-input-wrapper--disabled){border-color:var(--wb-border-hover)}
|
||||
:where(.yc-capability-center) ._root_mbn4e_1 .wb-input-wrapper:focus-within:not(.wb-input-wrapper--disabled){border-color:var(--wb-border-popover-strong)}
|
||||
:where(.yc-capability-center) ._root_mbn4e_1 .wb-input__inner::placeholder{color:color-mix(in srgb, var(--wb-color-text-primary) 30%, transparent)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._root_mbn4e_1 .wb-input-wrapper,:where(.yc-capability-center) [data-theme=dark] ._root_mbn4e_1 .wb-input-wrapper,:where(.yc-capability-center) .vscode-dark ._root_mbn4e_1 .wb-input-wrapper,:where(.yc-capability-center) .dark ._root_mbn4e_1 .wb-input-wrapper{background:var(--wb-bg-primary);border-color:var(--wb-border-default)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._root_mbn4e_1 .wb-input-wrapper:hover:not(.wb-input-wrapper--disabled),:where(.yc-capability-center) [data-theme=dark] ._root_mbn4e_1 .wb-input-wrapper:hover:not(.wb-input-wrapper--disabled),:where(.yc-capability-center) .vscode-dark ._root_mbn4e_1 .wb-input-wrapper:hover:not(.wb-input-wrapper--disabled),:where(.yc-capability-center) .dark ._root_mbn4e_1 .wb-input-wrapper:hover:not(.wb-input-wrapper--disabled){border-color:var(--wb-border-hover)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._root_mbn4e_1 .wb-input-wrapper:focus-within:not(.wb-input-wrapper--disabled),:where(.yc-capability-center) [data-theme=dark] ._root_mbn4e_1 .wb-input-wrapper:focus-within:not(.wb-input-wrapper--disabled),:where(.yc-capability-center) .vscode-dark ._root_mbn4e_1 .wb-input-wrapper:focus-within:not(.wb-input-wrapper--disabled),:where(.yc-capability-center) .dark ._root_mbn4e_1 .wb-input-wrapper:focus-within:not(.wb-input-wrapper--disabled){border-color:var(--wb-border-popover-strong)}
|
||||
:where(.yc-capability-center) .ec-picker-header-right ._root_mbn4e_1{width:220px}
|
||||
@media (width<=540px){:where(.yc-capability-center) ._root_mbn4e_1{flex-shrink:1;width:100%;min-width:0}}
|
||||
@media (width<=767px){:where(.yc-capability-center) ._root_mbn4e_1{width:180px;min-width:120px}}
|
||||
:where(.yc-capability-center) ._section_1fi36_1{--wb-expert-featured-card-width:264px;--wb-expert-featured-card-height:202px;--wb-expert-featured-card-half-height:101px;--wb-expert-featured-header-height:80px;--ec-featured-scene-bg:var(--wb-bg-card);--ec-featured-scene-shadow:0 4px 12px color-mix(in srgb, var(--wb-palette-black-100) 3%, transparent);--ec-featured-scene-fade-color:var(--wb-home-bg-secondary);--ec-featured-scene-row-hover-bg:#0000000d;--ec-featured-scene-tag-bg:#0000000d;--ec-featured-scene-tag-color:#393939}
|
||||
:where(.yc-capability-center) .expert-center-dark ._section_1fi36_1{--ec-featured-scene-bg:var(--wb-bg-card-strong);--ec-featured-scene-shadow:0 16px 32px -8px #0000002e;--ec-featured-scene-row-hover-bg:var(--wb-bg-active);--ec-featured-scene-tag-bg:#ffffff14;--ec-featured-scene-tag-color:var(--ec-text-strong)}
|
||||
:where(.yc-capability-center) ._section_1fi36_1{margin-bottom:var(--wb-spacing-7);flex-direction:column;display:flex;position:relative;overflow:hidden}
|
||||
:where(.yc-capability-center) ._section_1fi36_1:before,:where(.yc-capability-center) ._section_1fi36_1:after{content:"";z-index:3;width:32px;height:calc(var(--wb-expert-featured-card-height) + var(--wb-spacing-1));pointer-events:none;opacity:0;transition:opacity .2s;position:absolute;top:0}
|
||||
:where(.yc-capability-center) ._section_1fi36_1:before{background:linear-gradient(to left, transparent 0%, var(--ec-featured-scene-fade-color) 100%);left:0}
|
||||
:where(.yc-capability-center) ._section_1fi36_1:after{background:linear-gradient(to right, transparent 0%, var(--ec-featured-scene-fade-color) 100%);right:0}
|
||||
:where(.yc-capability-center) ._section_1fi36_1._maskedStart_1fi36_47:before,:where(.yc-capability-center) ._section_1fi36_1._maskedEnd_1fi36_48:after{opacity:1}
|
||||
:where(.yc-capability-center) ._prev_1fi36_51,:where(.yc-capability-center) ._next_1fi36_52{top:var(--wb-expert-featured-card-half-height);z-index:4;border:1px solid var(--wb-palette-gray-4);background:var(--wb-bg-primary);width:20px;height:20px;color:var(--wb-color-text-secondary);box-shadow:var(--wb-shadow-popup-medium);cursor:pointer;border-radius:999px;justify-content:center;align-items:center;padding:0;transition:color .12s,border-color .12s;display:inline-flex;position:absolute;transform:translateY(-50%)}
|
||||
:where(.yc-capability-center) ._prev_1fi36_51:hover,:where(.yc-capability-center) ._next_1fi36_52:hover{color:var(--wb-color-text-primary);border-color:var(--wb-border-default)}
|
||||
:where(.yc-capability-center) ._prev_1fi36_51{left:0}
|
||||
:where(.yc-capability-center) ._next_1fi36_52{right:0}
|
||||
:where(.yc-capability-center) ._prevIcon_1fi36_82,:where(.yc-capability-center) ._nextIcon_1fi36_83{border-top:1.5px solid;border-right:1.5px solid;width:6px;height:6px}
|
||||
:where(.yc-capability-center) ._prevIcon_1fi36_82{transform:translate(1px)rotate(-135deg)}
|
||||
:where(.yc-capability-center) ._nextIcon_1fi36_83{transform:translate(-1px)rotate(45deg)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._prev_1fi36_51,:where(.yc-capability-center) .expert-center-dark ._next_1fi36_52{background:var(--wb-bg-primary);border-color:var(--wb-palette-gray-4);color:var(--wb-palette-white-70)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._prev_1fi36_51:hover,:where(.yc-capability-center) .expert-center-dark ._next_1fi36_52:hover{color:var(--wb-color-text-primary);border-color:var(--wb-border-default)}
|
||||
:where(.yc-capability-center) ._strip_1fi36_106{gap:var(--wb-spacing-5);cursor:grab;user-select:none;scrollbar-width:none;-ms-overflow-style:none;flex-direction:row;padding-bottom:2px;display:flex;overflow:auto hidden}
|
||||
:where(.yc-capability-center) ._strip_1fi36_106::-webkit-scrollbar{display:none}
|
||||
:where(.yc-capability-center) ._dragging_1fi36_121,:where(.yc-capability-center) ._dragging_1fi36_121 *{cursor:grabbing}
|
||||
:where(.yc-capability-center) ._section_14m5u_1{--wb-expert-featured-card-width:264px;--wb-expert-featured-card-height:202px;--wb-expert-featured-card-half-height:101px;--wb-expert-featured-header-height:80px;--ec-featured-scene-bg:var(--wb-bg-card);--ec-featured-scene-shadow:0 4px 12px color-mix(in srgb, var(--wb-palette-black-100) 3%, transparent);--ec-featured-scene-fade-color:var(--wb-home-bg-secondary);--ec-featured-scene-row-hover-bg:#0000000d;--ec-featured-scene-tag-bg:#0000000d;--ec-featured-scene-tag-color:#393939}
|
||||
:where(.yc-capability-center) .expert-center-dark ._section_14m5u_1{--ec-featured-scene-bg:var(--wb-bg-card-strong);--ec-featured-scene-shadow:0 16px 32px -8px #0000002e;--ec-featured-scene-row-hover-bg:var(--wb-bg-active);--ec-featured-scene-tag-bg:#ffffff14;--ec-featured-scene-tag-color:var(--ec-text-strong)}
|
||||
:where(.yc-capability-center) ._section_14m5u_1{flex-direction:column;display:flex}
|
||||
:where(.yc-capability-center) ._grid_14m5u_24{gap:var(--wb-spacing-5);display:flex;overflow:hidden}
|
||||
:where(.yc-capability-center) ._card_14m5u_29{width:var(--wb-expert-featured-card-width);height:var(--wb-expert-featured-card-height);border-radius:var(--wb-radius-2xl);background:var(--wb-palette-brand-1);box-shadow:var(--ec-featured-scene-shadow);box-sizing:border-box;border:none;flex-direction:column;flex-shrink:0;display:flex;position:relative;overflow:hidden}
|
||||
:where(.yc-capability-center) ._sceneLabel_14m5u_43{width:68px;height:24px;top:calc(var(--wb-spacing-9) + var(--wb-spacing-2));left:var(--wb-spacing-6);position:absolute}
|
||||
:where(.yc-capability-center) ._rows_14m5u_50{top:var(--wb-expert-featured-header-height);width:100%;height:calc(var(--wb-expert-featured-card-height) - var(--wb-expert-featured-header-height));padding:var(--wb-spacing-5) var(--wb-spacing-4);background:var(--wb-bg-card);box-sizing:border-box;flex-direction:column;gap:0;display:flex;position:absolute;left:0}
|
||||
:where(.yc-capability-center) ._row_14m5u_50{align-items:center;gap:var(--wb-spacing-2);height:calc(var(--wb-font-body-line-height) + var(--wb-spacing-3));padding:var(--wb-spacing-2) var(--wb-spacing-3);display:flex}
|
||||
:where(.yc-capability-center) ._sceneName_14m5u_70{width:98px;height:var(--wb-font-body-line-height)}
|
||||
:where(.yc-capability-center) ._sceneNameShort_14m5u_74{width:84px}
|
||||
:where(.yc-capability-center) ._sceneArrow_14m5u_77{width:var(--wb-font-body-size);height:var(--wb-font-body-size);border-radius:var(--wb-radius-sm)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._card_14m5u_29{background:var(--wb-bg-card-strong);box-shadow:var(--ec-featured-scene-shadow)}
|
||||
:where(.yc-capability-center) ._card_91ass_18{flex:none;width:calc(25% - 9px);min-width:0;display:flex}
|
||||
:where(.yc-capability-center) ._card_91ass_18>*{flex:1;min-width:0}
|
||||
@media (width<=1279px){:where(.yc-capability-center) ._card_91ass_18{width:calc(33.3333% - 8px)}}
|
||||
@media (width<=991px),(width<=767px){:where(.yc-capability-center) ._card_91ass_18{width:calc(50% - 6px)}}
|
||||
@media (width<=575px){:where(.yc-capability-center) ._card_91ass_18{width:100%}}
|
||||
:where(.yc-capability-center) ._section_91ass_13._section_91ass_13:after{height:auto;top:36px;bottom:0}
|
||||
:where(.yc-capability-center) ._section_91ass_13 ._prev_91ass_13,:where(.yc-capability-center) ._section_91ass_13 ._next_91ass_54{margin:auto 0;top:48px;bottom:12px;transform:none}
|
||||
:where(.yc-capability-center) ._title_91ass_70._title_91ass_70{color:var(--wb-color-text-primary);margin-bottom:16px;font-size:16px;font-weight:500;line-height:24px}
|
||||
:where(.yc-capability-center) ._card_1a4sl_1{flex:0 0 var(--wb-expert-featured-card-width);width:var(--wb-expert-featured-card-width);height:var(--wb-expert-featured-card-height);border-radius:var(--wb-radius-2xl);background:var(--ec-featured-scene-bg);border:calc(var(--wb-spacing-1) / 4) solid var(--wb-color-border-primary);box-shadow:var(--ec-featured-scene-shadow);box-sizing:border-box;flex-direction:column;flex-shrink:0;align-items:flex-start;display:flex;position:relative;overflow:hidden}
|
||||
:where(.yc-capability-center) ._cover_1a4sl_17{width:100%;height:var(--wb-expert-featured-header-height);object-fit:cover;background:var(--wb-bg-card);position:absolute;inset:0 0 auto}
|
||||
:where(.yc-capability-center) ._cover_1a4sl_17.is-pending{visibility:hidden}
|
||||
:where(.yc-capability-center) ._content_1a4sl_29{z-index:1;width:100%;height:100%;position:relative}
|
||||
:where(.yc-capability-center) ._name_1a4sl_35{top:calc(var(--wb-spacing-9) + var(--wb-spacing-2));left:var(--wb-spacing-6);max-width:calc(100% - var(--wb-spacing-9));min-height:var(--wb-font-spec-body-lg-line-height);color:var(--ec-text-strong);font-size:var(--wb-font-spec-body-lg-size);font-weight:var(--wb-font-weight-600);line-height:var(--wb-font-spec-body-lg-line-height);text-overflow:ellipsis;white-space:nowrap;position:absolute;overflow:hidden}
|
||||
:where(.yc-capability-center) .expert-center-dark ._name_1a4sl_35{color:var(--ec-text-strong)}
|
||||
:where(.yc-capability-center) ._experts_1a4sl_52{top:var(--wb-expert-featured-header-height);width:100%;height:calc(var(--wb-expert-featured-card-height) - var(--wb-expert-featured-header-height));padding:var(--wb-spacing-5) var(--wb-spacing-4);background:var(--wb-bg-card);box-sizing:border-box;flex-direction:column;gap:0;display:flex;position:absolute;left:0}
|
||||
:where(.yc-capability-center) ._expert_1a4sl_49{align-items:center;gap:var(--wb-spacing-2);width:fit-content;max-width:100%;height:calc(var(--wb-font-body-line-height) + var(--wb-spacing-3));padding:var(--wb-spacing-2) var(--wb-spacing-3);border-radius:var(--wb-radius-lg);color:var(--ec-text-primary);cursor:pointer;text-align:left;box-sizing:border-box;background:0 0;border:none;display:flex}
|
||||
:where(.yc-capability-center) ._expert_1a4sl_49:hover,:where(.yc-capability-center) ._expert_1a4sl_49:focus-visible{background:var(--ec-featured-scene-row-hover-bg)}
|
||||
:where(.yc-capability-center) ._expert_1a4sl_49:focus-visible{outline:1px solid var(--ec-primary);outline-offset:1px}
|
||||
:where(.yc-capability-center) ._placeholder_1a4sl_89,:where(.yc-capability-center) ._pendingExpert_1a4sl_90{pointer-events:none}
|
||||
:where(.yc-capability-center) ._pendingExpert_1a4sl_90 ._avatar_1a4sl_93{background:color-mix(in srgb, var(--ec-text-strong) 12%, transparent);color:var(--ec-text-strong)}
|
||||
:where(.yc-capability-center) ._placeholderAvatar_1a4sl_97,:where(.yc-capability-center) ._placeholderName_1a4sl_98{background:linear-gradient(90deg, color-mix(in srgb, var(--ec-border) 65%, transparent) 25%, color-mix(in srgb, var(--ec-bg-secondary) 85%, white) 50%, color-mix(in srgb, var(--ec-border) 65%, transparent) 75%);background-size:200% 100%;animation:1.5s ease-in-out infinite _placeholder_1a4sl_89}
|
||||
:where(.yc-capability-center) ._placeholderName_1a4sl_98{border-radius:6px;width:96px;height:16px}
|
||||
@keyframes _placeholder_1a4sl_89{0%{background-position:-200% 0}to{background-position:200% 0}}
|
||||
:where(.yc-capability-center) ._loadingExpert_1a4sl_116{pointer-events:none;cursor:default}
|
||||
:where(.yc-capability-center) ._avatar_1a4sl_93{background:var(--ec-surface-muted);width:24px;height:24px;color:var(--ec-text-contrast);border-radius:999px;flex-shrink:0;justify-content:center;align-items:center;font-size:11px;font-weight:600;display:inline-flex;position:relative;overflow:hidden}
|
||||
:where(.yc-capability-center) ._avatar_1a4sl_93 img{object-fit:cover;width:100%;height:100%;position:absolute;inset:0}
|
||||
:where(.yc-capability-center) ._loadingName_1a4sl_142{background:linear-gradient(90deg, color-mix(in srgb, var(--ec-border) 65%, transparent) 25%, color-mix(in srgb, var(--ec-bg-secondary) 85%, white) 50%, color-mix(in srgb, var(--ec-border) 65%, transparent) 75%);background-size:200% 100%;border-radius:6px;width:96px;height:16px;animation:1.5s ease-in-out infinite _loading_1a4sl_116}
|
||||
:where(.yc-capability-center) ._expertName_1a4sl_152{min-width:0;color:var(--wb-color-text-secondary);font-size:var(--wb-font-body-size);font-weight:var(--wb-font-body-weight);line-height:var(--wb-font-body-line-height);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}
|
||||
:where(.yc-capability-center) ._expertArrow_1a4sl_162{color:var(--wb-icon-muted);flex-shrink:0}
|
||||
:where(.yc-capability-center) ._tag_1a4sl_166.wb-tag{background:var(--ec-featured-scene-tag-bg);max-width:64px;height:22px;color:var(--ec-featured-scene-tag-color);box-sizing:border-box;border:none;border-radius:6px;flex:none;justify-content:center;align-items:center;padding:0 7px;font-size:11px;font-weight:500;line-height:16px;display:inline-flex;overflow:hidden}
|
||||
:where(.yc-capability-center) ._tag_1a4sl_166 .wb-tag__label{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}
|
||||
@keyframes _loading_1a4sl_116{0%{background-position:-200% 0}to{background-position:200% 0}}
|
||||
:where(.yc-capability-center) ._wrap_1fhtx_1{min-width:0}
|
||||
:where(.yc-capability-center) ._tabs_1fhtx_4{border:1px solid var(--wb-border-weak);background:var(--wb-bg-tertiary);box-sizing:border-box;border-radius:6px;flex-wrap:nowrap;align-items:center;gap:2px;min-width:0;height:28px;padding:2px;display:inline-flex}
|
||||
:where(.yc-capability-center) ._tab_1fhtx_4{height:24px;color:var(--wb-color-text-tertiary);white-space:nowrap;cursor:pointer;background:0 0;border:none;border-radius:4px;flex-shrink:0;justify-content:center;align-items:center;padding:0 8px;font-size:12px;font-weight:500;line-height:22px;transition:background .15s,color .15s;display:inline-flex}
|
||||
:where(.yc-capability-center) ._tab_1fhtx_4:hover{color:var(--wb-color-text-secondary)}
|
||||
:where(.yc-capability-center) ._tab_1fhtx_4._active_1fhtx_38{background:var(--wb-bg-card);color:var(--wb-color-text-primary)}
|
||||
:where(.yc-capability-center) ._count_1fhtx_42{color:inherit;font-size:11px;font-weight:400;font-family:var(--font-mono,monospace);margin-left:4px;display:inline}
|
||||
:where(.yc-capability-center) .expert-center-dark ._tab_1fhtx_4._active_1fhtx_38{background:var(--wb-bg-elevated)}
|
||||
:where(.yc-capability-center) .expert-center-light.theme-switching ._tab_1fhtx_4,:where(.yc-capability-center) body.theme-switching ._tab_1fhtx_4{transition:none}
|
||||
:where(.yc-capability-center) ._row_ki2oo_1{margin-bottom:var(--wb-spacing-5);justify-content:space-between;align-items:center;gap:12px;display:flex}
|
||||
:where(.yc-capability-center) ._tabs_ki2oo_8{align-items:center;gap:var(--wb-spacing-5);flex-shrink:0;display:inline-flex}
|
||||
:where(.yc-capability-center) ._actions_ki2oo_14{align-items:center;gap:12px;min-width:0;margin-left:auto;display:inline-flex}
|
||||
:where(.yc-capability-center) ._tab_ki2oo_8{color:var(--wb-text-weak);font-size:var(--wb-font-spec-body-lg-size);font-weight:600;line-height:var(--wb-font-spec-body-lg-line-height);white-space:nowrap;cursor:pointer;background:0 0;border:none;align-items:center;gap:6px;padding:0;transition:opacity .15s;display:inline-flex}
|
||||
:where(.yc-capability-center) ._tab_ki2oo_8:is(span){cursor:default}
|
||||
:where(.yc-capability-center) ._tab_ki2oo_8:hover,:where(.yc-capability-center) ._tab_ki2oo_8._active_ki2oo_42{color:var(--wb-color-text-primary)}
|
||||
:where(.yc-capability-center) ._sortGroup_ki2oo_45{height:var(--wb-spacing-8);padding:var(--wb-spacing-1);border:calc(var(--wb-spacing-1) / 4) solid var(--wb-color-border-primary);border-radius:var(--wb-radius-lg);background:var(--wb-bg-inset-strong);box-sizing:border-box;flex-shrink:0;align-items:center;gap:2px;display:inline-flex}
|
||||
:where(.yc-capability-center) ._sortBtn_ki2oo_57{height:calc(var(--wb-spacing-8) - var(--wb-spacing-1) * 2);padding:0 var(--wb-spacing-4);border-radius:var(--wb-radius-md);color:var(--wb-color-text-secondary);font-size:var(--wb-font-spec-body-md-size);font-weight:var(--wb-font-weight-400);line-height:var(--wb-font-spec-body-md-line-height);cursor:pointer;white-space:nowrap;background:0 0;border:none;justify-content:center;align-items:center;transition:background .15s,color .15s;display:inline-flex}
|
||||
:where(.yc-capability-center) ._sortBtn_ki2oo_57:hover{color:var(--wb-color-text-secondary)}
|
||||
:where(.yc-capability-center) ._sortBtn_ki2oo_57._active_ki2oo_42{background:var(--wb-bg-card);color:var(--wb-color-text-primary);font-weight:var(--wb-font-weight-600)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._sortBtn_ki2oo_57._active_ki2oo_42{background:var(--wb-bg-elevated);box-shadow:none}
|
||||
:where(.yc-capability-center) .expert-center-light.theme-switching ._sortBtn_ki2oo_57,:where(.yc-capability-center) body.theme-switching ._sortBtn_ki2oo_57{transition:none}
|
||||
:where(.yc-capability-center) .ec-delete-confirm-content{color:var(--wb-color-text-secondary);flex-direction:column;gap:12px;font-size:14px;line-height:1.6;display:flex}
|
||||
:where(.yc-capability-center) .ec-delete-confirm-cloud-option{border:1px solid var(--wb-border-default);background:color-mix(in srgb, var(--wb-bg-card) 74%, var(--wb-bg-hover));color:var(--wb-color-text-primary);cursor:pointer;border-radius:10px;align-items:flex-start;gap:8px;padding:10px 12px;display:flex}
|
||||
:where(.yc-capability-center) .ec-delete-confirm-cloud-option input{flex-shrink:0;margin:3px 0 0}
|
||||
:where(.yc-capability-center) .ec-delete-confirm-cloud-option:has(input:disabled){cursor:not-allowed;opacity:.7}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal .wb-modal__close{z-index:1;pointer-events:auto}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal .wb-modal__body{overflow:visible}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal .wb-modal__footer{padding-top:20px}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal .wb-modal__footer-actions>button,:where(.yc-capability-center) .expert-cloud-upload-confirm-modal .wb-modal__footer>button{border-radius:8px;min-width:66px;height:32px}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__content{min-width:0;padding-top:8px}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__hero{text-align:center;flex-direction:column;align-items:center;display:flex}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__flow{justify-content:center;align-items:center;gap:16px;height:40px;display:flex}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__arrow{display:inline-flex}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__target-icon{flex:0 0 40px;overflow:visible}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__avatar-stack{flex:none;height:40px;position:relative}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__avatar{background:var(--wb-bg-secondary);width:40px;height:40px;box-shadow:0 0 0 2px var(--wb-modal-panel-bg);color:var(--wb-color-text-primary);border-radius:50%;justify-content:center;align-items:center;font-size:18px;font-weight:600;display:inline-flex;position:absolute;top:0;overflow:hidden}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__avatar img{object-fit:cover;width:100%;height:100%;display:block}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__avatar--count{background:var(--wb-bg-secondary);font-size:18px}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__title,:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__summary,:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__description{text-align:center}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__title{color:var(--wb-color-text-primary);margin:16px 0 0;font-size:16px;font-weight:600;line-height:24px}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__summary{color:var(--wb-color-text-primary);margin:8px 0 0;font-size:14px;font-weight:600;line-height:20px}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__description{color:var(--wb-color-text-secondary);margin:12px 0 0;font-size:14px;line-height:22px}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__risk-panel,:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__status{background:var(--wb-bg-secondary);color:var(--wb-color-text-secondary);border-radius:8px;margin-top:16px;padding:12px;font-size:14px;line-height:22px}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__risk-panel p,:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__status p{margin:0}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__status{align-items:flex-start;gap:8px;display:flex}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__progress-content{flex:1;min-width:0}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__progress-content p{margin:0}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__progress-text{color:var(--wb-color-text-tertiary);margin-top:6px;font-size:12px;line-height:18px}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__progress-bar{background:color-mix(in srgb, var(--wb-brand-primary) 12%, transparent);border-radius:999px;height:6px;margin-top:8px;overflow:hidden}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__progress-bar-fill{border-radius:inherit;background:var(--wb-brand-primary);height:100%;transition:width .2s}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__status-copy{flex-direction:column;gap:2px;min-width:0;display:flex}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__status-hint{color:var(--wb-color-text-tertiary,var(--wb-color-text-secondary));font-size:12px;line-height:18px}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__spinner{border:2px solid var(--wb-brand-primary);border-top-color:#0000;border-radius:50%;width:14px;height:14px;margin-top:4px;animation:.8s linear infinite expert-cloud-upload-confirm-spin}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__checkbox{color:var(--wb-color-text-secondary);cursor:pointer;align-items:flex-start;gap:8px;margin-top:10px;display:flex}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__checkbox input{flex-shrink:0;margin-top:4px}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__footer{justify-content:space-between;align-items:center;gap:12px;width:100%;display:flex}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__footer-actions{align-items:center;gap:12px;margin-left:auto;display:flex}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__footer-checkbox{align-items:center;margin-top:0;font-size:14px;line-height:22px}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__footer-checkbox input{width:16px;height:16px;margin-top:0}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__risk-panel--error{background:color-mix(in srgb, var(--wb-status-error,#ff4d4f) 10%, var(--wb-bg-secondary))}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__error-title{color:var(--wb-color-text-primary);font-weight:600}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__failed-list{margin-top:10px}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__failed-list ul{flex-direction:column;gap:6px;margin:6px 0 0;padding:0;list-style:none;display:flex}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__failed-list li{color:var(--wb-color-text-tertiary);justify-content:space-between;gap:12px;font-size:12px;line-height:18px;display:flex}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__failed-list li span:first-child{max-width:160px;color:var(--wb-color-text-secondary);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}
|
||||
:where(.yc-capability-center) .expert-cloud-upload-confirm-modal__failed-list li span:last-child{text-align:right;text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}
|
||||
@keyframes expert-cloud-upload-confirm-spin{to{transform:rotate(360deg)}}
|
||||
:where(.yc-capability-center) .refund-confirm-modal-overlay{z-index:100002;background:linear-gradient(#0009,#0009);justify-content:center;align-items:center;animation:.16s refund-confirm-modal-fade-in;display:flex;position:fixed;inset:0}
|
||||
:where(.yc-capability-center) .refund-confirm-modal-card{box-sizing:border-box;background:var(--wb-bg-primary,#fff);border:1px solid var(--wb-border-card,#00000014);border-radius:12px;flex-direction:column;gap:12px;width:480px;max-width:calc(100vw - 32px);padding:24px;animation:.18s refund-confirm-modal-pop-in;display:flex;box-shadow:0 6px 12px #0000000a,0 8px 48px #00000014}
|
||||
@keyframes refund-confirm-modal-fade-in{0%{opacity:0}to{opacity:1}}
|
||||
@keyframes refund-confirm-modal-pop-in{0%{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}
|
||||
:where(.yc-capability-center) .refund-confirm-modal-header{justify-content:space-between;align-items:flex-start;gap:12px;display:flex}
|
||||
:where(.yc-capability-center) .refund-confirm-modal-title{min-width:0;color:var(--wb-color-text-primary,#000);flex:1;margin:0;font-size:16px;font-weight:600;line-height:24px}
|
||||
:where(.yc-capability-center) .refund-confirm-modal-close{width:24px;height:24px;color:var(--wb-color-text-secondary,#000000b3);cursor:pointer;background:0 0;border:none;border-radius:12px;flex-shrink:0;justify-content:center;align-items:center;padding:0;transition:background .15s,color .15s;display:flex}
|
||||
:where(.yc-capability-center) .refund-confirm-modal-close:hover{background:var(--wb-bg-hover,#0000000d);color:var(--wb-color-text-primary,#000)}
|
||||
:where(.yc-capability-center) .refund-confirm-modal-body{color:var(--wb-color-text-secondary,#000000b3);margin:0;font-size:14px;line-height:22px}
|
||||
:where(.yc-capability-center) .refund-confirm-modal-footer{justify-content:flex-end;gap:12px;padding-top:12px;display:flex}
|
||||
:where(.yc-capability-center) .refund-confirm-modal-btn{cursor:pointer;background:0 0;border-radius:6px;height:32px;padding:0 12px;font-size:14px;line-height:22px;transition:opacity .15s,background .15s}
|
||||
:where(.yc-capability-center) .refund-confirm-modal-btn--cancel{background:var(--wb-button-secondary-bg,#fff);border:1px solid var(--wb-button-secondary-border,#00000026);color:var(--wb-button-secondary-fg,#000000e6)}
|
||||
:where(.yc-capability-center) .refund-confirm-modal-btn--cancel:hover{background:var(--wb-button-secondary-bg-hover,#0000000d)}
|
||||
:where(.yc-capability-center) .refund-confirm-modal-btn--danger{color:#fff;background:#f5504e;border:none}
|
||||
:where(.yc-capability-center) .refund-confirm-modal-btn--danger:hover{background:#e63f3d}
|
||||
:where(.yc-capability-center) [data-theme=dark] .refund-confirm-modal-card,:where(.yc-capability-center) .vscode-dark .refund-confirm-modal-card,:where(.yc-capability-center) .dark .refund-confirm-modal-card{box-shadow:0 6px 12px #0003,0 8px 48px #0006}
|
||||
:where(.yc-capability-center) ._section_kfx5i_1{margin-bottom:var(--wb-spacing-8)}
|
||||
:where(.yc-capability-center) ._sectionTitle_kfx5i_4{align-items:center;gap:var(--wb-spacing-2);margin-bottom:var(--wb-spacing-5);color:var(--wb-color-text-primary);font-family:var(--wb-font-family,"PingFang SC");font-size:var(--wb-font-h3-size);font-weight:var(--wb-font-h3-weight);font-style:normal;line-height:normal;display:inline-flex}
|
||||
:where(.yc-capability-center) ._sectionIcon_kfx5i_16{width:var(--wb-spacing-5);height:var(--wb-spacing-5);flex-shrink:0;display:block}
|
||||
:where(.yc-capability-center) ._list_kfx5i_22{align-items:flex-start;gap:calc(var(--wb-spacing-5) - var(--wb-spacing-1));flex-shrink:0;grid-template-columns:repeat(3,minmax(0,1fr));display:grid}
|
||||
:where(.yc-capability-center) ._card_kfx5i_29{min-width:0;height:calc(var(--wb-spacing-12) * 2 + var(--wb-spacing-6) + var(--wb-spacing-3));box-sizing:border-box;border:.5px solid var(--wb-color-border-solid);border-radius:var(--wb-radius-2xl);background:var(--wb-bg-primary);color:inherit;text-align:left;cursor:pointer;transition:border-color var(--wb-motion-duration-fast) var(--wb-motion-easing-standard), background-color var(--wb-motion-duration-fast) var(--wb-motion-easing-standard);flex-direction:column;padding:0;display:flex;overflow:hidden}
|
||||
:where(.yc-capability-center) ._card_kfx5i_29:hover{border-color:var(--wb-color-border-solid);background:var(--wb-palette-gray-3)}
|
||||
:where(.yc-capability-center) ._expertCard_kfx5i_49{height:calc(var(--wb-spacing-12) * 2 + var(--wb-spacing-9) + var(--wb-spacing-4))}
|
||||
:where(.yc-capability-center) ._expertCard_kfx5i_49 ._cover_kfx5i_52{height:calc(var(--wb-spacing-10) * 2 + var(--wb-spacing-2))}
|
||||
:where(.yc-capability-center) ._expertCard_kfx5i_49 ._info_kfx5i_55{min-height:calc(var(--wb-spacing-10) + var(--wb-spacing-8));padding:var(--wb-spacing-4)}
|
||||
:where(.yc-capability-center) ._expertCard_kfx5i_49 ._subtitle_kfx5i_59{-webkit-line-clamp:2}
|
||||
:where(.yc-capability-center) ._compact_kfx5i_63{height:auto}
|
||||
:where(.yc-capability-center) ._compact_kfx5i_63 ._info_kfx5i_55{min-height:0}
|
||||
:where(.yc-capability-center) ._cover_kfx5i_52{width:100%;height:calc(var(--wb-spacing-12) + var(--wb-spacing-8) + var(--wb-spacing-1));background:var(--wb-bg-secondary);flex-shrink:0;display:flex;overflow:hidden}
|
||||
:where(.yc-capability-center) ._cover_kfx5i_52 img{object-fit:cover;width:100%;height:100%;display:block}
|
||||
:where(.yc-capability-center) ._coverPlaceholder_kfx5i_83{background:linear-gradient(135deg, var(--wb-bg-secondary), var(--wb-bg-hover));width:100%;height:100%}
|
||||
:where(.yc-capability-center) ._info_kfx5i_55{min-height:calc(var(--wb-spacing-10) + var(--wb-spacing-3) + var(--wb-spacing-1));padding:calc(var(--wb-spacing-3) + var(--wb-spacing-1)) var(--wb-spacing-4);box-sizing:border-box;align-items:flex-start;gap:var(--wb-spacing-1);flex-direction:column;flex-shrink:0;display:flex}
|
||||
:where(.yc-capability-center) ._title_kfx5i_98{color:var(--wb-color-text-primary);font-family:var(--wb-font-family,"PingFang SC");text-align:left;font-size:var(--wb-font-body-size);-webkit-line-clamp:1;-webkit-box-orient:vertical;align-self:stretch;font-style:normal;font-weight:600;line-height:20px;text-decoration-line:none;display:-webkit-box;overflow:hidden}
|
||||
:where(.yc-capability-center) ._subtitle_kfx5i_59{color:var(--wb-color-text-secondary);font-family:var(--wb-font-family,"PingFang SC");font-size:var(--wb-font-caption-size);-webkit-line-clamp:1;-webkit-box-orient:vertical;align-self:stretch;font-style:normal;font-weight:400;line-height:17px;text-decoration-line:none;display:-webkit-box;overflow:hidden}
|
||||
:where(.yc-capability-center) .expert-center-dark ._sectionTitle_kfx5i_4{color:var(--wb-palette-white-90)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._sectionIcon_kfx5i_16{filter:brightness(0)invert();opacity:1}
|
||||
:where(.yc-capability-center) .expert-center-dark ._card_kfx5i_29{border-color:var(--wb-color-border-solid);background:#313131}
|
||||
:where(.yc-capability-center) .expert-center-dark ._card_kfx5i_29:hover{border-color:var(--wb-color-border-solid);background:var(--wb-palette-gray-5)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._info_kfx5i_55{background:inherit}
|
||||
:where(.yc-capability-center) .expert-center-dark ._title_kfx5i_98{color:var(--wb-palette-white-90)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._subtitle_kfx5i_59{color:var(--wb-palette-white-70)}
|
||||
:where(.yc-capability-center) ._list_5j5f1_1{column-gap:var(--wb-spacing-6);row-gap:calc(var(--wb-spacing-4) - var(--wb-spacing-1));grid-template-columns:repeat(3,minmax(0,1fr));display:grid}
|
||||
:where(.yc-capability-center) ._member_5j5f1_7{align-items:center;gap:var(--wb-spacing-3);border-bottom:0;min-width:0;padding:0;display:flex}
|
||||
:where(.yc-capability-center) ._avatar_5j5f1_15{width:calc(var(--wb-spacing-7) + var(--wb-spacing-1));height:calc(var(--wb-spacing-7) + var(--wb-spacing-1));border-radius:var(--wb-radius-lg);background:var(--wb-bg-secondary);flex-shrink:0;overflow:hidden}
|
||||
:where(.yc-capability-center) ._avatar_5j5f1_15 img{object-fit:cover;width:100%;height:100%;display:block}
|
||||
:where(.yc-capability-center) ._avatarPlaceholder_5j5f1_29{width:100%;height:100%;color:var(--wb-text-on-inverse);font-size:calc(var(--wb-font-caption-size) - var(--wb-spacing-1) / 2);justify-content:center;align-items:center;font-weight:600;display:flex}
|
||||
:where(.yc-capability-center) ._info_5j5f1_39{min-width:0;min-height:var(--wb-spacing-7);justify-content:center;gap:var(--wb-spacing-0);flex-direction:column;flex:1;display:flex;overflow:hidden}
|
||||
:where(.yc-capability-center) ._roleRow_5j5f1_49{align-items:center;gap:var(--wb-spacing-2);width:100%;min-width:0;margin-bottom:calc(0px - var(--wb-spacing-2));line-height:var(--wb-font-code-line-height);display:flex}
|
||||
:where(.yc-capability-center) ._role_5j5f1_49{min-width:0;margin-bottom:calc(var(--wb-spacing-1) / 2);color:var(--wb-color-text-primary);font-family:var(--wb-font-family,"PingFang SC");font-size:var(--wb-font-caption-size);font-style:normal;font-weight:500;line-height:var(--wb-font-code-line-height);white-space:nowrap;text-overflow:ellipsis;flex:0 auto;text-decoration-line:none;overflow:hidden}
|
||||
:where(.yc-capability-center) ._name_5j5f1_73{text-overflow:ellipsis;width:100%;min-width:0;color:var(--wb-color-text-tertiary);font-family:var(--wb-font-family,"PingFang SC");font-size:calc(var(--wb-font-caption-size) - var(--wb-spacing-1));font-style:normal;font-weight:400;line-height:var(--wb-font-caption-line-height);white-space:nowrap;text-decoration-line:none;display:block;overflow:hidden}
|
||||
:where(.yc-capability-center) ._leaderBadge_5j5f1_88{margin-bottom:calc(var(--wb-spacing-1) / 2);align-items:center;gap:var(--wb-spacing-2);padding:0 calc(var(--wb-spacing-2) + var(--wb-spacing-1)) 0 var(--wb-spacing-2);border-radius:var(--wb-radius-sm);box-sizing:border-box;background:var(--wb-bg-hover);color:var(--wb-color-text-secondary);font-family:var(--wb-font-family,"PingFang SC");font-size:calc(var(--wb-font-caption-size) - var(--wb-spacing-1) / 2);font-style:normal;font-weight:500;line-height:var(--wb-font-caption-line-height);flex-shrink:0;text-decoration-line:none;display:flex}
|
||||
:where(.yc-capability-center) ._leaderBadgeIcon_5j5f1_106{object-fit:contain;flex-shrink:0;width:9.76098px;height:10.5px;display:block}
|
||||
:where(.yc-capability-center) ._skeletonAvatar_5j5f1_113{width:100%;height:100%}
|
||||
:where(.yc-capability-center) ._skeletonRole_5j5f1_117{border-radius:4px;width:72px;height:13px;display:inline-block}
|
||||
:where(.yc-capability-center) ._skeletonName_5j5f1_123{border-radius:4px;width:48px;height:11px;display:inline-block}
|
||||
:where(.yc-capability-center) ._panel_1yk5g_1{--ec-qpc-panel-padding-block:var(--wb-spacing-9);--ec-qpc-panel-padding-inline:var(--wb-spacing-7);--ec-qpc-bubble-padding-block:calc(var(--wb-spacing-4) - var(--wb-spacing-1) / 2);--ec-qpc-bubble-padding-inline:var(--wb-spacing-5);--ec-qpc-bubble-gap:var(--wb-spacing-4);--ec-qpc-bubble-radius:var(--wb-radius-2xl);--ec-qpc-bubble-shadow:var(--wb-shadow-card-soft);--ec-qpc-content-gap:var(--wb-spacing-4);--ec-qpc-slot-height:calc(var(--wb-spacing-10) + var(--wb-spacing-2));--ec-qpc-static-height:calc(var(--ec-qpc-slot-height) * 3 + var(--ec-qpc-bubble-gap) * 2);--ec-qpc-shadow-safe-x:var(--wb-spacing-4);--ec-qpc-shadow-safe-y:var(--wb-spacing-7);--ec-qpc-edge-opacity:.78;--ec-qpc-focus-scale:1.018;justify-content:center;align-items:stretch;gap:var(--wb-spacing-5);margin-top:var(--wb-spacing-5);background:0 0;border-radius:0;flex-direction:column;flex-shrink:0;padding:0;display:flex;position:relative}
|
||||
:where(.yc-capability-center) ._stack_1yk5g_28{gap:var(--wb-spacing-4);flex-direction:column;width:100%;display:flex}
|
||||
:where(.yc-capability-center) ._stack_1yk5g_28 ._slot_1yk5g_34{height:auto}
|
||||
:where(.yc-capability-center) ._carouselStage_1yk5g_37{width:100%;height:var(--ec-qpc-static-height);isolation:isolate;position:relative;overflow:visible}
|
||||
:where(.yc-capability-center) ._viewport_1yk5g_44{inset-block:calc(0px - var(--ec-qpc-shadow-safe-y));inset-inline:calc(0px - var(--ec-qpc-shadow-safe-x));padding-block:var(--ec-qpc-shadow-safe-y);padding-inline:var(--ec-qpc-shadow-safe-x);pointer-events:auto;position:absolute;overflow:hidden}
|
||||
:where(.yc-capability-center) ._viewport_1yk5g_44:before,:where(.yc-capability-center) ._viewport_1yk5g_44:after{content:"";left:var(--ec-qpc-shadow-safe-x);right:var(--ec-qpc-shadow-safe-x);z-index:4;height:var(--ec-qpc-shadow-safe-y);pointer-events:none;position:absolute}
|
||||
:where(.yc-capability-center) ._viewport_1yk5g_44:before{background:linear-gradient(180deg, var(--wb-bg-secondary) 0%, transparent 100%);top:0}
|
||||
:where(.yc-capability-center) ._viewport_1yk5g_44:after{background:linear-gradient(0deg, var(--wb-bg-secondary) 0%, transparent 100%);bottom:0}
|
||||
:where(.yc-capability-center) ._track_1yk5g_71{gap:var(--ec-qpc-bubble-gap);will-change:transform;backface-visibility:hidden;flex-direction:column;width:100%;transition-property:transform;transition-timing-function:ease-in-out;display:flex}
|
||||
:where(.yc-capability-center) ._slot_1yk5g_34{--ec-qpc-slot-focus:0;--ec-qpc-slot-focus-scale:1;width:100%;height:var(--ec-qpc-slot-height);box-sizing:border-box;transform:scale(var(--ec-qpc-slot-focus-scale));transition:opacity var(--wb-motion-duration-sm) var(--wb-motion-easing-standard), transform var(--wb-motion-duration-sm) var(--wb-motion-easing-standard);flex:none;justify-content:center;align-items:center;display:flex;position:relative}
|
||||
:where(.yc-capability-center) ._carousel_1yk5g_37 ._slot_1yk5g_34{opacity:var(--ec-qpc-edge-opacity);will-change:transform, opacity;backface-visibility:hidden}
|
||||
:where(.yc-capability-center) ._carousel_1yk5g_37 ._slot_1yk5g_34._hidden_1yk5g_100{pointer-events:none}
|
||||
:where(.yc-capability-center) ._carousel_1yk5g_37 ._slot_1yk5g_34._focused_1yk5g_103,:where(.yc-capability-center) ._carousel_1yk5g_37 ._stack_1yk5g_28:has(._slot_1yk5g_34:hover) ._slot_1yk5g_34:hover,:where(.yc-capability-center) ._carousel_1yk5g_37 ._stack_1yk5g_28:has(._slot_1yk5g_34:focus-within) ._slot_1yk5g_34:focus-within{--ec-qpc-slot-focus:1;--ec-qpc-slot-focus-scale:var(--ec-qpc-focus-scale);opacity:1;z-index:6}
|
||||
:where(.yc-capability-center) ._carousel_1yk5g_37 ._stack_1yk5g_28:has(._slot_1yk5g_34:hover) ._slot_1yk5g_34,:where(.yc-capability-center) ._carousel_1yk5g_37 ._stack_1yk5g_28:has(._slot_1yk5g_34:focus-within) ._slot_1yk5g_34{opacity:var(--ec-qpc-edge-opacity)}
|
||||
:where(.yc-capability-center) ._btn_1yk5g_115{width:100%;padding:var(--wb-spacing-4) var(--wb-spacing-5);box-sizing:border-box;justify-content:center;align-items:center;gap:var(--wb-spacing-5);background:var(--wb-palette-gray-2);border-radius:var(--wb-radius-2xl);box-shadow:none;color:var(--wb-color-text-secondary);font-family:var(--wb-font-family,"PingFang SC");font-size:var(--wb-font-body-size);font-weight:var(--wb-font-body-weight);font-style:normal;line-height:var(--wb-font-body-line-height);letter-spacing:0;text-align:left;cursor:pointer;transition:background var(--wb-motion-duration-sm) var(--wb-motion-easing-standard), border-color var(--wb-motion-duration-sm) var(--wb-motion-easing-standard);border:.5px solid #0000;flex-shrink:0;display:flex}
|
||||
:where(.yc-capability-center) ._btn_1yk5g_115:hover{background:var(--wb-palette-gray-3);border-color:var(--wb-color-border-solid)}
|
||||
:where(.yc-capability-center) ._btn_1yk5g_115:focus-visible{outline:calc(var(--wb-spacing-1) / 2) solid var(--wb-border-focus);outline-offset:calc(var(--wb-spacing-1) / 2)}
|
||||
:where(.yc-capability-center) ._text_1yk5g_147{min-width:0;color:inherit;white-space:normal;overflow-wrap:anywhere;line-clamp:2;-webkit-line-clamp:2;-webkit-box-orient:vertical;flex:1;display:-webkit-box;overflow:hidden}
|
||||
:where(.yc-capability-center) ._action_1yk5g_159{width:var(--wb-spacing-5);height:var(--wb-spacing-5);color:var(--wb-palette-black-50);transition:color var(--wb-motion-duration-sm) var(--wb-motion-easing-standard);flex-shrink:0;display:block}
|
||||
:where(.yc-capability-center) ._actionTrigger_1yk5g_167{width:var(--wb-spacing-5);height:var(--wb-spacing-5);justify-content:center;align-items:center;display:inline-flex}
|
||||
:where(.yc-capability-center) ._btn_1yk5g_115:hover ._action_1yk5g_159{color:var(--wb-palette-black-70)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._btn_1yk5g_115{color:var(--wb-palette-white-90);background:#292929;border-color:#0000}
|
||||
:where(.yc-capability-center) .expert-center-dark ._btn_1yk5g_115:hover{background:#ffffff14;border-color:#3d3d3d}
|
||||
:where(.yc-capability-center) .expert-center-dark ._action_1yk5g_159,:where(.yc-capability-center) .expert-center-dark ._btn_1yk5g_115:hover ._action_1yk5g_159{color:var(--wb-palette-white-90)}
|
||||
@media (prefers-reduced-motion:reduce){:where(.yc-capability-center) ._track_1yk5g_71,:where(.yc-capability-center) ._slot_1yk5g_34,:where(.yc-capability-center) ._btn_1yk5g_115{transition:none!important}}
|
||||
:where(.yc-capability-center) ._scrollShell_944zj_1{flex-direction:column;flex:1;min-height:0;display:flex;position:relative}
|
||||
:where(.yc-capability-center) ._body_944zj_8{min-height:0;padding:0 var(--wb-spacing-7) var(--wb-spacing-7);scrollbar-width:thin;scrollbar-color:var(--wb-border-default) transparent;flex:1;overflow-y:auto}
|
||||
:where(.yc-capability-center) ._body_944zj_8::-webkit-scrollbar{width:4px}
|
||||
:where(.yc-capability-center) ._body_944zj_8::-webkit-scrollbar-thumb{background:var(--wb-border-default);border-radius:999px}
|
||||
:where(.yc-capability-center) ._scrollFade_944zj_23{z-index:2;height:calc(var(--wb-spacing-10) + var(--wb-spacing-4));pointer-events:none;opacity:0;transition:opacity var(--wb-motion-duration-sm) var(--wb-motion-easing-standard);position:absolute;left:0;right:0}
|
||||
:where(.yc-capability-center) ._scrollFadeTop_944zj_33{background:linear-gradient(180deg, var(--wb-bg-primary) 0%, transparent 100%);top:0}
|
||||
:where(.yc-capability-center) ._scrollFadeBottom_944zj_37{background:linear-gradient(0deg, var(--wb-bg-primary) 0%, transparent 100%);bottom:0}
|
||||
:where(.yc-capability-center) ._scrollShell_944zj_1._hasTopFade_944zj_41 ._scrollFadeTop_944zj_33,:where(.yc-capability-center) ._scrollShell_944zj_1._hasBottomFade_944zj_42 ._scrollFadeBottom_944zj_37{opacity:1}
|
||||
:where(.yc-capability-center) ._top_944zj_45{align-items:center;gap:var(--wb-spacing-7);margin-bottom:var(--wb-spacing-7);padding:var(--wb-spacing-7) var(--wb-spacing-7) 0;flex-shrink:0;display:flex}
|
||||
:where(.yc-capability-center) ._avatar_944zj_53{width:calc(var(--wb-spacing-12) + var(--wb-spacing-3));height:calc(var(--wb-spacing-12) + var(--wb-spacing-3));border-radius:var(--wb-radius-4xl);flex-shrink:0;justify-content:center;align-items:center;display:flex;position:relative;overflow:hidden}
|
||||
:where(.yc-capability-center) ._avatar_944zj_53 img{object-fit:cover;width:100%;height:100%;display:block;position:absolute;inset:0}
|
||||
:where(.yc-capability-center) ._avatarImagePending_944zj_72{visibility:hidden}
|
||||
:where(.yc-capability-center) .ec-modal-tag-chip{height:var(--wb-spacing-7);padding:0 calc(var(--wb-spacing-3) + var(--wb-spacing-1));border-radius:var(--wb-radius-md);background:var(--wb-palette-gray-3);color:var(--wb-color-text-secondary);font-size:var(--wb-font-caption-size);border:0;align-items:center;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-modal-tag-chip--paid{align-items:center;gap:var(--wb-spacing-1);padding:0 4px 0 var(--wb-spacing-3);color:#000000b3;background:#fceed0;font-weight:400;display:inline-flex}
|
||||
:where(.yc-capability-center) .ec-modal-tag-chip--paid__icon{width:calc(var(--wb-spacing-5) - var(--wb-spacing-1));height:calc(var(--wb-spacing-5) - var(--wb-spacing-1));object-fit:contain;flex-shrink:0;display:block}
|
||||
:where(.yc-capability-center) .ec-modal-tag-chip--purchased{cursor:pointer;font:inherit;border:0}
|
||||
:where(.yc-capability-center) .ec-modal-tag-chip--purchased__chevron{width:var(--wb-spacing-4);height:var(--wb-spacing-4);margin-right:calc(-1 * var(--wb-spacing-1));color:#00000080;flex-shrink:0;display:block}
|
||||
:where(.yc-capability-center) .ec-modal-header-summon-btn.ec-modal-header-summon-btn--paid{height:var(--wb-spacing-8);padding:0 calc(var(--wb-spacing-6) + var(--wb-spacing-2));gap:var(--wb-spacing-1);color:#edcb91;font-size:var(--wb-font-body-size);font-weight:var(--wb-font-weight-500);line-height:var(--wb-font-body-line-height);letter-spacing:0;background:#000000e6}
|
||||
:where(.yc-capability-center) .ec-modal-header-summon-btn.ec-modal-header-summon-btn--paid:hover{background:color-mix(in srgb, #000000e6 85%, var(--wb-palette-gray-4));color:#edcb91;opacity:1}
|
||||
:where(.yc-capability-center) ._avatarFallback_944zj_143{width:100%;height:100%;color:var(--wb-button-primary-fg);border-radius:var(--wb-radius-4xl);background:linear-gradient(135deg, var(--wb-avatar-bg-fallback-6), var(--wb-avatar-bg-fallback-2));justify-content:center;align-items:center;font-size:20px;font-weight:700;display:flex}
|
||||
:where(.yc-capability-center) ._info_944zj_155{flex:1;min-width:0}
|
||||
:where(.yc-capability-center) ._titleRow_944zj_159{justify-content:space-between;align-items:center;gap:var(--wb-spacing-4);flex-wrap:nowrap;width:100%;display:flex}
|
||||
:where(.yc-capability-center) ._actions_944zj_167{align-items:center;gap:var(--wb-spacing-4);flex-shrink:0;margin-left:auto;display:flex}
|
||||
:where(.yc-capability-center) ._role_944zj_174{min-width:0;color:var(--wb-color-text-primary);font-family:var(--wb-font-family,"PingFang SC");font-size:var(--wb-font-h2-size);font-weight:600;line-height:var(--wb-font-h2-line-height);letter-spacing:0;text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden;display:block!important}
|
||||
:where(.yc-capability-center) ._rolePrimary_944zj_188,:where(.yc-capability-center) ._roleSecondary_944zj_189{vertical-align:middle}
|
||||
:where(.yc-capability-center) ._roleSecondary_944zj_189{color:var(--wb-color-text-tertiary);font-weight:var(--wb-font-weight-400)}
|
||||
:where(.yc-capability-center) ._roleSeparator_944zj_196{width:calc(var(--wb-spacing-1) / 2);height:var(--wb-font-caption-line-height);margin:0 var(--wb-spacing-3);object-fit:fill;vertical-align:middle;flex-shrink:0;display:inline-block}
|
||||
:where(.yc-capability-center) ._cloudBadge_944zj_205{margin-left:var(--wb-spacing-2);vertical-align:middle;flex-shrink:0;display:inline-block}
|
||||
:where(.yc-capability-center) ._headerSummonBtn_944zj_125{width:auto;height:var(--wb-spacing-8);margin-top:var(--wb-spacing-4);padding:0 var(--wb-spacing-5);justify-content:center;align-items:center;gap:var(--wb-spacing-2);background:var(--wb-bg-inverse);color:var(--wb-button-primary-fg);border-radius:var(--wb-radius-lg);font-family:var(--wb-font-family,"PingFang SC");font-size:var(--wb-font-body-size);font-weight:var(--wb-font-weight-500);line-height:var(--wb-font-body-line-height);letter-spacing:0;cursor:pointer;border:none;flex-shrink:0;transition:background .15s,opacity .15s;display:flex}
|
||||
:where(.yc-capability-center) ._headerSummonBtn_944zj_125:hover{background:color-mix(in srgb, var(--wb-palette-black-100) 80%, var(--wb-palette-gray-4));opacity:1}
|
||||
:where(.yc-capability-center) ._sectionTitle_944zj_237{margin-bottom:var(--wb-spacing-1)}
|
||||
:where(.yc-capability-center) ._content_944zj_240{gap:var(--wb-spacing-5);margin-bottom:var(--wb-spacing-8);flex-direction:column;display:flex}
|
||||
:where(.yc-capability-center) ._desc_944zj_246{color:var(--wb-color-text-primary);font-size:var(--wb-font-body-size);line-height:var(--wb-font-body-line-height);margin:0}
|
||||
:where(.yc-capability-center) ._tagsList_944zj_252{gap:var(--wb-spacing-2);flex-wrap:wrap;display:flex}
|
||||
:where(.yc-capability-center) ._tagChip_944zj_80{height:var(--wb-spacing-7);padding:0 var(--wb-spacing-3);border-radius:var(--wb-radius-sm);background:var(--wb-palette-gray-3);color:var(--wb-color-text-secondary);font-size:var(--wb-font-body-size);line-height:var(--wb-font-body-line-height);border:0;align-items:center;display:inline-flex}
|
||||
:where(.yc-capability-center) ._metadataRow_944zj_269{width:100%;height:calc(var(--wb-spacing-12) + var(--wb-spacing-5));margin-bottom:var(--wb-spacing-8);border-top:1px solid var(--wb-border-subtle);border-bottom:1px solid var(--wb-border-subtle);box-sizing:border-box;display:flex}
|
||||
:where(.yc-capability-center) ._metadataRow_944zj_269:last-child{margin-bottom:0}
|
||||
:where(.yc-capability-center) ._metadataItem_944zj_286{min-width:0;height:100%;padding:0 var(--wb-spacing-4);justify-content:center;align-items:center;gap:var(--wb-spacing-2);box-sizing:border-box;flex-direction:column;flex:1 1 0;display:flex}
|
||||
:where(.yc-capability-center) ._metadataValue_944zj_283{max-width:100%;color:var(--wb-color-text-primary);font-size:var(--wb-font-spec-body-lg-size);font-weight:var(--wb-font-weight-600);line-height:var(--wb-font-spec-body-lg-line-height);text-overflow:ellipsis;white-space:nowrap;display:block;overflow:hidden}
|
||||
:where(.yc-capability-center) ._metadataValueWithIcon_944zj_309{align-items:center;gap:var(--wb-spacing-2);display:inline-flex}
|
||||
:where(.yc-capability-center) ._metadataValueWithIcon_944zj_309 svg,:where(.yc-capability-center) ._metadataValueWithIcon_944zj_309 ._metadataUsageIcon_944zj_315{flex-shrink:0}
|
||||
:where(.yc-capability-center) ._metadataUsageIcon_944zj_315{width:var(--wb-spacing-5);height:var(--wb-spacing-5);background:currentColor;display:block;mask:url("data:image/svg+xml,%3c?xml%20version='1.0'%20encoding='utf-8'%20?%3e%3csvg%20xmlns='http://www.w3.org/2000/svg'%20xmlns:xlink='http://www.w3.org/1999/xlink'%20width='16'%20height='16'%3e%3cclipPath%20id='clip_0'%3e%3crect%20width='16'%20height='16'/%3e%3c/clipPath%3e%3cg%20clip-path='url(%23clip_0)'%3e%3cpath%20fill='%23000'%20transform='matrix(1%200%200%201%201%20-0.749512)'%20d='M8.5821%205.6358Q8.5782%205.6639%208.5705%205.7201Q8.503%206.2088%208.5048%206.3468Q8.505%206.3625%208.5056%206.3752Q8.5182%206.3775%208.5337%206.3799Q8.6701%206.4004%209.1635%206.4Q9.2202%206.4%209.2486%206.4L11.2827%206.4Q11.4084%206.4%2011.6448%206.3982Q12.4721%206.3919%2012.7885%206.4466Q13.5075%206.5709%2013.761%207.112Q14.0144%207.6531%2013.6495%208.285Q13.489%208.563%2012.9545%209.1946Q12.8018%209.3751%2012.7213%209.4716L8.5533%2014.4732Q8.4211%2014.6319%208.1737%2014.934Q7.4209%2015.8535%207.0953%2016.1442Q6.3479%2016.8116%205.6832%2016.5183Q5.0185%2016.225%205.0077%2015.223Q5.0031%2014.7866%205.1748%2013.6107Q5.2312%2013.2243%205.2593%2013.0197L5.4179%2011.8642Q5.4219%2011.8349%205.4295%2011.7799Q5.497%2011.2912%205.4952%2011.1532Q5.495%2011.1375%205.4944%2011.1248Q5.4819%2011.1225%205.4663%2011.1201Q5.3299%2011.0996%204.8365%2011.1Q4.7798%2011.1%204.7514%2011.1L2.7173%2011.1Q2.5916%2011.1%202.3552%2011.1018Q1.5279%2011.1081%201.2115%2011.0534Q0.4925%2010.9291%200.239%2010.388Q-0.0144%209.847%200.3505%209.215Q0.511%208.937%201.0455%208.3054Q1.1982%208.1249%201.2787%208.0284L5.4467%203.0268Q5.5789%202.8681%205.8263%202.566Q6.5791%201.6465%206.9047%201.3558Q7.6521%200.6884%208.3168%200.9817Q8.9815%201.275%208.9923%202.277Q8.9969%202.7134%208.8252%203.8893Q8.7688%204.2756%208.7407%204.4803L8.5821%205.6358ZM7.3932%205.4726L7.5518%204.3171Q7.5806%204.1074%207.6378%203.7159Q7.796%202.6327%207.7923%202.2899Q7.7917%202.2289%207.7882%202.1795Q7.7495%202.2103%207.704%202.2509Q7.4483%202.4792%206.7547%203.3262Q6.5041%203.6324%206.3685%203.795L2.2005%208.7966Q2.1171%208.8967%201.9615%209.0806Q1.4965%209.6301%201.3897%209.8151Q1.3752%209.8401%201.3645%209.8609Q1.3873%209.866%201.4158%209.8709Q1.6263%209.9073%202.3461%209.9018Q2.5872%209.9%202.7173%209.9L4.7514%209.9Q4.7789%209.9%204.8357%209.9Q5.4192%209.8995%205.6449%209.9335Q6.1372%2010.0076%206.4129%2010.3237Q6.6886%2010.6399%206.6951%2011.1377Q6.698%2011.3659%206.6183%2011.9439Q6.6106%2011.9996%206.6068%2012.0274L6.4482%2013.1829Q6.4194%2013.3926%206.3622%2013.7841Q6.204%2014.8673%206.2077%2015.2101Q6.2083%2015.2711%206.2118%2015.3205Q6.2505%2015.2897%206.296%2015.2491Q6.5517%2015.0208%207.2453%2014.1738Q7.496%2013.8676%207.6315%2013.705L11.7995%208.7034Q11.8829%208.6033%2012.0385%208.4194Q12.5035%207.8699%2012.6103%207.6849Q12.6248%207.6599%2012.6355%207.6391Q12.6127%207.634%2012.5842%207.6291Q12.3737%207.5927%2011.6539%207.5982Q11.4129%207.6%2011.2827%207.6L9.2486%207.6Q9.2211%207.6%209.1643%207.6Q8.5808%207.6005%208.3551%207.5665Q7.8628%207.4924%207.5871%207.1763Q7.3114%206.8601%207.3049%206.3623Q7.302%206.1342%207.3817%205.5561Q7.3894%205.5004%207.3932%205.4726Z'%20fill-rule='evenodd'/%3e%3c/g%3e%3c/svg%3e") 50%/contain no-repeat}
|
||||
:where(.yc-capability-center) ._metadataItemClickable_944zj_331{cursor:pointer}
|
||||
:where(.yc-capability-center) ._metadataItemClickable_944zj_331:hover ._metadataValue_944zj_283{color:var(--wb-color-text-brand,var(--wb-color-text-primary))}
|
||||
:where(.yc-capability-center) ._metadataValueText_944zj_283{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}
|
||||
:where(.yc-capability-center) ._metadataDeveloperJumpIcon_944zj_343{width:var(--wb-spacing-5);height:var(--wb-spacing-5);color:var(--wb-color-text-tertiary);flex-shrink:0}
|
||||
:where(.yc-capability-center) ._metadataDeveloperJumpIcon_944zj_343:hover{color:var(--wb-color-text-primary)}
|
||||
:where(.yc-capability-center) ._metadataLabel_944zj_352{color:var(--wb-color-text-tertiary);font-size:var(--wb-font-caption-size);font-weight:var(--wb-font-caption-weight);line-height:var(--wb-font-caption-line-height);white-space:nowrap}
|
||||
:where(.yc-capability-center) ._quickPrompts_944zj_359{margin-bottom:var(--wb-spacing-8)}
|
||||
:where(.yc-capability-center) ._quickPrompts_944zj_359:last-child{margin-bottom:0}
|
||||
:where(.yc-capability-center) ._sectionTitleWithIcon_944zj_365{align-items:center;gap:var(--wb-spacing-2);margin-bottom:var(--wb-spacing-1);color:var(--wb-color-text-primary);font-family:var(--wb-font-family,"PingFang SC");font-size:var(--wb-font-h3-size);font-weight:var(--wb-font-h3-weight);font-style:normal;line-height:normal;display:inline-flex}
|
||||
:where(.yc-capability-center) ._sectionIcon_944zj_377{width:var(--wb-spacing-5);height:var(--wb-spacing-5);flex-shrink:0;display:block}
|
||||
:where(.yc-capability-center) ._teamMembersIcon_944zj_383{width:auto;height:var(--wb-spacing-5)}
|
||||
:where(.yc-capability-center) ._team_944zj_383{margin-bottom:0}
|
||||
:where(.yc-capability-center) ._team_944zj_383 ._sectionTitleWithIcon_944zj_365{margin-bottom:var(--wb-spacing-5)}
|
||||
:where(.yc-capability-center) ._more_944zj_394{gap:var(--wb-spacing-5);margin-top:var(--wb-spacing-8);flex-direction:column;display:flex}
|
||||
:where(.yc-capability-center) ._moreTitle_944zj_400{align-items:center;gap:var(--wb-spacing-2);color:var(--wb-color-text-primary);font-family:var(--wb-font-family,"PingFang SC");font-size:var(--wb-font-h3-size);font-weight:var(--wb-font-h3-weight);line-height:var(--wb-font-body-line-height);display:flex}
|
||||
:where(.yc-capability-center) ._moreTitleIcon_944zj_410{width:var(--wb-spacing-5);height:var(--wb-spacing-5);mask:var(--expert-more-title-icon) center / contain no-repeat;-webkit-mask:var(--expert-more-title-icon) center / contain no-repeat;background:currentColor;flex-shrink:0;display:block}
|
||||
:where(.yc-capability-center) ._moreBar_944zj_419{box-sizing:border-box;min-width:0;padding:var(--wb-spacing-4) var(--wb-spacing-5);border-radius:var(--wb-radius-2xl);background:var(--wb-bg-secondary);align-items:center;display:flex}
|
||||
:where(.yc-capability-center) ._moreLine_944zj_428{min-width:0;color:var(--wb-color-text-secondary);font-size:var(--wb-font-body-size);line-height:var(--wb-font-body-line-height);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
|
||||
:where(.yc-capability-center) .expert-center-dark ._close_944zj_442:hover{background:color-mix(in srgb, var(--wb-palette-white-100) 5%, var(--wb-palette-gray-4))}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-modal-tag-chip--paid{color:#000000b3;background:#fceed0}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-modal-header-summon-btn.ec-modal-header-summon-btn--paid{color:#000000bf;background:#fceed0}
|
||||
:where(.yc-capability-center) .expert-center-dark .ec-modal-header-summon-btn.ec-modal-header-summon-btn--paid:hover{background:color-mix(in srgb, #fceed0 88%, var(--wb-palette-black-100));color:#000000bf;opacity:1}
|
||||
:where(.yc-capability-center) .expert-center-dark ._roleSecondary_944zj_189{color:var(--wb-palette-white-70)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._roleSeparator_944zj_196{filter:brightness(0)invert();opacity:.7}
|
||||
:where(.yc-capability-center) .expert-center-dark ._sectionIcon_944zj_377{filter:brightness(0)invert();opacity:1}
|
||||
:where(.yc-capability-center) .expert-center-dark ._tagChip_944zj_80{color:#ffffffa6;background:#ffffff0a}
|
||||
:where(.yc-capability-center) .expert-center-dark ._sectionTitleWithIcon_944zj_365{color:var(--wb-palette-white-90)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._headerSummonBtn_944zj_125{background:color-mix(in srgb, var(--wb-palette-white-100) 92%, transparent);color:var(--wb-palette-black-90)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._headerSummonBtn_944zj_125:hover{background:var(--wb-button-primary-bg-hover);color:var(--wb-palette-black-90);opacity:1}
|
||||
@keyframes _developer-profile-overlay-in_385qb_1{0%{opacity:0}to{opacity:1}}
|
||||
@keyframes _developer-profile-card-in_385qb_1{0%{opacity:0;transform:scale(.96)translateY(8px)}to{opacity:1;transform:scale(1)translateY(0)}}
|
||||
:where(.yc-capability-center) ._overlay_385qb_3{z-index:1000;background:var(--wb-color-mask-color);justify-content:center;align-items:center;padding:20px;animation:.15s _developer-profile-overlay-in_385qb_1;display:flex;position:fixed;inset:0}
|
||||
:where(.yc-capability-center) ._card_385qb_3{background:var(--wb-bg-primary);width:min(640px,100vw - 40px);max-height:calc(100vh - 112px);box-shadow:var(--wb-shadow-dialog-md);border-radius:24px;flex-direction:column;animation:.2s forwards _developer-profile-card-in_385qb_1;display:flex;position:relative;overflow:hidden}
|
||||
:where(.yc-capability-center) ._window_385qb_2{background:var(--wb-bg-primary);flex-direction:column;width:100vw;height:100vh;display:flex;overflow:hidden}
|
||||
:where(.yc-capability-center) ._window_385qb_2 ._body_385qb_56{padding-top:0}
|
||||
:where(.yc-capability-center) ._windowChrome_385qb_60{-webkit-app-region:drag;flex:none;align-items:center;height:44px;padding-left:10px;display:flex}
|
||||
:where(.yc-capability-center) ._windowChromeTrailing_385qb_70{justify-content:flex-end;padding-left:0;padding-right:8px}
|
||||
:where(.yc-capability-center) ._windowCloseButton_385qb_75{width:24px;height:24px;color:var(--wb-color-text-secondary);cursor:pointer;-webkit-app-region:no-drag;background:0 0;border:none;border-radius:6px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}
|
||||
:where(.yc-capability-center) ._windowCloseButton_385qb_75:hover{background:var(--wb-bg-hover);color:var(--wb-color-text-primary)}
|
||||
:where(.yc-capability-center) ._windowCloseButton_385qb_75:active{background:var(--wb-bg-active)}
|
||||
:where(.yc-capability-center) ._closeButton_385qb_98{z-index:1;width:28px;height:28px;color:var(--wb-color-text-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;justify-content:center;align-items:center;display:flex;position:absolute;top:16px;right:16px}
|
||||
:where(.yc-capability-center) ._closeButton_385qb_98:hover{background:var(--wb-bg-hover);color:var(--wb-color-text-primary)}
|
||||
:where(.yc-capability-center) ._bodyShell_385qb_119{flex-direction:column;flex:1;min-height:0;display:flex;position:relative}
|
||||
:where(.yc-capability-center) ._body_385qb_56{scrollbar-width:thin;scrollbar-color:var(--wb-border-default) transparent;flex-direction:column;flex:1;min-height:0;padding:24px;display:flex;overflow-y:auto}
|
||||
:where(.yc-capability-center) ._body_385qb_56::-webkit-scrollbar{width:8px}
|
||||
:where(.yc-capability-center) ._body_385qb_56::-webkit-scrollbar-track{background:0 0}
|
||||
:where(.yc-capability-center) ._body_385qb_56::-webkit-scrollbar-thumb{background:var(--wb-border-default);background-clip:content-box;border:2px solid #0000;border-radius:12px}
|
||||
:where(.yc-capability-center) ._bottomFade_385qb_154{pointer-events:none;background:linear-gradient(180deg, transparent, var(--wb-bg-primary));height:60px;position:absolute;bottom:0;left:0;right:0}
|
||||
:where(.yc-capability-center) ._header_385qb_163{flex-direction:column;align-items:center;gap:12px;padding:0 4px;display:flex}
|
||||
:where(.yc-capability-center) ._avatar_385qb_171{object-fit:cover;background:var(--wb-bg-secondary);border-radius:24px;width:72px;height:72px}
|
||||
:where(.yc-capability-center) ._avatarFallback_385qb_178{background:var(--wb-bg-secondary);width:72px;height:72px;color:var(--wb-color-text-secondary);border-radius:24px;justify-content:center;align-items:center;font-size:24px;font-weight:600;display:flex}
|
||||
:where(.yc-capability-center) ._nameRow_385qb_190{align-items:center;gap:6px;max-width:100%;display:flex}
|
||||
:where(.yc-capability-center) ._name_385qb_190{color:var(--wb-color-text-primary);text-overflow:ellipsis;white-space:nowrap;margin:0;font-size:20px;font-weight:600;line-height:28px;overflow:hidden}
|
||||
:where(.yc-capability-center) ._certBadgePersonal_385qb_206{color:var(--wb-color-text-warning-default);flex:none;align-items:center;display:inline-flex}
|
||||
:where(.yc-capability-center) ._certBadgeEnterprise_385qb_212{color:var(--wb-color-text-brand-default);flex:none;align-items:center;display:inline-flex}
|
||||
:where(.yc-capability-center) ._certTooltip_385qb_220{--wb-tooltip-bg:var(--wb-bg-primary);--wb-tooltip-border-radius:12px;--wb-tooltip-padding-TB:8px;--wb-tooltip-padding-LR:12px;border:.5px solid var(--wb-color-border-solid);box-shadow:var(--wb-shadow-popup-medium)}
|
||||
:where(.yc-capability-center) ._certCard_385qb_228{flex-direction:column;gap:2px;display:flex}
|
||||
:where(.yc-capability-center) ._certTypePersonal_385qb_233{color:var(--wb-color-text-warning-default);font-size:12px;font-weight:500;line-height:20px}
|
||||
:where(.yc-capability-center) ._certTypeEnterprise_385qb_239{color:var(--wb-color-text-brand-default);font-size:12px;font-weight:500;line-height:20px}
|
||||
:where(.yc-capability-center) ._certName_385qb_245{color:var(--wb-color-text-primary);font-size:12px;line-height:20px}
|
||||
:where(.yc-capability-center) ._about_385qb_250{background:var(--wb-bg-secondary);border-radius:16px;margin-top:32px;padding:16px 20px}
|
||||
:where(.yc-capability-center) ._aboutTitle_385qb_256{color:var(--wb-color-text-primary);margin:0;font-size:16px;font-weight:600;line-height:24px}
|
||||
:where(.yc-capability-center) ._aboutBio_385qb_263{color:var(--wb-color-text-primary);white-space:pre-wrap;word-break:break-word;margin:12px 0 0;font-size:14px;line-height:22px}
|
||||
:where(.yc-capability-center) ._contacts_385qb_271{flex-wrap:wrap;align-items:center;gap:8px 12px;margin-top:8px;display:flex}
|
||||
:where(.yc-capability-center) ._contact_385qb_271{max-width:160px;height:24px;color:var(--wb-color-text-secondary);text-align:left;cursor:pointer;background:0 0;border:none;border-radius:6px;flex:none;align-items:center;gap:4px;padding:0;font-size:12px;line-height:20px;display:inline-flex}
|
||||
:where(.yc-capability-center) ._contactExpanded_385qb_296{background:var(--wb-bg-surface-overlay-soft);color:var(--wb-color-text-primary);padding:0 8px}
|
||||
:where(.yc-capability-center) ._contactIcon_385qb_301{flex:none;align-items:center;display:inline-flex}
|
||||
:where(.yc-capability-center) ._contactText_385qb_306{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}
|
||||
:where(.yc-capability-center) ._tabs_385qb_311{align-items:center;gap:24px;margin-top:32px;padding:0 4px;display:flex}
|
||||
:where(.yc-capability-center) ._tab_385qb_311{color:var(--wb-color-text-tertiary);cursor:pointer;background:0 0;border:none;align-items:center;gap:4px;padding:0;font-size:16px;font-weight:500;line-height:24px;display:inline-flex}
|
||||
:where(.yc-capability-center) ._tab_385qb_311:hover{color:var(--wb-color-text-primary)}
|
||||
:where(.yc-capability-center) ._tabActive_385qb_334{color:var(--wb-color-text-primary);font-weight:600}
|
||||
:where(.yc-capability-center) ._tabCount_385qb_339{font-weight:400}
|
||||
:where(.yc-capability-center) ._grid_385qb_342{grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;margin-top:16px;display:grid}
|
||||
:where(.yc-capability-center) ._emptyState_385qb_348,:where(.yc-capability-center) ._errorState_385qb_349{text-align:center;color:var(--wb-color-text-tertiary);margin-top:16px;padding:32px 0;font-size:13px;line-height:22px}
|
||||
:where(.yc-capability-center) ._assetCard_385qb_359{border:.5px solid var(--wb-border-card);background:var(--wb-bg-card);min-height:90px;box-shadow:0 4px 12px color-mix(in srgb, var(--wb-palette-black-100) 3%, transparent);text-align:left;cursor:pointer;border-radius:16px;flex-direction:column;gap:12px;padding:20px;transition:background .15s;display:flex}
|
||||
:where(.yc-capability-center) ._assetCard_385qb_359:hover{background:var(--wb-bg-card-hover)}
|
||||
:where(.yc-capability-center) ._assetCardExpert_385qb_359{min-height:126px}
|
||||
:where(.yc-capability-center) ._assetHead_385qb_382{align-items:center;gap:12px;display:flex}
|
||||
:where(.yc-capability-center) ._assetIcon_385qb_387{object-fit:cover;background:var(--wb-bg-secondary);border-radius:16px;flex:none;width:48px;height:48px}
|
||||
:where(.yc-capability-center) ._assetIconFrame_385qb_399{border:.5px solid var(--wb-border-card);border-radius:16px;flex:none;width:48px;height:48px;display:flex;overflow:hidden}
|
||||
:where(.yc-capability-center) ._assetHeadText_385qb_408{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
|
||||
:where(.yc-capability-center) ._assetName_385qb_415{color:var(--wb-color-text-primary);text-overflow:ellipsis;white-space:nowrap;font-size:16px;font-weight:500;line-height:24px;overflow:hidden}
|
||||
:where(.yc-capability-center) ._assetDescription_385qb_425{color:var(--wb-color-text-secondary);text-overflow:ellipsis;white-space:nowrap;font-size:14px;line-height:22px;overflow:hidden}
|
||||
:where(.yc-capability-center) ._assetDescriptionQuiet_385qb_434{color:var(--wb-color-text-tertiary)}
|
||||
:where(.yc-capability-center) ._assetTags_385qb_438{flex-wrap:nowrap;gap:4px;height:24px;display:flex;overflow:hidden;mask-image:linear-gradient(90deg,#000 90%,#0000)}
|
||||
:where(.yc-capability-center) ._assetTag_385qb_438{background:var(--wb-bg-secondary);color:var(--wb-color-text-tertiary);white-space:nowrap;border-radius:4px;flex:none;padding:2px 8px;font-size:14px;line-height:20px}
|
||||
:where(.yc-capability-center) ._overlay_p0cpn_1{background:var(--wb-modal-overlay-mask-color);backdrop-filter:blur(var(--wb-modal-overlay-mask-blur));justify-content:center;align-items:center;padding:20px;animation:.15s _overlay-in_p0cpn_1;display:flex;position:fixed;inset:0}
|
||||
@keyframes _overlay-in_p0cpn_1{0%{opacity:0}to{opacity:1}}
|
||||
:where(.yc-capability-center) ._card_p0cpn_20{background:var(--wb-bg-primary);border-radius:var(--wb-radius-4xl);opacity:0;flex-direction:column;animation:.2s forwards _modal-in_p0cpn_1;display:flex;position:relative;overflow:hidden}
|
||||
@keyframes _modal-in_p0cpn_1{0%{opacity:0;transform:scale(.95)translateY(8px)}to{opacity:1;transform:scale(1)translateY(0)}}
|
||||
:where(.yc-capability-center) ._expertView_p0cpn_40{width:min(calc(var(--wb-spacing-12) * 10), calc(100vw - var(--wb-spacing-9)));max-width:none;height:min(calc(var(--wb-spacing-12) * 11 + var(--wb-spacing-5)), calc(100vh - var(--wb-spacing-12)));max-height:min(calc(var(--wb-spacing-12) * 11 + var(--wb-spacing-5)), calc(100vh - var(--wb-spacing-12)));border:.5px solid var(--wb-color-border-solid);box-shadow:var(--wb-shadow-dialog);transition:opacity var(--wb-motion-duration-fast) var(--wb-motion-easing-standard), height var(--wb-motion-duration-sm) var(--wb-motion-easing-standard)}
|
||||
:where(.yc-capability-center) ._caseView_p0cpn_49{width:min(calc(var(--wb-spacing-12) * 10), calc(100vw - var(--wb-spacing-9)));max-width:none;height:min(calc(var(--wb-spacing-12) * 10), calc(100vh - var(--wb-spacing-12)));max-height:min(calc(var(--wb-spacing-12) * 10), calc(100vh - var(--wb-spacing-12)));box-shadow:var(--wb-shadow-dialog-md);transition:opacity var(--wb-motion-duration-fast) var(--wb-motion-easing-standard), height var(--wb-motion-duration-sm) var(--wb-motion-easing-standard)}
|
||||
:where(.yc-capability-center) ._caseView_p0cpn_49:has(.dc-detail-page.is-enlarged){width:min(calc(var(--wb-spacing-12) * 15), calc(100vw - var(--wb-spacing-8)));height:min(calc(var(--wb-spacing-12) * 11 + var(--wb-spacing-5)), calc(100vh - var(--wb-spacing-12) - var(--wb-spacing-5)));max-height:calc(100vh - var(--wb-spacing-12) - var(--wb-spacing-5))}
|
||||
:where(.yc-capability-center) ._caseView_p0cpn_49 .dc-detail-secondary-btn{height:var(--wb-spacing-8);padding:0 var(--wb-spacing-5);border:calc(var(--wb-spacing-1) / 2) solid var(--wb-border-default);border-radius:var(--wb-radius-lg);background:var(--wb-bg-primary);color:var(--wb-color-text-primary);font-size:var(--wb-font-body-size);line-height:var(--wb-font-code-line-height);cursor:pointer;white-space:nowrap;transition:background var(--wb-motion-duration-sm) var(--wb-motion-easing-standard), color var(--wb-motion-duration-sm) var(--wb-motion-easing-standard);justify-content:center;align-items:center;font-family:inherit;font-weight:400;display:inline-flex}
|
||||
:where(.yc-capability-center) ._caseView_p0cpn_49 .dc-detail-secondary-btn:hover:not(:disabled){background:var(--wb-bg-hover)}
|
||||
:where(.yc-capability-center) ._caseView_p0cpn_49 .dc-detail-page.is-enlarged .dc-detail-secondary-btn{pointer-events:auto;outline:calc(var(--wb-spacing-1) / 2) solid color-mix(in srgb, var(--wb-text-on-inverse) 20%, transparent);outline-offset:0;color:var(--wb-text-on-inverse);background:color-mix(in srgb, var(--wb-bg-inverse) 80%, transparent);border:none}
|
||||
:where(.yc-capability-center) ._caseView_p0cpn_49 .dc-detail-page.is-enlarged .dc-detail-secondary-btn:hover:not(:disabled){opacity:1;background:color-mix(in srgb, var(--wb-bg-inverse) 95%, transparent)}
|
||||
:where(.yc-capability-center) .expert-center-dark ._expertView_p0cpn_40{border-color:var(--wb-color-border-solid)}
|
||||
/* Explicit offline-service adapters; source cards and their body/CSS remain recovered above. */
|
||||
.yc-capability-center .capability-expert-body{flex:1;min-height:0;display:flex;flex-direction:column}
|
||||
.yc-capability-center .capability-market-panel{display:contents}
|
||||
.yc-capability-center .capability-expert-empty{flex:1;min-height:280px;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:12px}
|
||||
.yc-capability-center .capability-service-note{font-size:13px;line-height:20px;color:var(--wb-color-text-tertiary);margin:0}
|
||||
.yc-capability-center .capability-catalog-status{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:8px 0 16px;flex-wrap:wrap}
|
||||
.yc-capability-center .ec-my-created-empty-title{margin:0}
|
||||
.capability-detail-host{position:fixed;inset:0;z-index:1000}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/* REA ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94; source modules _1mrhu_ and _s54xa_ only. */
|
||||
/* contexts-BkjwYSZT.css sha256=50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380 */
|
||||
.yc-skill-picker ._panel_1mrhu_14{background:var(--cb-dropdown-bg-color);border:.5px solid var(--cb-popover-border);box-shadow:var(--cb-shadow-popover);box-sizing:border-box;border-radius:16px;flex-direction:column;gap:0;padding:8px;display:flex}
|
||||
.yc-skill-picker ._panel_1mrhu_14>:not(:first-child):not(._divider_1mrhu_25){margin-top:8px}
|
||||
.yc-skill-picker ._panel_1mrhu_14>._divider_1mrhu_25,.yc-skill-picker ._panel_1mrhu_14>._list_1mrhu_31+._divider_1mrhu_25,.yc-skill-picker ._panel_1mrhu_14>._empty_1mrhu_32+._divider_1mrhu_25,.yc-skill-picker ._panel_1mrhu_14>._divider_1mrhu_25+*{margin-top:0}
|
||||
.yc-skill-picker ._panel_1mrhu_14{user-select:none;z-index:40;overflow:hidden;outline:none!important}
|
||||
.yc-skill-picker ._panel_1mrhu_14:focus,.yc-skill-picker ._panel_1mrhu_14:focus-visible{outline:none!important}
|
||||
.yc-skill-picker ._body_1mrhu_49{flex-direction:column;flex:auto;min-height:0;display:flex}
|
||||
.yc-skill-picker ._search_1mrhu_59{background:var(--cb-search-input-bg);box-sizing:border-box;border-radius:8px;flex:0 0 32px;align-items:center;gap:8px;height:32px;padding:0 8px;display:flex}
|
||||
.yc-skill-picker ._searchIcon_1mrhu_71{width:16px;height:16px;color:var(--cb-text-secondary);flex:0 0 16px;justify-content:center;align-items:center;display:inline-flex}
|
||||
.yc-skill-picker ._searchInput_1mrhu_81{min-width:0;color:var(--cb-text-primary);caret-color:var(--cb-green-color);background:0 0;border:none;outline:none;flex:auto;padding:0;font-family:PingFang SC,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:14px;line-height:22px}
|
||||
.yc-skill-picker ._searchInput_1mrhu_81::placeholder{color:var(--cb-text-muted)}
|
||||
.yc-skill-picker ._searchClear_1mrhu_98{cursor:pointer;width:16px;height:16px;color:var(--cb-text-muted);background:0 0;border:none;flex:0 0 16px;justify-content:center;align-items:center;padding:0;display:inline-flex}
|
||||
.yc-skill-picker ._searchClear_1mrhu_98:hover{color:var(--cb-text-tertiary)}
|
||||
.yc-skill-picker ._list_1mrhu_31{scrollbar-width:thin;scrollbar-color:transparent transparent;flex-direction:column;flex:0 auto;gap:2px;min-height:0;display:flex;overflow-y:auto}
|
||||
.yc-skill-picker ._list_1mrhu_31::-webkit-scrollbar{width:4px}
|
||||
.yc-skill-picker ._list_1mrhu_31::-webkit-scrollbar-thumb{background:0 0;border-radius:12px;transition:background-color .2s}
|
||||
.yc-skill-picker ._list_1mrhu_31::-webkit-scrollbar-track{background:0 0}
|
||||
.yc-skill-picker ._panel_1mrhu_14:hover ._list_1mrhu_31{scrollbar-color:var(--cb-scrollbar-thumb) transparent}
|
||||
.yc-skill-picker ._panel_1mrhu_14:hover ._list_1mrhu_31::-webkit-scrollbar-thumb{background:var(--cb-scrollbar-thumb)}
|
||||
.yc-skill-picker ._panel_1mrhu_14:hover ._list_1mrhu_31::-webkit-scrollbar-thumb:hover{background:var(--cb-scrollbar-thumb-hover)}
|
||||
.yc-skill-picker ._itemRow_1mrhu_150{cursor:pointer;box-sizing:border-box;min-height:32px;color:var(--cb-text-primary);border-radius:8px;align-items:center;gap:8px;padding:4px 8px;font-family:PingFang SC,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:14px;line-height:22px;display:flex}
|
||||
.yc-skill-picker ._itemRow_1mrhu_150:hover{background:var(--cb-dropdown-item-hover-bg-color)}
|
||||
.yc-skill-picker ._itemRowSelected_1mrhu_168{background:var(--cb-list-item-selected-bg)}
|
||||
.yc-skill-picker ._itemRowSelected_1mrhu_168:hover{background:var(--cb-dropdown-item-hover-bg-color)}
|
||||
.yc-skill-picker ._itemIcon_1mrhu_175{width:16px;height:16px;color:var(--cb-text-secondary);flex:0 0 16px;justify-content:center;align-items:center;display:inline-flex}
|
||||
.yc-skill-picker ._itemLabel_1mrhu_185{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}
|
||||
.yc-skill-picker ._divider_1mrhu_25{background:var(--cb-popover-divider);flex:0 0 1px;height:1px}
|
||||
.yc-skill-picker ._empty_1mrhu_32{height:32px;color:var(--cb-text-muted);justify-content:center;align-items:center;padding:0 8px;font-family:PingFang SC,sans-serif;font-size:14px;line-height:22px;display:flex}
|
||||
.yc-skill-picker ._highlight_1mrhu_214{color:var(--cb-green-color);font-weight:inherit;background:0 0;padding:0}
|
||||
.yc-skill-picker ._actionGroup_1mrhu_224{flex-direction:column;flex:none;gap:2px;display:flex}
|
||||
.yc-skill-picker ._actionRow_1mrhu_231{width:100%;min-height:32px;color:var(--cb-text-primary);text-align:left;cursor:pointer;box-sizing:border-box;background:0 0;border:none;border-radius:8px;align-items:center;gap:8px;padding:4px 8px;font-family:PingFang SC,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:14px;line-height:22px;display:flex}
|
||||
.yc-skill-picker ._actionRow_1mrhu_231:hover:not(:disabled){background:var(--cb-dropdown-item-hover-bg-color)}
|
||||
.yc-skill-picker ._actionRow_1mrhu_231:disabled{cursor:not-allowed;opacity:.5}
|
||||
.yc-skill-picker ._actionIcon_1mrhu_257{width:16px;height:16px;color:var(--cb-text-secondary);flex:0 0 16px;justify-content:center;align-items:center;display:inline-flex}
|
||||
.yc-skill-picker ._actionIcon_1mrhu_257 svg{width:100%;height:100%}
|
||||
.yc-skill-picker ._actionLabel_1mrhu_271{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}
|
||||
.yc-skill-picker ._actionSuffix_1mrhu_279{color:var(--cb-text-tertiary);flex-shrink:0;align-items:center;gap:4px;font-size:12px;line-height:20px;display:inline-flex}
|
||||
.yc-skill-picker ._actionSuffixChevron_1mrhu_289{width:14px;height:14px;color:var(--cb-text-muted);justify-content:center;align-items:center;display:inline-flex}
|
||||
/* wb-input-add-CDNucW11.css sha256=07566131dac43d1854af1ecb973bf9736a63723a0786ba0d2d5287ed6f45f2f6 */
|
||||
.yc-skill-picker ._item_s54xa_16{cursor:pointer;box-sizing:border-box;border-radius:8px;align-items:flex-start;gap:8px;height:52px;padding:4px 8px;display:flex}
|
||||
.yc-skill-picker ._item_s54xa_16:hover,.yc-skill-picker ._itemBusy_s54xa_30{background:var(--cb-dropdown-item-hover-bg-color)}
|
||||
.yc-skill-picker ._itemDisabled_s54xa_34{cursor:default}
|
||||
.yc-skill-picker ._itemIcon_s54xa_49{flex:0 0 16px;justify-content:center;align-items:center;width:16px;height:16px;margin-top:3px;display:inline-flex}
|
||||
.yc-skill-picker ._itemIcon_s54xa_49>*{width:16px;height:16px}
|
||||
.yc-skill-picker ._itemBody_s54xa_63{flex-direction:column;flex:auto;justify-content:center;gap:0;min-width:0;display:flex}
|
||||
.yc-skill-picker ._itemTitle_s54xa_72{color:var(--cb-text-primary);white-space:nowrap;align-items:center;gap:6px;font-family:PingFang SC,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:14px;line-height:22px;display:flex;overflow:hidden}
|
||||
.yc-skill-picker ._itemTitleText_s54xa_86{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}
|
||||
.yc-skill-picker ._sourceTag_s54xa_93,.yc-skill-picker ._syncStatus_s54xa_94{background:color-mix(in srgb, currentColor 10%, transparent);color:var(--wb-color-text-tertiary);border-radius:4px;flex-shrink:0;padding:0 4px;font-family:PingFang SC,sans-serif;font-size:11px;line-height:16px}
|
||||
.yc-skill-picker ._secondaryAction_s54xa_105{color:var(--cb-text-link);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:4px;flex:none;align-self:center;padding:2px 4px}
|
||||
.yc-skill-picker ._secondaryAction_s54xa_105:disabled{color:var(--cb-text-muted);cursor:not-allowed}
|
||||
.yc-skill-picker ._secondaryActionIcon_s54xa_122{width:16px;height:16px;color:var(--cb-text-muted);background:0 0;border-radius:4px;justify-content:center;align-items:center;margin-left:4px;padding:0;display:inline-flex;position:relative}
|
||||
.yc-skill-picker ._secondaryActionIcon_s54xa_122:hover:not(:disabled){color:var(--cb-text-primary);background:0 0}
|
||||
.yc-skill-picker ._secondaryActionIcon_s54xa_122:disabled{color:var(--cb-text-muted);cursor:not-allowed}
|
||||
.yc-skill-picker ._secondaryActionIcon_s54xa_122>svg{width:16px;height:16px}
|
||||
.yc-skill-picker ._secondaryActionDot_s54xa_149{border:1px solid var(--cb-dropdown-bg-color);box-sizing:border-box;background:#f80;border-radius:50%;width:6px;height:6px;position:absolute;top:-1px;right:-1px}
|
||||
.yc-skill-picker ._cloudBadge_s54xa_162{width:16px;height:16px;color:var(--cb-text-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex}
|
||||
.yc-skill-picker ._cloudBadge_s54xa_162>svg{width:16px;height:16px}
|
||||
.yc-skill-picker ._hoverHint_s54xa_177{width:16px;height:16px;color:var(--cb-text-muted);flex:0 0 16px;justify-content:center;align-self:center;align-items:center;margin-left:4px;display:none}
|
||||
.yc-skill-picker ._item_s54xa_16:hover ._hoverHint_s54xa_177{display:inline-flex}
|
||||
.yc-skill-picker ._hoverHint_s54xa_177>svg{width:16px;height:16px}
|
||||
.yc-skill-picker ._itemDesc_s54xa_196{color:var(--cb-text-tertiary);text-overflow:ellipsis;white-space:nowrap;font-family:PingFang SC,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:12px;line-height:20px;overflow:hidden}
|
||||
.yc-skill-picker ._pinBtn_s54xa_219{cursor:pointer;width:16px;height:16px;color:var(--cb-text-muted);background:0 0;border:none;flex:0 0 16px;justify-content:center;align-self:center;align-items:center;padding:0;transition:color .15s;display:none}
|
||||
.yc-skill-picker ._item_s54xa_16:hover ._pinBtn_s54xa_219{display:inline-flex}
|
||||
.yc-skill-picker ._pinBtn_s54xa_219._pinned_s54xa_217{color:var(--cb-text-primary);display:inline-flex}
|
||||
.yc-skill-picker ._pinBtn_s54xa_219:hover{color:var(--cb-text-primary)}
|
||||
.yc-skill-picker ._pinIconRest_s54xa_216,.yc-skill-picker ._pinIconHover_s54xa_216{justify-content:center;align-items:center;width:16px;height:16px;display:inline-flex}
|
||||
.yc-skill-picker ._pinIconRest_s54xa_216>svg,.yc-skill-picker ._pinIconHover_s54xa_216>svg{width:16px;height:16px}
|
||||
.yc-skill-picker ._pinIconHover_s54xa_216,.yc-skill-picker ._item_s54xa_16:hover ._pinBtn_s54xa_219._pinned_s54xa_217 ._pinIconRest_s54xa_216{display:none}
|
||||
.yc-skill-picker ._item_s54xa_16:hover ._pinBtn_s54xa_219._pinned_s54xa_217 ._pinIconHover_s54xa_216{display:inline-flex}
|
||||
@@ -0,0 +1,20 @@
|
||||
/* Host adapters only; source declarations live in skills-source.css. */
|
||||
.yc-skills-page{min-height:0;flex:1}
|
||||
.yc-skills-scroll{min-height:0;overflow:auto;flex:1;padding:0 24px 24px;scrollbar-width:thin;scrollbar-color:var(--wb-border-default) transparent}
|
||||
.yc-skill-add{position:relative;display:inline-flex;flex:none}
|
||||
.yc-skill-add-menu{position:absolute;right:0;top:calc(100% + 4px);z-index:110;min-width:140px}
|
||||
.yc-skill-add-menu button{width:100%;text-align:left;font:inherit;border:0;background:transparent}
|
||||
.yc-skill-avatar{display:inline-flex;align-items:center;justify-content:center;flex:none;background:var(--wb-bg-secondary);color:var(--wb-color-text-secondary);font-size:20px;font-weight:600}
|
||||
.yc-skill-error{color:var(--wb-error-foreground,#d54941);font-size:13px;line-height:1.5;overflow-wrap:anywhere}
|
||||
.yc-skill-error button{margin-left:12px;color:inherit;background:none;border:0;text-decoration:underline;cursor:pointer}
|
||||
.yc-skill-notice{position:absolute;z-index:120;top:8px;right:24px;margin:0;padding:8px 12px;border:1px solid var(--wb-border-subtle);border-radius:8px;background:var(--wb-bg-popover);box-shadow:var(--wb-shadow-dialog-md);font-size:13px;color:var(--wb-color-text-secondary);pointer-events:none}
|
||||
.yc-skill-market-unavailable{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:320px;gap:12px;color:var(--wb-color-text-tertiary);font-size:14px}
|
||||
.yc-skill-market-unavailable p{margin:0}
|
||||
.yc-skill-card-keyboard{min-width:0;outline-offset:3px;border-radius:16px}
|
||||
.yc-skill-card-keyboard>.sm-card{height:100%}
|
||||
.yc-skills-page .sm-card-menu-item{background:none;border:0;width:100%;font:inherit;text-align:left}
|
||||
.yc-skills-page .sm-card-menu-item:disabled{opacity:.4;cursor:not-allowed}
|
||||
.yc-skill-overwrite{display:flex;gap:8px;align-items:center;font-size:13px;color:var(--wb-color-text-secondary)}
|
||||
.yc-skill-local-note{font-size:12px;line-height:1.5;color:var(--wb-color-text-tertiary);margin:0}
|
||||
.yc-skill-files{display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--wb-color-text-secondary)}
|
||||
.yc-skills-page .wb-button__content{display:inline-flex;align-items:center;justify-content:center;gap:4px}
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"version": "5.8.2",
|
||||
"rootSha256": "035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918",
|
||||
"evidence": [
|
||||
"ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb",
|
||||
"ev_e8faf60b3d22b99ede302f2d0efdfb3145f53f59789d4a345f5892dc13a589a4",
|
||||
"ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"name": "skills-market-Cwf-NvXV.js",
|
||||
"sha256": "f1434cadabf5ed3762f00fc2a356bc7418b0b4aab27db62dc9fbbe5add6f6dfb"
|
||||
},
|
||||
{
|
||||
"name": "standalone-skill-detail-modal-Bi5X8CZ4.js",
|
||||
"sha256": "ee7de2a7123f94110aa01258d3d6b3fcc3ce48f2ec5fd8f91cb3367c817a0f01"
|
||||
},
|
||||
{
|
||||
"name": "skills-market-d_hn_qWB.css",
|
||||
"sha256": "935d55effde68d03b2fbc2529ec94b19a6cd7210d9bc79febd6d115b115f60b6"
|
||||
},
|
||||
{
|
||||
"name": "standalone-skill-detail-modal-fe_BFVdd.css",
|
||||
"sha256": "7967df52bacb7594f934b32d8d5fb24854dbc8c47d0217d4677c7d7da0655f49"
|
||||
},
|
||||
{
|
||||
"name": "src-Bh3YZ35H.css",
|
||||
"sha256": "88dbdc1cd3f59d0e8b75824512a465a2be714bc7af481d790fe3a9484f85e01a"
|
||||
},
|
||||
{
|
||||
"name": "contexts-BkjwYSZT.css",
|
||||
"sha256": "50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380"
|
||||
}
|
||||
],
|
||||
"bodies": [
|
||||
{
|
||||
"name": "CardMoreIcon",
|
||||
"start": 12409,
|
||||
"end": 12739,
|
||||
"sha256": "ccbeac0b692db2d34fb5cf6bac12ab9c547de287df406b6163fb68865bdfda69"
|
||||
},
|
||||
{
|
||||
"name": "PlusIcon",
|
||||
"start": 12739,
|
||||
"end": 13103,
|
||||
"sha256": "b1435baab2b5a9c31788c1c12326d970232a6f53f8f029b04cb3153cfbc35ae6"
|
||||
},
|
||||
{
|
||||
"name": "TrashIcon",
|
||||
"start": 24350,
|
||||
"end": 25480,
|
||||
"sha256": "523985d97c28d1ac4ac40dd596fff4ff312b0945bb98fe23e9ecd5e165030453"
|
||||
},
|
||||
{
|
||||
"name": "FolderIcon",
|
||||
"start": 31026,
|
||||
"end": 31415,
|
||||
"sha256": "c15cffc9816c9cbb71298d0431415731e0379b6003da45412654779ab8471ace"
|
||||
},
|
||||
{
|
||||
"name": "CloseIcon",
|
||||
"start": 31415,
|
||||
"end": 31785,
|
||||
"sha256": "88f98478a7a8ad8c2cf4672f604a94a39df5abc10180bfabd7d063707df9352d"
|
||||
},
|
||||
{
|
||||
"name": "UseSkillIcon",
|
||||
"start": 31785,
|
||||
"end": 33672,
|
||||
"sha256": "9f675cac3549753e1f15d740d8ebf5993ad22ff83eeabda87e5de861c1b7a557"
|
||||
},
|
||||
{
|
||||
"name": "DocIcon",
|
||||
"start": 43671,
|
||||
"end": 46403,
|
||||
"sha256": "1506d7ecfb41449a9f1c8348bb2e71814b5afbf105ce421bd434355a80f48b6e"
|
||||
},
|
||||
{
|
||||
"name": "BasicInfoIcon",
|
||||
"start": 46403,
|
||||
"end": 48030,
|
||||
"sha256": "d8e07a9daebdc4519f3b07a4b48e94f6614c2910de64f2623ec6f26036c2b90a"
|
||||
},
|
||||
{
|
||||
"name": "CardFrame",
|
||||
"start": 10076,
|
||||
"end": 13702,
|
||||
"sha256": "035eae9801741016cea11571c4bca6f7fc6053718ed4f1afb2197ce8b09dcdf7"
|
||||
}
|
||||
],
|
||||
"adaptations": [
|
||||
"React singleton and JSX runtime are local.",
|
||||
"CardFrame source body is unchanged; local record data, avatar fallback, tooltip and checkbox are explicit host adapters.",
|
||||
"CSS declarations are unchanged with a capability-center selector scope; skill-market topbar ancestry is adapted to the shared portal header."
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"file": "src/renderer/src/capabilities/skills-source.mjs",
|
||||
"sha256": "d39f3b58a4b287b6cbde4ce39ce227305f83fe7c36707267449bcacf81c268ba",
|
||||
"bytes": 13070
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/capabilities/skills-source.d.mts",
|
||||
"sha256": "3877a709cbd65bb8125d75b6f0517a18364dc2cf787c3784b798344e1e5cf93a",
|
||||
"bytes": 744
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/capabilities/skills-source.css",
|
||||
"sha256": "f55bc94b2eb0493dbafa848acb715007ee8774a12f874055be95e4708f2ea03c",
|
||||
"bytes": 157794
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/* Pinned REA declarations. Generated by scripts/test-skills-reference.mjs --write. */
|
||||
:where(.yc-capability-center) .wb-button{box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;transition:background-color var(--wb-button-transition-duration) var(--wb-button-transition-easing), color var(--wb-button-transition-duration) var(--wb-button-transition-easing), border-color var(--wb-button-transition-duration) var(--wb-button-transition-easing), transform var(--wb-button-transition-duration) var(--wb-button-transition-easing);border-radius:var(--wb-button-border-radius-medium);border:none;outline:none;justify-content:center;align-items:center;font-family:PingFang SC,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-weight:500;text-decoration:none;display:inline-flex}:where(.yc-capability-center) .wb-button--medium{height:var(--wb-button-height-medium);padding:0 var(--wb-button-padding-medium);font-size:var(--wb-button-font-size-medium);gap:var(--wb-button-gap-medium);min-width:var(--wb-button-min-width-medium);line-height:var(--wb-button-line-height-medium)}:where(.yc-capability-center) .wb-button--xsmall{height:var(--wb-button-height-xsmall);font-size:var(--wb-button-font-size-small);line-height:var(--wb-button-height-xsmall);border-radius:var(--wb-button-border-radius-small);gap:0;padding:0}:where(.yc-capability-center) .wb-button--xsmall.wb-button--icon-only{width:var(--wb-button-height-xsmall);height:var(--wb-button-height-xsmall)}:where(.yc-capability-center) .wb-button--small{height:var(--wb-button-height-small);padding:0 var(--wb-button-padding-small);font-size:var(--wb-button-font-size-small);gap:var(--wb-button-gap-small);min-width:var(--wb-button-min-width-small);border-radius:var(--wb-button-border-radius-small);line-height:var(--wb-button-line-height-small)}:where(.yc-capability-center) .wb-button--small .wb-button__icon{width:var(--wb-button-icon-size-small);height:var(--wb-button-icon-size-small)}:where(.yc-capability-center) .wb-button--large{height:var(--wb-button-height-large);padding:0 var(--wb-button-padding-large);font-size:var(--wb-button-font-size-large);gap:var(--wb-button-gap-large);border-radius:var(--wb-button-border-radius-large);line-height:var(--wb-button-line-height-large)}:where(.yc-capability-center) .wb-button--primary{background-color:var(--wb-button-primary-bg);color:var(--wb-button-primary-fg)}:where(.yc-capability-center) .wb-button--primary:hover:not(:disabled){background-color:var(--wb-button-primary-bg-hover)}:where(.yc-capability-center) .wb-button--primary:active:not(:disabled){background-color:var(--wb-button-primary-bg-active)}:where(.yc-capability-center) .wb-button--primary:disabled,:where(.yc-capability-center) .wb-button--primary.wb-button--disabled{background-color:var(--wb-button-primary-bg-disabled);color:var(--wb-button-primary-fg-disabled);cursor:not-allowed}:where(.yc-capability-center) .wb-button--secondary{background-color:var(--wb-button-secondary-bg);color:var(--wb-button-secondary-fg);border:1px solid var(--wb-button-secondary-border)}:where(.yc-capability-center) .wb-button--secondary:hover:not(:disabled){background-color:var(--wb-button-secondary-bg-hover)}:where(.yc-capability-center) .wb-button--secondary:active:not(:disabled){background-color:var(--wb-button-secondary-bg-active)}:where(.yc-capability-center) .wb-button--secondary:disabled,:where(.yc-capability-center) .wb-button--secondary.wb-button--disabled{color:var(--wb-button-secondary-fg-disabled);border-color:var(--wb-button-secondary-border-disabled);cursor:not-allowed}:where(.yc-capability-center) .wb-button--grey{background-color:var(--wb-button-grey-bg);color:var(--wb-button-grey-fg)}:where(.yc-capability-center) .wb-button--grey:hover:not(:disabled){background-color:var(--wb-button-grey-bg-hover)}:where(.yc-capability-center) .wb-button--grey:active:not(:disabled){background-color:var(--wb-button-grey-bg-active)}:where(.yc-capability-center) .wb-button--grey:disabled,:where(.yc-capability-center) .wb-button--grey.wb-button--disabled{background-color:var(--wb-button-grey-bg-disabled);color:var(--wb-button-grey-fg-disabled);cursor:not-allowed}:where(.yc-capability-center) .wb-button--ghost{background-color:var(--wb-button-ghost-bg);color:var(--wb-button-ghost-fg);font-weight:400}:where(.yc-capability-center) .wb-button--ghost:hover:not(:disabled){background-color:var(--wb-button-ghost-bg-hover);color:var(--wb-button-ghost-fg-hover)}:where(.yc-capability-center) .wb-button--ghost:active:not(:disabled){background-color:var(--wb-button-ghost-bg-active);color:var(--wb-button-ghost-fg-hover)}:where(.yc-capability-center) .wb-button--ghost:disabled,:where(.yc-capability-center) .wb-button--ghost.wb-button--disabled{background-color:var(--wb-button-ghost-bg);color:var(--wb-button-ghost-fg-disabled);cursor:not-allowed}:where(.yc-capability-center) .wb-button--link{background-color:var(--wb-button-link-bg);color:var(--wb-button-link-fg);min-width:0;height:auto;padding:0;font-weight:400}:where(.yc-capability-center) .wb-button--link:hover:not(:Line truncated
|
||||
|
||||
:where(.yc-capability-center) .skill-market{background:var(--wb-home-bg-secondary);flex-direction:column;height:100%;display:flex;position:relative;overflow:hidden}@media (width<=600px){:where(.yc-capability-center) .skill-market{padding:0 16px}}:where(.yc-capability-center) .skill-market .skill-market__content{scrollbar-gutter:stable;scrollbar-width:thin;scrollbar-color:var(--wb-palette-black-10,#0000001a) transparent;flex-direction:column;flex:1;min-height:0;padding:0 24px 24px;display:flex;overflow:hidden auto}:where(.yc-capability-center) .skill-market .skill-market__content::-webkit-scrollbar{width:4px}:where(.yc-capability-center) .skill-market .skill-market__content::-webkit-scrollbar-track{background:0 0}:where(.yc-capability-center) .skill-market .skill-market__content::-webkit-scrollbar-thumb{background:var(--wb-palette-black-10,#0000001a);border-radius:12px}:where(.yc-capability-center) body.vscode-dark .skill-market .skill-market__content,:where(.yc-capability-center) body[data-vscode-theme-kind=vscode-dark] .skill-market .skill-market__content{scrollbar-color:var(--wb-palette-white-10,#ffffff1a) transparent}:where(.yc-capability-center) body.vscode-dark .skill-market .skill-market__content::-webkit-scrollbar-thumb{background:var(--wb-palette-white-10,#ffffff1a)}:where(.yc-capability-center) body[data-vscode-theme-kind=vscode-dark] .skill-market .skill-market__content::-webkit-scrollbar-thumb{background:var(--wb-palette-white-10,#ffffff1a)}:where(.yc-capability-center) .sm-back-to-top{z-index:10;border:1px solid var(--wb-border-default);background:var(--wb-bg-primary);width:32px;height:32px;color:var(--wb-color-text-secondary);cursor:pointer;opacity:.85;border-radius:50%;justify-content:center;align-items:center;transition:all .2s;display:flex;position:absolute;bottom:16px;right:16px;box-shadow:0 2px 8px #0000001f}:where(.yc-capability-center) .sm-back-to-top:hover{opacity:1;color:var(--wb-color-text-primary);border-color:var(--wb-color-text-secondary);box-shadow:0 4px 12px #0000002e}:where(.yc-capability-center) .sm-back-to-top:active{transform:scale(.92)}:where(.yc-capability-center) .sm-batch-update-modal-mask{background:var(--wb-modal-overlay-mask-color);backdrop-filter:blur(var(--wb-modal-overlay-mask-blur));z-index:9999;justify-content:center;align-items:center;animation:.15s sm-batch-update-modal-fade-in;display:flex;position:fixed;inset:0}:where(.yc-capability-center) .sm-batch-update-modal{background:var(--wb-bg-primary);border:1px solid var(--wb-border-subtle);width:420px;max-width:calc(100vw - 32px);max-height:calc(100vh - 64px);box-shadow:0 20px 60px color-mix(in srgb, var(--wb-palette-black-100) 20%, transparent);border-radius:16px;flex-direction:column;gap:16px;padding:20px 24px 24px;animation:.18s sm-batch-update-modal-slide-in;display:flex}:where(.yc-capability-center) .sm-batch-update-modal-header{justify-content:space-between;align-items:center;display:flex}:where(.yc-capability-center) .sm-batch-update-modal-header span{color:var(--wb-color-text-primary);font-size:15px;font-weight:600}:where(.yc-capability-center) .sm-batch-update-modal-close{cursor:pointer;width:28px;height:28px;color:var(--wb-color-text-secondary);background:0 0;border:none;border-radius:8px;justify-content:center;align-items:center;transition:all .15s;display:inline-flex}:where(.yc-capability-center) .sm-batch-update-modal-close:hover{background:var(--wb-bg-hover);color:var(--wb-color-text-primary)}:where(.yc-capability-center) .sm-batch-update-modal-close:disabled{opacity:.5;cursor:not-allowed}:where(.yc-capability-center) .sm-batch-update-modal-content{flex-direction:column;gap:12px;display:flex;overflow:hidden}:where(.yc-capability-center) .sm-batch-update-description{color:var(--wb-color-text-secondary);margin:0;font-size:13px;line-height:1.5}:where(.yc-capability-center) .sm-batch-update-list{border:1px solid var(--wb-border-subtle);background:color-mix(in srgb, var(--wb-border-subtle) 10%, transparent);border-radius:8px;max-height:179px;margin:0;padding:0;list-style:none;overflow-y:auto}:where(.yc-capability-center) .sm-batch-update-item{border-bottom:1px solid var(--wb-border-subtle);justify-content:space-between;align-items:center;padding:10px 12px;display:flex}:where(.yc-capability-center) .sm-batch-update-item:last-child{border-bottom:none}:where(.yc-capability-center) .sm-batch-update-item-name{color:var(--wb-color-text-primary);text-overflow:ellipsis;white-space:nowrap;flex:1;font-size:13px;font-weight:500;overflow:hidden}:where(.yc-capability-center) .sm-batch-update-item-version{color:var(--wb-color-text-tertiary);flex-shrink:0;margin-left:12px;font-size:12px}:where(.yc-capability-center) .sm-batch-update-modal-footer{--cb-button-primary:var(--wb-button-secondary-bg);--cb-button-primary-foreground:var(--wb-button-secondary-fg);--cb-input-button-background:var(--wb-button-primary-bg);--cb-input-button-foreground:var(--wb-button-primary-fg);justify-content:flex-end;gap:8px;margin-top:4px;displaLine truncated
|
||||
|
||||
@keyframes sm-skill-detail-overlay-in{0%{opacity:0}to{opacity:1}}@keyframes sm-skill-detail-card-in{0%{opacity:0;transform:scale(.95)translateY(8px)}to{opacity:1;transform:scale(1)translateY(0)}}@keyframes sm-skill-detail-skeleton-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}:where(.yc-capability-center) .sm-skill-detail-overlay{z-index:1000;background:var(--wb-modal-overlay-mask-color);backdrop-filter:blur(var(--wb-modal-overlay-mask-blur));justify-content:center;align-items:center;padding:20px;animation:.15s sm-skill-detail-overlay-in;display:flex;position:fixed;inset:0}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-card{background:var(--wb-bg-primary);width:520px;max-width:90vw;max-height:85vh;box-shadow:var(--wb-shadow-dialog-md);border-radius:16px;flex-direction:column;animation:.2s forwards sm-skill-detail-card-in;display:flex;position:relative;overflow:hidden}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-card.is-skill-view{border-radius:24px;flex-direction:column;width:min(640px,100vw - 40px);max-width:min(640px,100vw - 40px);height:auto;max-height:calc(100vh - 112px);display:flex;overflow:hidden}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-card.is-skill-view .dc-case-preview-body{min-height:calc(var(--wb-spacing-12) * 6)}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-card.is-skill-view.is-case-view{min-height:min(560px,100vh - 112px)}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-card.is-skill-view.is-case-view .dc-case-preview{padding-bottom:16px}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-card.is-skill-view.is-case-view .dc-case-preview-body{min-height:calc(var(--wb-spacing-12) * 2)}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-card.is-skill-view.is-case-view .dc-case-preview-footer{margin-top:12px}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-card.is-skill-view .sm-skill-detail-view{flex:1;min-height:0;overflow:hidden}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-view{flex-direction:column;height:100%;display:flex;overflow:hidden}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-header{z-index:2;flex-direction:row;flex-shrink:0;align-items:flex-start;gap:16px;padding:24px 24px 0;display:flex;position:relative}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-icon-wrap{border:1px solid var(--wb-border-card-static);border-radius:16px;flex-shrink:0;justify-content:center;align-items:center;width:68px;height:68px;display:flex;overflow:hidden}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-icon-wrap>img,:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-icon-wrap>span{width:100%;height:100%;border-radius:16px!important}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-header-body{flex-direction:column;flex:1;gap:8px;min-width:0;display:flex}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-title-row{justify-content:space-between;align-items:center;gap:12px;display:flex}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-title-row h2{min-width:0;color:var(--wb-color-text-primary);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:20px;font-weight:600;line-height:1.35;overflow:hidden}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-secondary{flex-shrink:0;align-items:center;gap:12px;display:flex}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-close{box-sizing:border-box;border-radius:var(--wb-radius-md);width:24px;height:24px;color:var(--wb-color-text-tertiary);cursor:pointer;background:0 0;border:none;flex-shrink:0;justify-content:center;align-items:center;padding:0;transition:background .15s,color .15s;display:flex;position:static}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-close svg{width:12px;height:12px;color:inherit;flex-shrink:0}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-close:hover{background:var(--wb-bg-hover);color:var(--wb-color-text-primary)}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-toggle{flex-shrink:0;align-self:center}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-action-row{flex-wrap:wrap;align-items:center;gap:8px;min-height:32px;display:flex}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-action-row:empty{display:none}:where(.yc-capability-center) .sm-skill-detail-overlay .sm-skill-detail-btn-skeleton{box-sizing:border-box;background:linear-gradient(90deg, color-mix(in srgb, var(--wb-border-subtle) 50%, transparent) 25%, var(--wb-bg-hover) 50%, color-mix(in srgb, var(--wb-border-subtle) 50%, transparent) 75%);background-size:200% 100%;border-radius:8px;flex-shrink:0;width:96px;height:Line truncated
|
||||
|
||||
:where(.yc-capability-center) .skill-upload-modal-mask{background:var(--wb-modal-overlay-mask-color);backdrop-filter:blur(var(--wb-modal-overlay-mask-blur));z-index:10002;justify-content:center;align-items:center;animation:.15s skill-modal-fade-in;display:flex;position:fixed;inset:0}@keyframes skill-modal-fade-in{0%{opacity:0}to{opacity:1}}:where(.yc-capability-center) .skill-upload-modal{background:var(--wb-bg-popover);border:1px solid var(--wb-border-subtle);border-radius:16px;flex-direction:column;gap:16px;width:460px;max-width:calc(100vw - 32px);padding:20px 24px 24px;animation:.18s skill-modal-slide-in;display:flex;box-shadow:0 20px 60px #0003}@keyframes skill-modal-slide-in{0%{opacity:0;transform:translateY(8px)scale(.98)}to{opacity:1;transform:translateY(0)scale(1)}}:where(.yc-capability-center) .skill-upload-modal-header{justify-content:space-between;align-items:center;display:flex}:where(.yc-capability-center) .skill-upload-modal-header span{color:var(--wb-color-text-primary);font-size:15px;font-weight:600}:where(.yc-capability-center) .skill-upload-modal-close{cursor:pointer;width:28px;height:28px;color:var(--wb-color-text-secondary);background:0 0;border:none;border-radius:8px;justify-content:center;align-items:center;transition:all .15s;display:inline-flex}:where(.yc-capability-center) .skill-upload-modal-close:hover{background:var(--wb-bg-hover);color:var(--wb-color-text-primary)}:where(.yc-capability-center) .skill-upload-drop-zone{border:1.5px dashed var(--wb-border-subtle);text-align:center;background:color-mix(in srgb, var(--wb-border-subtle) 15%, transparent);cursor:pointer;border-radius:10px;flex-direction:column;align-items:center;gap:12px;padding:32px 24px;transition:border-color .2s,background .2s;display:flex;position:relative;overflow:hidden}:where(.yc-capability-center) .skill-upload-drop-zone:hover{border-color:var(--wb-color-text-secondary);background:color-mix(in srgb, var(--wb-border-subtle) 22%, transparent)}:where(.yc-capability-center) .skill-upload-drop-zone--dragging{border-color:var(--wb-color-text-brand-default);background:color-mix(in srgb, var(--wb-color-text-brand-default) 8%, transparent);border-style:solid}:where(.yc-capability-center) .skill-upload-drop-zone--dragging .skill-upload-drop-title{color:var(--wb-color-text-brand-default)}:where(.yc-capability-center) .skill-upload-drop-zone--disabled{cursor:not-allowed;pointer-events:none}:where(.yc-capability-center) .skill-upload-overlay{z-index:2;background:var(--wb-bg-popover);border-radius:10px;flex-direction:column;justify-content:center;align-items:center;gap:12px;animation:.2s skill-modal-fade-in;display:flex;position:absolute;inset:0}:where(.yc-capability-center) .skill-upload-overlay-icon{width:28px;height:28px;font-size:28px}:where(.yc-capability-center) .skill-upload-overlay-spinner{border:2.5px solid color-mix(in srgb, var(--wb-color-text-secondary) 40%, transparent);border-top-color:var(--wb-color-text-brand-default);border-radius:50%;width:28px;height:28px;animation:.8s linear infinite skill-upload-spin}:where(.yc-capability-center) .skill-upload-overlay-text{color:var(--wb-color-text-primary);font-size:13px;font-weight:500}:where(.yc-capability-center) .skill-upload-overlay-desc{max-width:280px;color:var(--wb-color-text-secondary);text-align:center;font-size:12px;line-height:1.6}@keyframes skill-upload-spin{to{transform:rotate(360deg)}}:where(.yc-capability-center) .skill-upload-drop-icon{color:var(--wb-color-text-secondary);opacity:.7;justify-content:center;align-items:center;display:flex}:where(.yc-capability-center) .skill-upload-drop-title{color:var(--wb-color-text-primary);font-size:14px}:where(.yc-capability-center) .skill-upload-requirements{padding:0 2px}:where(.yc-capability-center) .skill-upload-requirements-title{color:var(--wb-color-text-primary);margin-bottom:8px;font-size:13px;font-weight:500}:where(.yc-capability-center) .skill-upload-requirements-list{flex-direction:column;gap:6px;margin:0;padding-left:18px;display:flex}:where(.yc-capability-center) .skill-upload-requirements-list li,:where(.yc-capability-center) .skill-upload-requirements-list-item{color:var(--wb-color-text-secondary);font-size:13px;line-height:1.5}:where(.yc-capability-center) .skill-upload-pick-zip-link{color:var(--wb-color-text-link,var(--wb-color-brand-primary));cursor:pointer;text-underline-offset:2px;background:0 0;border:0;margin-top:10px;padding:0;font-size:13px;text-decoration:underline}:where(.yc-capability-center) .skill-upload-pick-zip-link:hover{opacity:.85}:where(.yc-capability-center) .skill-upload-type-tags{flex-wrap:wrap;gap:8px;padding:0 2px;display:flex}:where(.yc-capability-center) .skill-upload-type-tag{background:var(--wb-bg-tertiary);color:var(--wb-color-text-secondary);border-radius:4px;align-items:center;gap:4px;padding:3px 10px;font-size:12px;display:inline-flex}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import type {ComponentType,ReactNode} from 'react';
|
||||
export const CardFrame:ComponentType<{title:string;description:string;installed?:boolean;installedListMode?:boolean;disabled?:boolean;batchMode?:boolean;selected?:boolean;onToggleSelect?():void;onOpen?():void;actions?:ReactNode;className?:string}>;
|
||||
export const CardMoreIcon:ComponentType;
|
||||
export const PlusIcon:ComponentType;
|
||||
export const TrashIcon:ComponentType;
|
||||
export const FolderIcon:ComponentType;
|
||||
export const CloseIcon:ComponentType;
|
||||
export const UseSkillIcon:ComponentType;
|
||||
export const DocIcon:ComponentType;
|
||||
export const BasicInfoIcon:ComponentType;
|
||||
export function setSkillPrimitives(value:{Avatar:ComponentType<any>;Tooltip:ComponentType<any>;Checkbox:ComponentType<any>}):void;
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as React from 'react';import * as JSX from 'react/jsx-runtime';
|
||||
function CardMoreIcon(){return JSX.jsxs(`svg`,{width:`14`,height:`14`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[ JSX.jsx(`circle`,{cx:`5`,cy:`12`,r:`1`}), JSX.jsx(`circle`,{cx:`12`,cy:`12`,r:`1`}), JSX.jsx(`circle`,{cx:`19`,cy:`12`,r:`1`})]})}
|
||||
function PlusIcon(){return JSX.jsx(`svg`,{width:`12`,height:`12`,viewBox:`0 0 12 12`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,"aria-hidden":`true`,children: JSX.jsx(`path`,{fill:`currentColor`,fillRule:`evenodd`,transform:`matrix(1 0 0 1 1.125 1.12207)`,d:`M5.6 5.6L5.6 10L4.4 10L4.4 5.6L0 5.6L0 4.4L4.4 4.4L4.4 0L5.6 0L5.6 4.4L10 4.4L10 5.6L5.6 5.6Z`})})}
|
||||
function TrashIcon(){return JSX.jsx(`svg`,{width:`14`,height:`14`,viewBox:`0 0 14 14`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children: JSX.jsx(`path`,{fill:`currentColor`,transform:`matrix(1 0 0 1 0.875 0.875)`,d:`M9.5375 2.1L12.25 2.1L12.25 3.15L8.75 3.15L8.75 3.15L2.275 3.15L2.275 7Q2.275 8.7468 2.417 9.152Q2.8208 10.3042 3.973 10.708Q4.3782 10.85 6.125 10.85Q7.8718 10.85 8.277 10.708Q9.4292 10.3042 9.833 9.152Q9.975 8.7468 9.975 7L9.975 4.2L11.025 4.2L11.025 7Q11.025 8.9254 10.8239 9.4993Q10.2531 11.1281 8.6243 11.6989Q8.0504 11.9 6.125 11.9Q4.1996 11.9 3.6257 11.6989Q1.9969 11.1281 1.4261 9.4993Q1.225 8.9254 1.225 7L1.225 3.15L0 3.15L0 2.1L2.7125 2.1L2.7798 1.9384Q3.0762 1.227 3.2128 0.9788Q3.4591 0.5314 3.7932 0.3086Q4.1274 0.0858 4.6351 0.0306Q4.9168 -0 5.6875 0L6.5625 0Q7.3332 -0 7.6149 0.0306Q8.1226 0.0858 8.4568 0.3086Q8.7909 0.5314 9.0371 0.9788Q9.1738 1.2271 9.4702 1.9384L9.5375 2.1ZM3.8522 2.1Q4.202 1.298 4.3757 1.1823Q4.5741 1.05 5.6875 1.05L6.5625 1.05Q7.6759 1.05 7.8743 1.1823Q8.048 1.298 8.3978 2.1L3.8522 2.1ZM6.65 4.375L5.6 4.375L5.6 9.625L6.65 9.625L6.65 4.375Z`,fillRule:`evenodd`})})}
|
||||
function FolderIcon(){return JSX.jsx(`svg`,{width:`14`,height:`14`,viewBox:`0 0 16 16`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children: JSX.jsx(`path`,{d:`M1.5 4a1 1 0 0 1 1-1h3.086a1 1 0 0 1 .707.293l1.414 1.414A1 1 0 0 0 8.414 5H13.5a1 1 0 0 1 1 1v6.5a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1V4Z`,stroke:`currentColor`,strokeWidth:`1.2`,strokeLinecap:`round`,strokeLinejoin:`round`})})}
|
||||
function CloseIcon(){return JSX.jsx(`svg`,{width:`16`,height:`16`,viewBox:`0 0 10.8698 10.8698`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children: JSX.jsx(`path`,{fill:`currentColor`,fillRule:`evenodd`,d:`M6.3046 5.4349L10.8698 10L10 10.8698L5.4349 6.3046L0.8698 10.8698L0 10L4.5651 5.4349L0 0.8698L0.8698 0L5.4349 4.5651L10 0L10.8698 0.8698L6.3046 5.4349Z`})})}
|
||||
function UseSkillIcon(){return JSX.jsx(`svg`,{width:`14`,height:`14`,viewBox:`0 0 14.4531 14.6507`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children: JSX.jsx(`path`,{fill:`currentColor`,transform:`matrix(1 0 0 1 0 0.00116893)`,d:`M7.2266 0.0001C7.2614 0.0001 7.297 0.0001 7.3333 0.0001C8.0268 -0.0004 8.9863 -0.0011 9.9521 0.17C10.9684 0.3502 12.061 0.7315 12.8926 1.5636C14.2855 2.9577 14.4531 4.8385 14.4531 6.2804C14.4531 7.7222 14.2853 9.6021 12.8926 10.9962C12.094 11.7954 11.0936 12.1824 10.1406 12.3721C9.1916 12.5611 8.2453 12.5626 7.5312 12.5606C7.2983 12.56 6.9785 12.7013 6.5684 13.0948C6.17 13.4771 5.7651 14.0227 5.3662 14.6495L4.3535 14.005C4.7714 13.3484 5.2367 12.709 5.7373 12.2286C6.2261 11.7597 6.8348 11.3587 7.5342 11.3604C8.2533 11.3624 9.0891 11.3581 9.9062 11.1954C10.7192 11.0335 11.4695 10.7233 12.0439 10.1485C13.073 9.1185 13.2539 7.6783 13.2539 6.2804C13.2539 4.8825 13.0729 3.4423 12.0439 2.4122C11.4564 1.8243 10.6378 1.5103 9.7432 1.3517C8.8809 1.1988 8.0118 1.1991 7.3052 1.1993L7.2266 1.1993L7.1478 1.1993C6.4412 1.1991 5.5722 1.1988 4.71 1.3517C3.8153 1.5103 2.9967 1.8242 2.4092 2.4122C1.3801 3.4423 1.1992 4.8825 1.1992 6.2804C1.1993 7.6782 1.3802 9.1185 2.4092 10.1485C3.0494 10.7892 3.9083 11.1001 4.8252 11.2442L4.7324 11.837L4.6387 12.4298C3.5897 12.2648 2.45 11.8863 1.5605 10.9962C0.1678 9.6021 0 7.7222 0 6.2804C0 4.8385 0.1676 2.9577 1.5605 1.5636C2.392 0.7316 3.4838 0.3502 4.5 0.17C5.4656 -0.0012 6.4253 -0.0005 7.1189 0.0001C7.1555 0.0001 7.1914 0.0001 7.2266 0.0001ZM10.5038 7.978L8.4321 7.1484Q8.3898 7.1315 8.3087 7.0998Q8.0047 6.981 7.8793 6.9046Q7.4651 6.6523 7.4653 6.2195Q7.4655 5.7867 7.8799 5.5347Q8.0053 5.4584 8.3094 5.3399Q8.3906 5.3083 8.4328 5.2914L10.5042 4.4638L10.9494 5.5782L9.3423 6.2203L10.9499 6.864L10.5038 7.978ZM5.2028 7.7196L5.2028 4.7196L4.0028 4.7196L4.0028 7.7196L5.2028 7.7196Z`,fillRule:`evenodd`})})}
|
||||
function DocIcon(){return JSX.jsx(`svg`,{width:`16`,height:`16`,viewBox:`0 0 16 16`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children: JSX.jsx(`path`,{fill:`currentColor`,transform:`matrix(1 0 0 1 1.90039 0.900304)`,d:`M6.0762 0C7.1535 0 7.8413 -0.0088 8.4726 0.2227C8.6425 0.285 8.8081 0.3596 8.9668 0.4463C9.557 0.769 10.0034 1.2927 10.7129 2.1035L11.2959 2.7705C11.5114 3.0168 11.7073 3.2385 11.8398 3.4278C11.9634 3.6043 12.13 3.8839 12.0898 4.2276C12.0632 4.4548 11.967 4.6683 11.8135 4.838C11.5814 5.0944 11.2611 5.1526 11.0469 5.1758C10.8173 5.2007 10.5214 5.1993 10.1943 5.1992C9.6946 5.1993 9.2753 5.2006 8.9365 5.1671C8.5881 5.1325 8.2597 5.057 7.9609 4.8643C7.7102 4.7025 7.4968 4.4891 7.335 4.2383C7.1423 3.9396 7.0667 3.6112 7.0322 3.2627C6.9987 2.9239 7 2.5047 7 2.0049L7 1.209C6.7531 1.2031 6.4529 1.1993 6.0762 1.1992C4.901 1.1993 4.0688 1.201 3.4287 1.2725C2.8006 1.3427 2.4308 1.4742 2.1504 1.6866C1.9753 1.8192 1.8192 1.9753 1.6865 2.1505C1.4741 2.4309 1.3426 2.8007 1.2725 3.4288C1.201 4.0688 1.1992 4.901 1.1992 6.0762L1.1992 8.1153C1.1992 9.2941 1.2006 10.1285 1.2725 10.7706C1.3431 11.4006 1.4757 11.7718 1.6895 12.0528C1.8204 12.2249 1.9743 12.3788 2.1465 12.5098C2.4275 12.7236 2.7987 12.8562 3.4287 12.9268C4.0708 12.9987 4.9051 13 6.084 13C7.2629 13 8.0972 12.9987 8.7392 12.9268C9.3694 12.8562 9.7405 12.7236 10.0215 12.5098C10.1936 12.3788 10.3475 12.2249 10.4785 12.0528C10.6922 11.7718 10.8249 11.4006 10.8955 10.7706C10.9674 10.1285 10.9678 9.2941 10.9678 8.1153L10.9678 6.0996L12.168 6.0996L12.168 8.1153C12.168 9.2665 12.169 10.18 12.0879 10.9043C12.0054 11.6406 11.8321 12.2554 11.4336 12.7793C11.2371 13.0376 11.0063 13.2684 10.748 13.4649C10.2241 13.8635 9.6094 14.0367 8.873 14.1192C8.1486 14.2003 7.2353 14.1993 6.084 14.1993C4.9327 14.1993 4.0193 14.2003 3.2949 14.1192C2.5586 14.0367 1.9439 13.8635 1.4199 13.4649C1.1617 13.2684 0.9309 13.0376 0.7344 12.7793C0.3358 12.2554 0.1626 11.6406 0.0801 10.9043C-0.0011 10.18 0 9.2665 0 8.1153L0 6.0762C0 4.9288 -0.0015 4.0181 0.0791 3.2959C0.1611 2.562 0.3334 1.9488 0.7295 1.4258C0.9285 1.1631 1.1631 0.9286 1.4258 0.7296C1.9488 0.3335 2.5619 0.1611 3.2959 0.0791C4.018 -0.0015 4.9287 0 6.0762 0ZM8.5996 10.6993L3.5996 10.6993L3.5996 9.5L8.5996 9.5L8.5996 10.6993ZM8.5996 7.6992L3.5996 7.6992L3.5996 6.5L8.5996 6.5L8.5996 7.6992ZM8.1992 2.0049C8.1992 2.5289 8.2002 2.8786 8.2266 3.1446C8.2519 3.401 8.2969 3.5154 8.3437 3.588C8.413 3.6951 8.5042 3.7863 8.6113 3.8555C8.6839 3.9023 8.7982 3.9473 9.0547 3.9727C9.3207 3.999 9.6703 4 10.1943 4C10.4404 4 10.62 3.9959 10.7568 3.9903C10.6709 3.8836 10.5556 3.7458 10.3935 3.5606L9.8096 2.8936C9.0424 2.0168 8.7501 1.6966 8.3906 1.5C8.3281 1.4659 8.2644 1.4339 8.1992 1.4053L8.1992 2.0049Z`})})}
|
||||
function BasicInfoIcon(){return JSX.jsx(`svg`,{width:`16`,height:`16`,viewBox:`0 0 16 16`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,"aria-hidden":`true`,children: JSX.jsx(`path`,{fill:`currentColor`,transform:`matrix(1 0 0 1 1.15001 1.14999)`,d:`M3.1581 0.5419Q2.6163 0 1.85 0Q1.0837 0 0.5419 0.5419Q0 1.0837 0 1.85Q0 2.6163 0.5419 3.1581Q1.0837 3.7 1.85 3.7Q2.6163 3.7 3.1581 3.1581Q3.7 2.6163 3.7 1.85Q3.7 1.0837 3.1581 0.5419ZM1.3904 1.3904Q1.5808 1.2 1.85 1.2Q2.1192 1.2 2.3096 1.3904Q2.5 1.5808 2.5 1.85Q2.5 2.1192 2.3096 2.3096Q2.1192 2.5 1.85 2.5Q1.5808 2.5 1.3904 2.3096Q1.2 2.1192 1.2 1.85Q1.2 1.5808 1.3904 1.3904ZM5.35 1.2485L13.85 1.2485L13.85 2.4485L5.35 2.4485L5.35 1.2485ZM1.85 5Q2.6163 5 3.1581 5.5419Q3.7 6.0837 3.7 6.85Q3.7 7.6163 3.1581 8.1581Q2.6163 8.7 1.85 8.7Q1.0837 8.7 0.5419 8.1581Q0 7.6163 0 6.85Q0 6.0837 0.5419 5.5419Q1.0837 5 1.85 5ZM1.85 6.2Q1.5808 6.2 1.3904 6.3904Q1.2 6.5808 1.2 6.85Q1.2 7.1192 1.3904 7.3096Q1.5808 7.5 1.85 7.5Q2.1192 7.5 2.3096 7.3096Q2.5 7.1192 2.5 6.85Q2.5 6.5808 2.3096 6.3904Q2.1192 6.2 1.85 6.2ZM13.85 6.2485L5.35 6.2485L5.35 7.4485L13.85 7.4485L13.85 6.2485ZM3.1581 10.5419Q2.6163 10 1.85 10Q1.0837 10 0.5419 10.5419Q0 11.0837 0 11.85Q0 12.6163 0.5419 13.1581Q1.0837 13.7 1.85 13.7Q2.6163 13.7 3.1581 13.1581Q3.7 12.6163 3.7 11.85Q3.7 11.0837 3.1581 10.5419ZM1.3904 11.3904Q1.5808 11.2 1.85 11.2Q2.1192 11.2 2.3096 11.3904Q2.5 11.5808 2.5 11.85Q2.5 12.1192 2.3096 12.3096Q2.1192 12.5 1.85 12.5Q1.5808 12.5 1.3904 12.3096Q1.2 12.1192 1.2 11.85Q1.2 11.5808 1.3904 11.3904ZM5.35 11.2485L8.85 11.2485L8.85 12.4485L5.35 12.4485L5.35 11.2485Z`,fillRule:`evenodd`})})}
|
||||
const V=()=>key=>key,We=value=>typeof value==='string'?value:'',G=JSX;let HostAvatar,HostTooltip,HostCheckbox;export function setSkillPrimitives({Avatar,Tooltip,Checkbox}){HostAvatar=Avatar;HostTooltip=Tooltip;HostCheckbox=Checkbox}const Ye=props=>JSX.jsx(HostAvatar,props),E=props=>JSX.jsx(HostTooltip,props),CardBatchCheckbox=props=>JSX.jsx(HostCheckbox,props);const Rr=()=>null,Er=()=>null,mr=()=>null;const renderSkillCardStatIcon=()=>null,Ze=value=>String(value),ki={};
|
||||
function CardFrame(e){let{title:t,description:n,iconUrl:r,stats:i,sourceLabel:a,badgeText:o,builtinBadgeText:s,paidBadgeText:c,expired:l=!1,reasonLabel:u,needsApiKey:d=!1,cloudAvailable:f=!1,riskResult:p,updateStatus:m=`none`,className:h,installed:g=!1,installedListMode:_=!1,disabled:v=!1,batchMode:y=!1,selected:b=!1,onToggleSelect:x,onUpdate:S,onOpen:C,actions:w,isFirst:T=!1}=e,D=V(),O=We(t),k=We(n),A=We(a),ee=We(o),te=We(s),j=We(c),M=!!i&&i.length>0,ne=m===`available`||m===`updating`,re=m===`updating`,getRiskTooltipContent=e=>{let t=e.tags[0]?.desc||e.fileAnalysis[0]?.description||D(`skills.security.riskTooltip.default`);return e.riskLevel===`high`?D(`skills.security.riskTooltip.high`,{desc:t}):e.riskLevel===`medium`?D(`skills.security.riskTooltip.medium`,{desc:t}):D(`skills.security.riskTooltip.low`,{desc:t})},handleUpdate=e=>{e.stopPropagation(),re||S?.()},ie=y?x:C,N=[`sm-card`,g&&`sm-card--installed`,_&&`sm-card--installed-list-mode`,ne&&`sm-card--has-update`,v&&`sm-card--disabled`,y&&`sm-card--batch-mode`,b&&`sm-card--selected`,o&&`sm-card--plugin`,l&&`sm-card--expired`,h].filter(Boolean).join(` `);return JSX.jsxs(`div`,{className:N,"data-market-perf":T?`first-item`:void 0,"data-track-id":`skill_card_click`,"data-track-name":`\u70B9\u51FB\u6280\u80FD\u5361\u7247`,"data-track-props":JSON.stringify({source:O}),role:y?`checkbox`:void 0,"aria-checked":y?b:void 0,onClick:ie,children:[u&& JSX.jsx(`div`,{className:`sm-card-reason`,children:u}), JSX.jsxs(`div`,{className:`sm-card-content`,children:[y&& JSX.jsx(CardBatchCheckbox,{selected:b}), JSX.jsx(`div`,{className:`sm-card-icon`,children: JSX.jsx(Ye,{name:O,iconUrl:r,size:48,borderRadius:16})}), JSX.jsx(`div`,{className:`sm-card-top`,children: JSX.jsxs(`div`,{className:`sm-card-body`,children:[ JSX.jsxs(`div`,{className:`sm-card-name-row`,children:[ JSX.jsx(E,{content:O,onlyWhenTruncated:!0,placement:`top`,maxWidth:250,children: JSX.jsx(`span`,{className:`sm-card-name`,children:O})}),A&& JSX.jsx(`span`,{className:`sm-card-source`,children:A}),ee&& JSX.jsx(`span`,{className:`sm-card-badge sm-card-badge--plugin`,children:ee}),te&& JSX.jsx(`span`,{className:`sm-card-badge sm-card-badge--builtin`,children:te}),p&& JSX.jsx(E,{content:getRiskTooltipContent(p),placement:`top`,maxWidth:280,children: JSX.jsx(`span`,{className:`sm-risk-badge sm-risk-badge--${p.riskLevel}`,children: JSX.jsx(Rr,{className:`sm-risk-badge__icon`})})}),d&& JSX.jsx(E,{content:D(`skills.needsApiKey`),placement:`top`,children: JSX.jsx(`span`,{className:`sm-card-name-status-icon sm-card-name-status-icon--api-key`,children: JSX.jsx(Er,{})})}),ne&& JSX.jsx(`span`,{className:`sm-update-icon${re?` sm-update-icon--updating`:``}`,onClick:handleUpdate,role:`button`,tabIndex:0,children: JSX.jsx(`span`,{className:`sm-update-icon__label`,children:D(re?`skills.skillhub.updating`:`skills.skillhub.updatable`)})})]}), JSX.jsxs(`div`,{className:`sm-card-desc`,children:[j&& JSX.jsx(`span`,{className:l?`sm-card-expired-badge`:`sm-card-paid-badge`,children:j}),k||`-`]}),f&& JSX.jsx(`div`,{className:`sm-card-tags`,children: JSX.jsx(E,{content:D(`skills.tag.cloudAvailableTooltip`),placement:`top`,children: JSX.jsxs(`span`,{className:`sm-card-cloud-tag`,children:[ JSX.jsx(mr,{}), JSX.jsx(`span`,{className:`sm-card-cloud-tag__label`,children:D(`skills.tag.cloudAvailable`)})]})})})]})}),w]}),M&&i? JSX.jsx(`div`,{className:`sm-card-stats`,children:i.map(e=> JSX.jsx(E,{content:D(ki[e.id],{count:String(e.value)}),children: JSX.jsxs(`span`,{className:`sm-card-stat`,children:[renderSkillCardStatIcon(e.icon),Ze(e.value)]})},e.id))}):null]})}
|
||||
export {CardFrame,CardMoreIcon,PlusIcon,TrashIcon,FolderIcon,CloseIcon,UseSkillIcon,DocIcon,BasicInfoIcon};
|
||||
@@ -0,0 +1,126 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { McpServerView } from '@shared/capabilities'
|
||||
|
||||
interface Scope {
|
||||
owner: string
|
||||
active: boolean
|
||||
request: number
|
||||
items: McpServerView[]
|
||||
loading: boolean
|
||||
error: string
|
||||
pendingId?: string
|
||||
}
|
||||
interface Flight { scope: Scope; kind: string; promise: Promise<void> }
|
||||
export interface ComposerConnectors {
|
||||
items: McpServerView[]
|
||||
loading: boolean
|
||||
error: string
|
||||
pendingId?: string
|
||||
refresh(): void
|
||||
change(id: string, enabled?: boolean): Promise<void>
|
||||
cancel(id: string): Promise<void>
|
||||
}
|
||||
const message = (reason: unknown): string => reason instanceof Error ? reason.message : String(reason || '连接器操作失败')
|
||||
|
||||
/** MCP configuration is global; owner only bounds the lifetime of this menu's UI. */
|
||||
export function useComposerConnectors(owner: string): ComposerConnectors {
|
||||
const scope = useMemo<Scope>(() => ({ owner, active: false, request: 0, items: [], loading: true, error: '' }), [owner])
|
||||
const latest = useRef(scope)
|
||||
latest.current = scope
|
||||
const flights = useRef(new Map<string, Flight>())
|
||||
const [view, setView] = useState(() => ({ scope, value: { ...scope } }))
|
||||
const current = useCallback(() => scope.active && latest.current === scope, [scope])
|
||||
const publish = useCallback(() => { if (current()) setView({ scope, value: { ...scope } }) }, [scope, current])
|
||||
const pending = useCallback(() => {
|
||||
scope.pendingId = [...flights.current].find(([, flight]) => flight.scope === scope)?.[0]
|
||||
publish()
|
||||
}, [scope, publish])
|
||||
|
||||
const list = useCallback(async (clearError: boolean): Promise<McpServerView[] | undefined> => {
|
||||
if (!current()) return undefined
|
||||
const request = ++scope.request
|
||||
scope.loading = true
|
||||
if (clearError) scope.error = ''
|
||||
publish()
|
||||
try {
|
||||
const items = await window.ycode.mcp.list()
|
||||
if (current() && request === scope.request) { scope.items = items; publish() }
|
||||
return items
|
||||
} catch (reason) {
|
||||
if (current() && request === scope.request) {
|
||||
// Keep the primary operation error if its final state refresh also fails.
|
||||
scope.error ||= message(reason)
|
||||
publish()
|
||||
}
|
||||
throw reason
|
||||
} finally {
|
||||
if (current() && request === scope.request) { scope.loading = false; publish() }
|
||||
}
|
||||
}, [scope, current, publish])
|
||||
const refresh = useCallback((): void => { void list(true).catch(() => undefined) }, [list])
|
||||
useEffect(() => {
|
||||
scope.active = true
|
||||
refresh()
|
||||
return () => { scope.active = false; ++scope.request }
|
||||
}, [scope, refresh])
|
||||
|
||||
const run = useCallback((id: string, kind: string, operation: () => Promise<unknown>, inspectError: boolean): Promise<void> => {
|
||||
if (!current()) return Promise.reject(new Error('连接器菜单已切换,请重试'))
|
||||
const previous = flights.current.get(id)
|
||||
if (previous?.kind === kind && previous.scope === scope) return previous.promise
|
||||
if (previous && kind !== 'cancel') {
|
||||
scope.error = '连接器正在操作,请稍后重试'
|
||||
publish()
|
||||
return Promise.reject(new Error(scope.error))
|
||||
}
|
||||
const flight: Flight = { scope, kind, promise: Promise.resolve() }
|
||||
const owns = (): boolean => current() && flights.current.get(id) === flight
|
||||
// Defer until the flight is registered, including synchronously rejecting host APIs.
|
||||
flight.promise = Promise.resolve().then(async () => {
|
||||
let failure: unknown
|
||||
try { await operation() } catch (reason) { failure = reason }
|
||||
if (owns()) {
|
||||
if (failure) scope.error = message(failure)
|
||||
try {
|
||||
const items = await list(false)
|
||||
if (owns() && !failure && inspectError) {
|
||||
const server = items?.find(item => item.id === id)
|
||||
if (server?.status === 'error') { failure = new Error(server.error || '连接器连接失败'); scope.error = message(failure); publish() }
|
||||
}
|
||||
} catch (reason) { failure ||= reason }
|
||||
}
|
||||
if (flights.current.get(id) === flight) { flights.current.delete(id); pending() }
|
||||
if (failure) throw failure
|
||||
})
|
||||
flights.current.set(id, flight)
|
||||
scope.error = ''
|
||||
pending()
|
||||
return flight.promise
|
||||
}, [scope, current, publish, pending, list])
|
||||
|
||||
const change = useCallback((id: string, enabled?: boolean): Promise<void> => run(id, `change:${String(enabled)}`, async () => {
|
||||
if (enabled !== undefined) { await window.ycode.mcp.setEnabled(id, enabled); return }
|
||||
// Read immediately before acting: other menus/settings can change this global registry.
|
||||
const server = (await window.ycode.mcp.list()).find(item => item.id === id)
|
||||
if (!server) throw new Error('MCP 服务已移除')
|
||||
if (!server.enabled) await window.ycode.mcp.setEnabled(id, true)
|
||||
else if (server.status !== 'connected') await window.ycode.mcp.connect(id)
|
||||
}, enabled !== false), [run])
|
||||
const cancel = useCallback((id: string): Promise<void> => {
|
||||
const previous = flights.current.get(id)
|
||||
return run(id, 'cancel', async () => {
|
||||
// Abort an existing transport now; an enable still awaiting persistence may create it
|
||||
// afterwards, so also disconnect once the superseded action has settled.
|
||||
let failure: unknown
|
||||
try { await window.ycode.mcp.disconnect(id) } catch (reason) { failure = reason }
|
||||
if (previous && previous.kind !== 'cancel') {
|
||||
await previous.promise.catch(() => undefined)
|
||||
await window.ycode.mcp.disconnect(id)
|
||||
} else if (failure) throw failure
|
||||
}, false)
|
||||
}, [run])
|
||||
|
||||
// A new owner renders empty immediately, before its effect/list promise runs.
|
||||
const visible = view.scope === scope ? view.value : scope
|
||||
return { items: visible.items, loading: visible.loading, error: visible.error, pendingId: visible.pendingId, refresh, change, cancel }
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { AppInfo } from '@shared/types'
|
||||
|
||||
interface Props {
|
||||
onClose(): void
|
||||
}
|
||||
|
||||
export default function AboutDialog({ onClose }: Props): React.JSX.Element {
|
||||
const [info, setInfo] = useState<AppInfo | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
void window.ycode.app.info().then(setInfo)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="overlay" onClick={onClose}>
|
||||
<div className="dialog dialog--narrow" onClick={(e) => e.stopPropagation()}>
|
||||
<h2 className="dialog__title">关于 YCode</h2>
|
||||
|
||||
<div className="about__brand">
|
||||
<span className="about__logo">Y</span>
|
||||
<div>
|
||||
<div className="about__name">{info?.name ?? 'YCode'}</div>
|
||||
<div className="about__version">版本 {info?.version ?? '—'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel" style={{ borderTop: 'none', paddingTop: 0 }}>
|
||||
<div className="kv">
|
||||
<span className="kv__k">Electron</span>
|
||||
<span className="kv__v">{info?.electron ?? '—'}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span className="kv__k">Chromium</span>
|
||||
<span className="kv__v">{info?.chrome ?? '—'}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span className="kv__k">Node</span>
|
||||
<span className="kv__v">{info?.node ?? '—'}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span className="kv__k">配置目录</span>
|
||||
<span className="kv__v" title={info?.configDir}>
|
||||
{info?.configDir ?? '—'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dialog__actions">
|
||||
<button className="btn" onClick={() => void window.ycode.app.openConfigDir()}>
|
||||
打开配置目录
|
||||
</button>
|
||||
<button className="btn btn--primary" onClick={onClose}>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ChatMessage } from '@shared/types'
|
||||
import { getModelIconByName } from '../workbuddy/composer/model-selector/source.mjs'
|
||||
import './assistant-message-details.css'
|
||||
|
||||
export function formatMessageTime(time: number | null | undefined, now = new Date()): string | undefined {
|
||||
if (time == null) return undefined
|
||||
const date = new Date(time)
|
||||
if (Number.isNaN(date.getTime())) return undefined
|
||||
|
||||
const hoursMinutes = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
if (date.toDateString() === now.toDateString()) return hoursMinutes
|
||||
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const year = date.getFullYear() === now.getFullYear() ? '' : `${date.getFullYear()}年`
|
||||
return `${year}${month}月${day}日 ${hoursMinutes}`
|
||||
}
|
||||
|
||||
function MessageTime({ time }: { time: number }): React.JSX.Element | null {
|
||||
const formatted = formatMessageTime(time)
|
||||
return formatted ? <span className="_message_time_tip_1p6pj_1 cb-message-time-tip">{formatted}</span> : null
|
||||
}
|
||||
|
||||
export function AssistantMessageDetails({ message }: { message: ChatMessage }): React.JSX.Element {
|
||||
return <span className="cb-assistant-message-details">
|
||||
{message.modelName && <span className="cb-credit-usage-text cb-credit-usage-text--feedback">
|
||||
<span className="_modelTag_1q2ou_65" title={message.modelId}>
|
||||
<span className="cb-model-tag-icon" aria-hidden="true">{getModelIconByName(message.modelName)}</span>
|
||||
<span className="_modelName_1q2ou_90">{message.modelName}</span>
|
||||
</span>
|
||||
</span>}
|
||||
<MessageTime time={message.createdAt} />
|
||||
</span>
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import MessageBubble, { planMarkdown } from './MessageBubble'
|
||||
import { useChatStore } from '../stores/chat'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
import SourceHome, { type SourceHomeMode } from './SourceHome'
|
||||
import SourceChatHeader from './SourceChatHeader'
|
||||
import { useConversationSearch } from '../workbuddy/conversation/useConversationSearch'
|
||||
import '../workbuddy/conversation/search-highlight-adapters.css'
|
||||
import SourceModelSelector from '../workbuddy/composer/model-selector/SourceModelSelector'
|
||||
import SourceWorkspacePicker from './SourceWorkspacePicker'
|
||||
import homeRobot from './source-home-assets/workbuddy-buddy-thumbnail-2713ed5b.png'
|
||||
import { expertText, type ExpertRecord, type SkillRecord } from '@shared/capabilities'
|
||||
import SourceExpertPicker from '../capabilities/SourceExpertPicker'
|
||||
import SourceActiveExpert from '../capabilities/SourceActiveExpert'
|
||||
import { useComposerConnectors } from '../capabilities/useComposerConnectors'
|
||||
import { SourceModeSubmenu, SourceConnectorsSubmenu, SourceReferenceFilesSubmenu } from '../workbuddy/composer/add-menu/SourceSubmenus'
|
||||
import type { ResourceEntry } from '@shared/resources'
|
||||
import { clearExpertInputPrefill, expertInputUpdate } from '../capabilities/expert-input'
|
||||
import SourceSkillPicker from '../capabilities/SourceSkillPicker'
|
||||
import { SkillsIcon } from '../capabilities/SkillSourceIcons'
|
||||
import { ComposerControls, type ComposerSendPayload } from '../workbuddy/composer/ComposerControls'
|
||||
import { QuestionFloating } from '../workbuddy/conversation'
|
||||
import type { AgentMode, ChatAttachment, PermissionMode } from '@shared/conversation'
|
||||
import type { ChatMessage } from '@shared/types'
|
||||
import { getRevertChanges } from '@shared/revert'
|
||||
import { collectWorkbenchArtifacts } from '../workbuddy/workbench/workbench-state'
|
||||
|
||||
interface Props {
|
||||
hidden?: boolean
|
||||
rightVisible: boolean
|
||||
onToggleRight(): void
|
||||
onOpenSettings(): void
|
||||
darkMode?: boolean
|
||||
}
|
||||
|
||||
export default function ChatPane({ rightVisible, onToggleRight, onOpenSettings, darkMode = false, hidden = false }: Props): React.JSX.Element {
|
||||
const messages = useChatStore((s) => s.messages)
|
||||
const running = useChatStore((s) => s.running)
|
||||
const error = useChatStore((s) => s.error)
|
||||
const sending = useChatStore((s) => s.sending)
|
||||
const sessions = useChatStore((s) => s.sessions)
|
||||
const currentId = useChatStore((s) => s.currentSessionId)
|
||||
const draftId = useChatStore((s) => s.draftId)
|
||||
const draftContext = useChatStore((s) => s.draftContext)
|
||||
const selectChatModel = useChatStore((s) => s.selectModel)
|
||||
const send = useChatStore((s) => s.send)
|
||||
const cancel = useChatStore((s) => s.cancel)
|
||||
const respond = useChatStore((s) => s.respond)
|
||||
const revert = useChatStore((s) => s.revert)
|
||||
const feedback = useChatStore((s) => s.feedback)
|
||||
const config = useSettingsStore((s) => s.config)
|
||||
const appInfo = useSettingsStore((s) => s.appInfo)
|
||||
const models = useSettingsStore((s) => s.models)
|
||||
const selectedModelId = useSettingsStore((s) => s.selectedModelId)
|
||||
const workspaces = useSettingsStore((s) => s.workspaces)
|
||||
const selectedWorkspaceId = useSettingsStore((s) => s.selectedWorkspaceId)
|
||||
const defaultWorkspaceRoot = useSettingsStore((s) => s.workspace)
|
||||
|
||||
const [drafts, setDrafts] = useState<Record<string, { text: string; attachments: ChatAttachment[]; resources?: string[]; skills?: Array<Pick<SkillRecord, 'id' | 'name'>>; editingId?: string; autoFilledPrompt?: string }>>({})
|
||||
const draftMoves = useRef(new Map<string, string>())
|
||||
const [skillPicker, setSkillPicker] = useState<{ owner: string; anchor: HTMLElement | null } | null>(null)
|
||||
const closeSkillPicker = useCallback(() => setSkillPicker(null), [])
|
||||
const [expertPicker, setExpertPicker] = useState<{ owner: string; anchor: HTMLElement | null } | null>(null)
|
||||
const [localExperts, setLocalExperts] = useState<ExpertRecord[]>([])
|
||||
const [expertError, setExpertError] = useState('')
|
||||
const closeExpertPicker = useCallback(() => setExpertPicker(null), [])
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
const refresh = (): void => { void window.ycode.experts.list().then(items => { if (active) { setLocalExperts(items); setExpertError('') } }).catch(error => { if (active) setExpertError(String(error)) }) }
|
||||
refresh()
|
||||
const dispose = window.ycode.chat.onEvent(event => { if (['done', 'cancelled', 'error'].includes(event.type)) refresh() })
|
||||
window.addEventListener('ycode:experts-changed', refresh)
|
||||
return () => { active = false; dispose(); window.removeEventListener('ycode:experts-changed', refresh) }
|
||||
}, [])
|
||||
const draftKey = currentId ?? `new-${draftId}`
|
||||
const entry = drafts[draftKey] ?? { text: '', attachments: [] }
|
||||
const draft = entry.text
|
||||
const setDraft = useCallback((value: string | ((current: string) => string)): void => {
|
||||
setDrafts((all) => { const current = all[draftKey] ?? { text: '', attachments: [] }; const text = typeof value === 'function' ? value(current.text) : value; return { ...all, [draftKey]: { ...current, text, autoFilledPrompt: text === current.autoFilledPrompt ? current.autoFilledPrompt : undefined } } })
|
||||
}, [draftKey])
|
||||
useEffect(() => {
|
||||
const activeOwner = (): string => { const state = useChatStore.getState(); return state.currentSessionId ?? `new-${state.draftId}` }
|
||||
const selectSkill = (event: Event): void => {
|
||||
const detail = (event as CustomEvent<{ skill: SkillRecord; owner: string }>).detail
|
||||
if (!detail?.skill || detail.owner !== activeOwner()) return
|
||||
setDrafts(all => { const previous = all[detail.owner] ?? { text: '', attachments: [] }; return { ...all, [detail.owner]: { ...previous, skills: [...(previous.skills ?? []).filter(item => item.id !== detail.skill.id), { id: detail.skill.id, name: detail.skill.name }] } } })
|
||||
}
|
||||
const intent = (event: Event): void => {
|
||||
const detail = (event as CustomEvent<{ text: string; owner: string; skill?: Pick<SkillRecord, 'id' | 'name'> }>).detail
|
||||
if (detail?.owner !== activeOwner() || typeof detail.text !== 'string') return
|
||||
setDrafts(all => ({ ...all, [detail.owner]: { ...(all[detail.owner] ?? { attachments: [] }), text: detail.text, ...(detail.skill ? { skills: [detail.skill] } : {}) } }))
|
||||
requestAnimationFrame(() => document.querySelector<HTMLTextAreaElement>('.wb-composer__input')?.focus())
|
||||
}
|
||||
const migrate = (event: Event): void => {
|
||||
const detail = (event as CustomEvent<{ from: string; to: string }>).detail
|
||||
if (!detail?.from?.startsWith('new-') || !detail.to) return
|
||||
draftMoves.current.set(detail.from, detail.to)
|
||||
if (draftMoves.current.size > 32) draftMoves.current.delete(draftMoves.current.keys().next().value!)
|
||||
setDrafts(all => { const previous = all[detail.from]; if (!previous) return all; const next = { ...all, [detail.to]: previous }; delete next[detail.from]; return next })
|
||||
}
|
||||
const expertInput = (event: Event): void => {
|
||||
const detail = (event as CustomEvent<{ owner: string; prompt?: string; explicit?: boolean; clear?: boolean }>).detail
|
||||
if (!detail || detail.owner !== activeOwner() || (!detail.clear && typeof detail.prompt !== 'string')) return
|
||||
setDrafts(all => { const previous = all[detail.owner] ?? { text: '', attachments: [] }; return { ...all, [detail.owner]: detail.clear ? clearExpertInputPrefill(previous) : expertInputUpdate(previous, detail.prompt!, detail.explicit, !detail.owner.startsWith('new-')) } })
|
||||
}
|
||||
window.addEventListener('ycode:select-skill', selectSkill); window.addEventListener('ycode:composer-intent', intent); window.addEventListener('ycode:composer-owner-migrated', migrate)
|
||||
window.addEventListener('ycode:expert-input', expertInput)
|
||||
return () => { window.removeEventListener('ycode:select-skill', selectSkill); window.removeEventListener('ycode:composer-intent', intent); window.removeEventListener('ycode:composer-owner-migrated', migrate); window.removeEventListener('ycode:expert-input', expertInput) }
|
||||
}, [])
|
||||
useEffect(() => { setSkillPicker(null); setExpertPicker(null) }, [draftKey, hidden])
|
||||
const [permissionMode, setPermissionMode] = useState<PermissionMode>('default-sandbox')
|
||||
const [confirmFullAccess, setConfirmFullAccess] = useState(true)
|
||||
const [revertTarget, setRevertTarget] = useState<ChatMessage | null>(null)
|
||||
const [revertError, setRevertError] = useState<string | null>(null)
|
||||
const [reverting, setReverting] = useState(false)
|
||||
const canRevert = (message: ChatMessage): boolean => {
|
||||
if (message.role !== 'user' || running || sending) return false
|
||||
try { return getRevertChanges(messages, message.id).length > 0 } catch { return false }
|
||||
}
|
||||
const [activeSegment, setActiveSegment] = useState<SourceHomeMode>('work')
|
||||
const [homeAction, setHomeAction] = useState<string | null>(null)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const followBottom = useRef(true)
|
||||
const quoteTable = useCallback((markdown: string): void => {
|
||||
setDraft((current) => current ? `${current}\n\n${markdown}` : markdown)
|
||||
document.querySelector<HTMLTextAreaElement>('.wb-composer__input')?.focus()
|
||||
}, [setDraft])
|
||||
useEffect(() => {
|
||||
const quote = (event: Event): void => {
|
||||
const detail = (event as CustomEvent<{ text: string; sessionId?: string }>).detail
|
||||
if (detail?.sessionId !== (currentId ?? undefined) || typeof detail.text !== 'string') return
|
||||
quoteTable(detail.text)
|
||||
}
|
||||
window.addEventListener('ycode:quote-text', quote)
|
||||
return () => window.removeEventListener('ycode:quote-text', quote)
|
||||
}, [currentId, quoteTable])
|
||||
|
||||
const title = sessions.find((s) => s.id === currentId)?.title ?? '新对话'
|
||||
const isHome = currentId === null || messages.length === 0
|
||||
const conversationSearch = useConversationSearch(messages)
|
||||
const currentSession = sessions.find((session) => session.id === currentId)
|
||||
const workspaceRoot = currentSession?.cwd ?? draftContext.cwd ?? defaultWorkspaceRoot
|
||||
const artifactCount = useMemo(() => collectWorkbenchArtifacts(messages, workspaceRoot).artifacts.length, [messages, workspaceRoot])
|
||||
const effectiveModelId = currentSession?.modelId ?? draftContext.modelId ?? selectedModelId
|
||||
const effectiveWorkspaceId = currentSession?.workspaceId ?? draftContext.workspaceId ?? selectedWorkspaceId
|
||||
const effectiveAgentMode = (currentId ? currentSession?.agentMode : draftContext.agentMode) ?? 'craft'
|
||||
// Left undefined when the session has no explicit choice; the selector then uses the model's own default.
|
||||
const effectiveThinkingLevel = currentSession?.thinkingLevel ?? draftContext.thinkingLevel
|
||||
const effectiveExpertId = currentSession ? currentSession.expertId : draftContext.expertId
|
||||
const selectedExpert = localExperts.find(item => item.id === effectiveExpertId)
|
||||
const activeExpert = useMemo(() => effectiveExpertId ? { id: effectiveExpertId,
|
||||
name: expertText(selectedExpert?.displayName) || selectedExpert?.name || '专家不可用',
|
||||
profession: expertText(selectedExpert?.profession), avatarUrl: selectedExpert?.avatar,
|
||||
expertType: selectedExpert?.expertType, suppressAutoSummon: true } : null, [effectiveExpertId, selectedExpert])
|
||||
const expertItems = useMemo(() => localExperts.map(expert => ({ ...expert, name: expertText(expert.displayName) || expert.name, profession: expertText(expert.profession), avatarUrl: expert.avatar, description: expertText(expert.displayDescription) || expert.description, disabled: !!expert.error || expert.expertType === 'team', searchText: [expert.id, expertText(expert.displayName), expertText(expert.profession), expert.description].join(' ').toLowerCase() })), [localExperts])
|
||||
const connectors = useComposerConnectors(`${draftKey}:${hidden ? 'hidden' : 'visible'}`)
|
||||
const [modeRevision, setModeRevision] = useState(0)
|
||||
const [referenceFiles, setReferenceFiles] = useState<{ owner: string; items: ResourceEntry[] }>({ owner: '', items: [] })
|
||||
const referenceScope = `${draftKey}:${workspaceRoot}`
|
||||
const referenceEpoch = useRef(0)
|
||||
const loadReferenceFiles = useCallback((): void => {
|
||||
const epoch = ++referenceEpoch.current, owner = draftKey
|
||||
if (hidden || !workspaceRoot) return
|
||||
void window.ycode.resources.list(currentId ?? undefined, effectiveWorkspaceId ?? undefined).then(items => {
|
||||
const state = useChatStore.getState()
|
||||
if (referenceEpoch.current === epoch && (state.currentSessionId ?? `new-${state.draftId}`) === owner) setReferenceFiles({ owner: referenceScope, items })
|
||||
}).catch(() => { if (referenceEpoch.current === epoch) setReferenceFiles({ owner: referenceScope, items: [] }) })
|
||||
}, [draftKey, currentId, workspaceRoot, effectiveWorkspaceId, hidden, referenceScope])
|
||||
useEffect(() => { loadReferenceFiles(); return () => { referenceEpoch.current++ } }, [loadReferenceFiles])
|
||||
const availableReferenceFiles = referenceFiles.owner === referenceScope ? referenceFiles.items : []
|
||||
const addMenuError = (reason: unknown): void => useChatStore.setState({ error: reason instanceof Error ? reason.message : String(reason) })
|
||||
|
||||
// 模型标签:OMP 内核激活时显示 OMP 的实际模型,否则显示自建网关的模型
|
||||
const modelLabel = models.find((model) => model.id === effectiveModelId)?.name || config?.model || appInfo?.omp?.model || '未配置模型'
|
||||
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current
|
||||
if (!el) return
|
||||
if (followBottom.current) el.scrollTop = el.scrollHeight
|
||||
}, [messages, running])
|
||||
|
||||
useEffect(() => {
|
||||
setRevertTarget(null)
|
||||
setRevertError(null)
|
||||
followBottom.current = true
|
||||
const el = scrollRef.current
|
||||
if (el) el.scrollTop = el.scrollHeight
|
||||
}, [currentId])
|
||||
|
||||
const submit = async ({ text, attachments, resources, mode }: ComposerSendPayload): Promise<void> => {
|
||||
if ((!text && !attachments.length) || running || sending) return
|
||||
followBottom.current = true
|
||||
await send(text, { attachments, resources, skillIds: entry.skills?.map(item => item.id), permissionMode: mode, ...(entry.editingId ? { fromMessageId: entry.editingId, action: 'edit' as const } : {}) })
|
||||
const destination = draftMoves.current.get(draftKey) ?? draftKey
|
||||
draftMoves.current.delete(draftKey)
|
||||
setDrafts((all) => ({ ...all, [draftKey]: { text: '', attachments: [] }, [destination]: { text: '', attachments: [] } }))
|
||||
}
|
||||
|
||||
const edit = (message: ChatMessage): void => {
|
||||
setDrafts((all) => ({ ...all, [draftKey]: { text: message.content, attachments: message.attachments ?? [], resources: message.resources ?? [], skills: message.skills, editingId: message.id } }))
|
||||
document.querySelector<HTMLTextAreaElement>('.wb-composer__input')?.focus()
|
||||
}
|
||||
const resend = (message: ChatMessage): Promise<void> => send(message.content, { attachments: message.attachments, resources: message.resources, skillIds: message.skills?.map(item => item.id), permissionMode, fromMessageId: message.id, action: 'resend' })
|
||||
const regenerate = (_requestId: string, message: ChatMessage): Promise<void> => {
|
||||
const preceding = messages.slice(0, messages.findIndex((item) => item.id === message.id)).findLast((item) => item.role === 'user')
|
||||
return preceding ? send(preceding.content, { attachments: preceding.attachments, resources: preceding.resources, skillIds: preceding.skills?.map(item => item.id), permissionMode, fromMessageId: message.id, action: 'regenerate' }) : Promise.resolve()
|
||||
}
|
||||
const lastAssistantId = messages.findLast((message) => message.role === 'assistant')?.id
|
||||
const question = messages.flatMap((message) => message.interactions ?? []).find((item) => item.kind === 'question' && ['pending', 'responding', 'error'].includes(item.status))
|
||||
|
||||
const bindDraftWorkspace = (ownerDraftId: number, workspaceId = useSettingsStore.getState().selectedWorkspaceId): void => {
|
||||
const settings = useSettingsStore.getState()
|
||||
const selected = settings.workspaces.find((item) => item.id === workspaceId)
|
||||
useChatStore.setState((state) => {
|
||||
if (state.currentSessionId !== null || state.draftId !== ownerDraftId) return state
|
||||
const context = { ...state.draftContext }
|
||||
delete context.workspaceId; delete context.cwd
|
||||
return { draftContext: selected ? { ...context, workspaceId: selected.id, cwd: selected.root } : context }
|
||||
})
|
||||
}
|
||||
const selectDraftWorkspace = async (id: string | null): Promise<void> => {
|
||||
const owner = useChatStore.getState()
|
||||
if (owner.currentSessionId !== null) return
|
||||
await useSettingsStore.getState().selectWorkspace(id)
|
||||
bindDraftWorkspace(owner.draftId)
|
||||
}
|
||||
const attachDraftWorkspace = async (): Promise<void> => {
|
||||
const owner = useChatStore.getState()
|
||||
if (owner.currentSessionId !== null) return
|
||||
const attachedWorkspaceId = await useSettingsStore.getState().attachWorkspace()
|
||||
if (attachedWorkspaceId !== undefined) bindDraftWorkspace(owner.draftId, attachedWorkspaceId)
|
||||
}
|
||||
const createDraftWorkspace = async (name: string): Promise<void> => {
|
||||
const owner = useChatStore.getState()
|
||||
if (owner.currentSessionId !== null) throw new Error('请先新建任务再创建工作空间')
|
||||
await useSettingsStore.getState().createWorkspace(name)
|
||||
bindDraftWorkspace(owner.draftId)
|
||||
}
|
||||
|
||||
/** 输入框(首页与会话内共用;首页编辑区更高,由 CSS 的 data-variant 区分) */
|
||||
const composer = (
|
||||
<>
|
||||
{entry.editingId && <div className="composer__edit-banner">编辑消息<button type="button" onClick={() => setDrafts((all) => ({ ...all, [draftKey]: { ...entry, editingId: undefined } }))}>取消</button></div>}
|
||||
<ComposerControls key={draftKey} className={isHome ? 'wb-composer--home' : undefined} sessionId={currentId} value={draft} onChange={value => { setDraft(value); if (value === '/') setSkillPicker({ owner: draftKey, anchor: document.querySelector('.wb-composer__add-button') }) }} attachments={entry.attachments} resources={entry.resources ?? []}
|
||||
onOpenSkills={anchor => setSkillPicker({ owner: draftKey, anchor })}
|
||||
onOpenExperts={anchor => { setSkillPicker(null); setExpertPicker({ owner: draftKey, anchor }); void window.ycode.experts.list().then(setLocalExperts).catch(error => setExpertError(String(error))) }}
|
||||
addMenuScopeKey={`${draftKey}:${hidden ? 'hidden' : 'visible'}`}
|
||||
onOpenAddMenu={() => { loadReferenceFiles(); connectors.refresh(); void window.ycode.experts.list().then(setLocalExperts).catch(reason => setExpertError(String(reason))) }}
|
||||
addMenuEntries={[{ id: 'add-files', children: [{ id: 'local-files', label: '本地文件' }] },
|
||||
...(availableReferenceFiles.length ? [{ id: 'reference-files' }] : []), { id: 'mode' }, { id: 'experts' }, { id: 'skills' }, { id: 'connectors' }]}
|
||||
addMenuSlots={{
|
||||
experts: ({ closeMenu }) => <SourceExpertPicker embedded items={expertItems} selectedId={effectiveExpertId} busy={sending || !!running} error={expertError} anchor={null} scopeKey={workspaceRoot ?? 'user'} onClose={closeMenu}
|
||||
onSelect={async expert => { if ((useChatStore.getState().currentSessionId ?? `new-${useChatStore.getState().draftId}`) !== draftKey) throw new Error('任务已切换'); await useChatStore.getState().selectExpert(expert.id) }}
|
||||
onManage={() => window.dispatchEvent(new CustomEvent('ycode:open-capabilities', { detail: { tab: 'experts', page: 'mine', returnOwner: draftKey } }))} />,
|
||||
skills: ({ closeMenu }) => <SourceSkillPicker embedded cwd={workspaceRoot} onClose={closeMenu} onImport={() => { void window.ycode.skills.importPath().catch(addMenuError) }} onSelect={skill => {
|
||||
setDrafts(all => { const previous = all[draftKey] ?? { text: '', attachments: [] }; return { ...all, [draftKey]: { ...previous, skills: [...(previous.skills ?? []).filter(item => item.id !== skill.id), { id: skill.id, name: skill.name }] } } })
|
||||
}} />,
|
||||
'reference-files': ({ closeMenu }) => <SourceReferenceFilesSubmenu provider={{ items: availableReferenceFiles.map(file => ({ id: file.path, name: file.name, path: file.path, isFolder: file.type === 'directory' })), onSelect: file => {
|
||||
const state = useChatStore.getState()
|
||||
if (!file.path || (state.currentSessionId ?? `new-${state.draftId}`) !== draftKey) return
|
||||
const resourcePath = file.path
|
||||
setDrafts(all => { const previous = all[draftKey] ?? { text: '', attachments: [] }; return { ...all, [draftKey]: { ...previous, resources: [...new Set([...(previous.resources ?? []), resourcePath])] } } })
|
||||
} }} onCloseMenu={closeMenu} />,
|
||||
mode: () => <SourceModeSubmenu key={`${effectiveAgentMode}:${modeRevision}`} selectedId={effectiveAgentMode} provider={{ disabled: sending || !!running, onChange: mode => {
|
||||
void useChatStore.getState().selectAgentMode(mode.id as AgentMode).catch(reason => { addMenuError(reason); setModeRevision(value => value + 1) })
|
||||
} }} />,
|
||||
connectors: ({ closeMenu }) => <><SourceConnectorsSubmenu provider={{ items: connectors.items.map(server => ({ id: server.id, name: server.name, description: server.transport === 'stdio' ? server.command : server.url,
|
||||
canToggle: server.trusted && !sending && !running, disabledReason: server.trusted ? undefined : '请先在管理连接器中批准当前配置',
|
||||
enabled: server.enabled && server.status === 'connected', isConnected: server.status === 'connected', everConnected: server.status === 'connected' || !!server.tools.length,
|
||||
status: connectors.pendingId === server.id ? 'connecting' : server.status, isCancellable: server.status === 'connecting', errorText: server.error })),
|
||||
onConnectItem: item => { void connectors.change(item.id).catch(() => undefined) }, onToggleItem: (item, enabled) => { void connectors.change(item.id, enabled).catch(() => undefined) },
|
||||
onCancelConnect: item => { void connectors.cancel(item.id).catch(() => undefined) }, onRepairItem: item => { void connectors.change(item.id).catch(() => undefined) },
|
||||
onSummonMore: () => window.dispatchEvent(new CustomEvent('ycode:open-capabilities', { detail: { tab: 'connectors' } })) }} onCloseMenu={closeMenu} />
|
||||
{connectors.error && <div className="msg__status msg__status--error" role="alert">{connectors.error}</div>}</>
|
||||
}}
|
||||
expertSlot={activeExpert && <span data-active-expert={effectiveExpertId}><SourceActiveExpert expert={activeExpert} scopeKey={draftKey} disabled={sending || !!running} onRemove={async () => {
|
||||
try { await useChatStore.getState().selectExpert(null) }
|
||||
catch (error) { useChatStore.setState({ error: error instanceof Error ? error.message : String(error) }); throw error }
|
||||
}} /></span>}
|
||||
capabilityReferences={!!entry.skills?.length && <div className="wb-composer__resources" aria-label="引用的能力">
|
||||
{entry.skills?.map(skill => <div className="wb-composer__resource" data-skill-uri={`skill://${encodeURIComponent(skill.name)}`} key={skill.id}><SkillsIcon /><span>{skill.name}</span><button type="button" aria-label={`移除技能 ${skill.name}`} onClick={() => setDrafts(all => ({ ...all, [draftKey]: { ...entry, skills: entry.skills?.filter(item => item.id !== skill.id) } }))}>×</button></div>)}</div>}
|
||||
floatingSlot={question?.kind === 'question' && <QuestionFloating key={question.id} config={{ questions: question.questions, title: question.title, toolCallId: question.toolCallId }}
|
||||
responding={question.status === 'responding'} error={question.error}
|
||||
labels={{ title: '需要你确认', skip: '跳过', next: '下一题', send: '发送回答', customAnswer: '输入其他回答', skipAll: '跳过全部', previousQuestion: '上一题', nextQuestion: '下一题', dismiss: '收起', notAnswered: '未回答' }}
|
||||
onSelect={(_text, answer) => respond(question.id, { kind: 'question', answer })}
|
||||
onSkip={() => void respond(question.id, { kind: 'question', answer: { questions: question.questions.map((item) => ({ id: item.id, question: item.question, answers: [] })) }, skipped: true }).catch(() => undefined)} />}
|
||||
onResourcesChange={(resources) => setDrafts((all) => ({ ...all, [draftKey]: { ...(all[draftKey] ?? { text: '', attachments: [] }), resources } }))}
|
||||
onAttachmentsChange={(attachments) => setDrafts((all) => ({ ...all, [draftKey]: { ...(all[draftKey] ?? { text: '' }), attachments } }))}
|
||||
attachmentHost={{ pick: () => window.ycode.attachments.pick(), stage: ({ filename, ...input }) => window.ycode.attachments.stage({ name: filename, ...input }), remove: (id) => window.ycode.attachments.remove(id) }}
|
||||
onRemoveAttachment={(id) => messages.some((message) => message.attachments?.some((item) => item.id === id)) ? Promise.resolve() : window.ycode.attachments.remove(id)}
|
||||
onReferencePath={async (path) => {
|
||||
const kind = await window.ycode.resources.classify(path, currentId ?? undefined)
|
||||
if (kind === 'unknown') throw new Error('无法引用该工作区资源')
|
||||
}}
|
||||
speech={{ transcribe: ({ wavBase64, mimeType }) => window.ycode.speech.transcribe({ audioBase64: wavBase64, mimeType }) }}
|
||||
onTranscript={(text) => setDraft((current) => `${current}${current ? ' ' : ''}${text}`)}
|
||||
permissionMode={permissionMode} onModeChange={setPermissionMode} permissionPlacement={isHome ? 'footer' : 'toolbar'} confirmFullAccessOnChange={confirmFullAccess}
|
||||
onSkipFutureFullAccessConfirmationChange={(skip) => setConfirmFullAccess(!skip)}
|
||||
onSend={submit} sending={sending || !!running} onStop={running ? cancel : undefined}
|
||||
modelSlot={<SourceModelSelector models={models} selectedId={effectiveModelId} label={modelLabel} disabled={sending || !!running} darkMode={darkMode}
|
||||
thinkingLevel={effectiveThinkingLevel}
|
||||
onThinkingLevelChange={(level) => useChatStore.getState().selectThinkingLevel(level)}
|
||||
onSelect={async (id) => { await selectChatModel(id); await useSettingsStore.getState().selectModel(id) }} onManage={() => {
|
||||
if (window.ycode.app.openSettings) void window.ycode.app.openSettings('model', 'new')
|
||||
else onOpenSettings()
|
||||
}} />}
|
||||
workspaceFooter={isHome ? <SourceWorkspacePicker key={draftKey} workspaces={workspaces} selectedId={effectiveWorkspaceId}
|
||||
disabled={currentId !== null || sending || !!running} darkMode={darkMode} onSelect={selectDraftWorkspace}
|
||||
onCreate={createDraftWorkspace} onOpenLocalFolder={attachDraftWorkspace} /> : undefined}
|
||||
placeholder={isHome ? '今天帮你做些什么? @ 添加上下文,/调用技能与指令' : '给 YCode 发送消息…'} />
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<section className="chat-pane cr-theme" style={hidden ? { display: 'none' } : undefined} data-view-id="main-content" data-cr-theme={darkMode ? 'dark' : 'light'}>
|
||||
{!isHome && <SourceChatHeader key={currentId} title={title} rightVisible={rightVisible} onToggleRight={onToggleRight}
|
||||
search={conversationSearch} onTitleChange={async (nextTitle) => {
|
||||
if (!currentId) return
|
||||
await window.ycode.session.rename(currentId, nextTitle)
|
||||
const sessions = await window.ycode.session.list()
|
||||
useChatStore.setState({ sessions })
|
||||
}} onShare={() => { window.dispatchEvent(new CustomEvent('ycode:not-implemented', { detail: '云端分享' })) }}
|
||||
onActionError={(error) => useChatStore.setState({ error: error instanceof Error ? error.message : String(error) })} />}
|
||||
{error && <div className="msg__status msg__status--error" role="alert">{error}</div>}
|
||||
{expertPicker && expertPicker.owner === draftKey && !hidden && <SourceExpertPicker key={draftKey} items={expertItems} selectedId={effectiveExpertId} busy={sending || !!running} error={expertError} anchor={expertPicker.anchor} scopeKey={workspaceRoot ?? 'user'} onClose={closeExpertPicker}
|
||||
onSelect={async expert => { if ((useChatStore.getState().currentSessionId ?? `new-${useChatStore.getState().draftId}`) !== expertPicker.owner) throw new Error('任务已切换'); await useChatStore.getState().selectExpert(expert.id) }}
|
||||
onManage={() => window.dispatchEvent(new CustomEvent('ycode:open-capabilities', { detail: { tab: 'experts', page: 'mine', returnOwner: draftKey } }))} />}
|
||||
{skillPicker && skillPicker.owner === draftKey && !hidden && <SourceSkillPicker key={draftKey} cwd={workspaceRoot} anchor={skillPicker.anchor} onClose={closeSkillPicker} onSelect={skill => {
|
||||
setDrafts(all => { const previous = all[draftKey] ?? { text: '', attachments: [] }; return { ...all, [draftKey]: { ...previous, text: previous.text === '/' ? '' : previous.text, skills: [...(previous.skills ?? []).filter(item => item.id !== skill.id), { id: skill.id, name: skill.name }] } } })
|
||||
}} />}
|
||||
|
||||
{isHome ? (
|
||||
<SourceHome mode={activeSegment} onModeChange={setActiveSegment} composer={composer} selectedAction={homeAction}
|
||||
onActionChange={(action) => setHomeAction(action?.id ?? null)} onPromptPick={setDraft} robotSrc={homeRobot} />
|
||||
) : (
|
||||
<>
|
||||
<div className="conversation-timeline"><div className="cr-message-list-viewport">
|
||||
<div className="chat-scroll cr-message-list" ref={scrollRef} onScroll={(event) => {
|
||||
const el = event.currentTarget
|
||||
followBottom.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80
|
||||
}}>
|
||||
<div className="chat-thread cr-message-list__content"><div className="cr-document">
|
||||
{messages.map((message) => (
|
||||
<MessageBubble key={message.id} message={message} sessionId={currentId} onQuote={quoteTable} darkMode={darkMode} onRespond={respond}
|
||||
artifactCount={!running && !sending && message.id === lastAssistantId ? artifactCount : 0}
|
||||
onShowArtifacts={() => window.dispatchEvent(new CustomEvent('ycode:open-artifacts', { detail: { sessionId: currentId } }))}
|
||||
buildingPlanId={running ? messages.find((item) => item.role === 'user' && item.requestId === running.runId)?.planId : undefined}
|
||||
onBuildPlan={async (plan) => { if (running || sending) throw new Error('请等待当前运行结束'); await send(`请执行以下计划:\n\n${planMarkdown(plan)}`, { permissionMode, planId: plan.id }) }}
|
||||
actions={{ busyState: running || sending ? 'talking' : 'idle', canRevert: canRevert(message),
|
||||
canRegenerate: !running && !sending, isSessionActive: true, isLastAssistant: message.id === lastAssistantId,
|
||||
requestId: message.requestId, requestMessages: message.requestId ? messages.filter((item) => item.requestId === message.requestId) : undefined,
|
||||
onEdit: edit, onResend: resend, onRegenerate: regenerate,
|
||||
onRevert: canRevert(message) ? () => { setRevertError(null); setRevertTarget(message) } : undefined,
|
||||
onFeedback: (target, value) => feedback(target.id, value) }} />
|
||||
))}
|
||||
</div></div>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div className="composer">
|
||||
<div className="composer__area">
|
||||
{composer}
|
||||
</div>
|
||||
<div className="composer__disclaimer">内容由 AI 生成,请核实重要信息</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{revertTarget && <div className="overlay" onClick={() => !reverting && setRevertTarget(null)}>
|
||||
<section className="dialog" role="dialog" aria-modal="true" aria-label="恢复到此版本" onClick={(event) => event.stopPropagation()}>
|
||||
<h2 className="dialog__title">恢复到此版本?</h2>
|
||||
<p>将恢复此消息之后由工具修改的文件,并退回对应的对话上下文。</p>
|
||||
{revertError && <p role="alert">{revertError}</p>}
|
||||
<div className="dialog__actions"><button className="btn" disabled={reverting} onClick={() => setRevertTarget(null)}>取消</button>
|
||||
<button className="btn btn--primary" disabled={reverting} onClick={() => {
|
||||
setReverting(true)
|
||||
void revert(revertTarget.id).then(() => setRevertTarget(null)).catch((error) => setRevertError(String(error instanceof Error ? error.message : error))).finally(() => setReverting(false))
|
||||
}}>{reverting ? '恢复中…' : '恢复'}</button></div>
|
||||
</section>
|
||||
</div>}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useCallback, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore, type ReactElement } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import type MarkdownToken from 'markdown-it/lib/token.mjs'
|
||||
import { ReactMarkdown } from '../workbuddy/markdown/ReactMarkdown'
|
||||
import { ReactMarkdownProvider, type TableActionContext } from '../workbuddy/markdown/context'
|
||||
import { createMarkdownParser } from '../workbuddy/markdown/parser'
|
||||
import { getMdReadyVersion, subscribeMdReady } from '../workbuddy/markdown/languages'
|
||||
import { applyMathPlugin, getMathVersion, loadMathModules, subscribeMathReady } from '../workbuddy/markdown/math'
|
||||
import { resolveCodeBlockFenceInfo } from '../workbuddy/markdown/code-fence'
|
||||
import '../styles/markdown.css'
|
||||
import { ImagePreview } from '../workbuddy/images/ImagePreview'
|
||||
import { requiresHostImageResolution, type ImagePreviewRequest, type ImageUrlResolver } from '../workbuddy/images/image-url'
|
||||
import { downloadImageInHost, downloadInHost, openPathInHost, resolveCodeInHost, resolveImageInHost } from '../workbuddy/host'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
|
||||
export { CopyButton } from '../workbuddy/components/CodeBlock'
|
||||
export { parseCodeBlockMeta, resolveCodeBlockFenceInfo } from '../workbuddy/markdown/code-fence'
|
||||
export type { CodeBlockFileMetadata, CodeBlockFenceInfo } from '../workbuddy/markdown/code-fence'
|
||||
|
||||
export interface MarkdownContentProps {
|
||||
content: string
|
||||
streaming?: boolean
|
||||
onQuote?: (markdown: string) => void
|
||||
messageId?: string
|
||||
darkMode?: boolean
|
||||
imageUrlResolver?: ImageUrlResolver
|
||||
}
|
||||
|
||||
// Compatibility utilities for existing callers/tests; product rendering uses recovered parser attributes.
|
||||
export function getCodeBlockLanguage(info: string): string { return resolveCodeBlockFenceInfo(info).language || 'plaintext' }
|
||||
export function getCodeBlockText(token: Pick<MarkdownToken, 'content'>): string { return token.content }
|
||||
export function getMappedSource(source: string, map: [number, number] | null): string {
|
||||
return map ? source.replace(/\r\n?/g, '\n').split('\n').slice(map[0], map[1]).join('\n') : ''
|
||||
}
|
||||
|
||||
/** YCode integration adapter; source-derived renderer lives in workbuddy/markdown. */
|
||||
export default function MarkdownContent({ content, streaming = false, onQuote, messageId, darkMode = false, imageUrlResolver = resolveImageInHost }: MarkdownContentProps): ReactElement {
|
||||
const fallbackId = useId()
|
||||
const workspace = useSettingsStore((state) => state.workspace)
|
||||
const paths = useMemo(() => workspace ? [workspace] : [], [workspace])
|
||||
const [imagePreview, setImagePreview] = useState<ImagePreviewRequest | null>(null)
|
||||
const previewVersion = useRef(0)
|
||||
useEffect(() => () => { previewVersion.current++ }, [])
|
||||
const onPreviewImage = useCallback((request: ImagePreviewRequest): void => {
|
||||
const version = ++previewVersion.current
|
||||
void Promise.all(request.urlList.map(async (url) => requiresHostImageResolution(url) ? await imageUrlResolver(url) : url))
|
||||
.then((urls) => {
|
||||
if (version !== previewVersion.current) return
|
||||
const selected = urls[request.initialIndex]
|
||||
const urlList = urls.filter((url): url is string => !!url)
|
||||
if (urlList.length) setImagePreview({ urlList, initialIndex: selected ? Math.max(0, urlList.indexOf(selected)) : 0 })
|
||||
}).catch(() => undefined)
|
||||
}, [imageUrlResolver])
|
||||
const closeImagePreview = useCallback(() => { previewVersion.current++; setImagePreview(null) }, [])
|
||||
const highlightVersion = useSyncExternalStore(subscribeMdReady, getMdReadyVersion, getMdReadyVersion)
|
||||
const mathVersion = useSyncExternalStore(subscribeMathReady, getMathVersion, getMathVersion)
|
||||
useEffect(() => { void loadMathModules() }, [])
|
||||
const mdInstance = useMemo(() => {
|
||||
const md = createMarkdownParser({ loading: true })
|
||||
applyMathPlugin(md)
|
||||
return md
|
||||
}, [mathVersion])
|
||||
const renderTableAction = useCallback(({ markdown, disabled }: TableActionContext) => onQuote && markdown ? (
|
||||
<button type="button" className="markdown-content__table-quote" disabled={disabled} onClick={() => onQuote(markdown)}>添加到对话</button>
|
||||
) : null, [onQuote])
|
||||
const value = useMemo(() => ({ mdInstance, highlightVersion, renderTableAction: onQuote ? renderTableAction : undefined,
|
||||
requestId: messageId || fallbackId, paths, resolveCode: resolveCodeInHost, pathClickHandler: { onPathClick: openPathInHost },
|
||||
imageUrlResolver, preferHostImageResolution: true, onPreviewImage, onDownload: downloadInHost, isDarkMode: darkMode }),
|
||||
[mdInstance, highlightVersion, onQuote, renderTableAction, messageId, fallbackId, paths, onPreviewImage, darkMode, imageUrlResolver])
|
||||
return <>
|
||||
<ReactMarkdownProvider value={value}><ReactMarkdown isFinished={!streaming} messageId={messageId}>{content}</ReactMarkdown></ReactMarkdownProvider>
|
||||
{imagePreview ? createPortal(<ImagePreview {...imagePreview} onClose={closeImagePreview} onDownload={downloadImageInHost} />, document.body) : null}
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
import type { ChatMessage, MessagePart } from '@shared/types'
|
||||
import SourceAgentAvatar from './SourceAgentAvatar'
|
||||
import SourceArtifactLink from './SourceArtifactLink'
|
||||
import { AssistantMessageDetails } from './AssistantMessageDetails'
|
||||
import { CompletedFold, LiveEventDisclosure } from '../workbuddy/conversation/CompletedFold'
|
||||
import type { CompletedFoldToolCallClassification } from '../workbuddy/conversation/completed-fold'
|
||||
import { resolveSourceToolIcon } from '../workbuddy/conversation/SourceToolIcons'
|
||||
import { SourceToolHeader } from '../workbuddy/conversation/SourceToolHeader'
|
||||
import { getCapturedToolKind } from '../workbuddy/conversation/ToolCallRenderer'
|
||||
import { findToolApproval, isActiveToolApproval, sourcePermissionMetadata, toolApprovalOption, type ToolApprovalControl } from '../workbuddy/conversation/tool-approval'
|
||||
import { sourceConversationLabel, type SourceConversationLabelKey } from '../workbuddy/conversation/source-labels'
|
||||
import { computeSourceFoldSummary } from '../workbuddy/conversation/recovered-fold-summary/index.mjs'
|
||||
import { Fragment, useState } from 'react'
|
||||
import type { MessageActionToolbarProps } from '../workbuddy/message-actions/MessageActionToolbar'
|
||||
import { MessageActionToolbar } from '../workbuddy/message-actions/MessageActionToolbar'
|
||||
import { PermissionConfirm, QuestionAnswerTranscript, ThinkingBlock, PlanCreateBlock, TaskBlock, ToolCallRenderer, type ConversationToolCall } from '../workbuddy/conversation'
|
||||
import { conversationLabel } from '../workbuddy/conversation/labels'
|
||||
import { downloadInHost, openPathInHost } from '../workbuddy/host'
|
||||
import { openArtifactPreview } from '../stores/resources'
|
||||
import type { InteractionResponse, PlanContent } from '@shared/conversation'
|
||||
import MarkdownContent from './MarkdownContent'
|
||||
import StreamingFooter from '../workbuddy/conversation/StreamingFooter'
|
||||
import StreamingTurnDuration from '../workbuddy/conversation/StreamingTurnDuration'
|
||||
import { getMessageParts } from '@shared/message-parts'
|
||||
import { projectWorkBuddyHistoryTool } from '@shared/workbuddy-history-projection'
|
||||
|
||||
interface Props {
|
||||
message: ChatMessage
|
||||
onQuote?(markdown: string): void
|
||||
darkMode?: boolean
|
||||
actions?: Omit<MessageActionToolbarProps, 'message'>
|
||||
onRespond?(id: string, response: InteractionResponse): Promise<void>
|
||||
sessionId?: string | null
|
||||
onBuildPlan?(plan: PlanContent): Promise<void>
|
||||
buildingPlanId?: string
|
||||
artifactCount?: number
|
||||
onShowArtifacts?(): void
|
||||
}
|
||||
|
||||
export default function MessageBubble({ message, onQuote, darkMode, actions, onRespond, sessionId, onBuildPlan, buildingPlanId, artifactCount = 0, onShowArtifacts }: Props): React.JSX.Element {
|
||||
const [actionError, setActionError] = useState<string | null>(null)
|
||||
const [liveEventsExpanded, setLiveEventsExpanded] = useState(false)
|
||||
const perform = (action: () => Promise<unknown>): void => {
|
||||
setActionError(null)
|
||||
void action().catch((error) => setActionError(error instanceof Error ? error.message : String(error)))
|
||||
}
|
||||
const viewPlan = (plan: PlanContent, selectedTaskId?: string | number): void => openArtifactPreview({ kind: 'artifact', path: plan.filePath || `plan:${plan.id}`,
|
||||
name: plan.title, artifactType: 'implementation-plan', content: planMarkdown(plan, selectedTaskId) })
|
||||
const downloadPlan = (plan: PlanContent): void => perform(() => downloadInHost({ filename: `${plan.title.replace(/[<>:"/\\|?*]/g, '_') || 'plan'}.md`, mimeType: 'text/markdown', encoding: 'utf8', data: planMarkdown(plan) }))
|
||||
// 用户消息:右侧灰色气泡
|
||||
if (message.role === 'user') {
|
||||
return (
|
||||
<div className="msg--user cr-frame cr-frame--right"><div className="cr-frame__content">
|
||||
<div className="cr-self-message"><div className="cr-self-message__content">
|
||||
<div className="bubble cr-self-bubble"><div className="cr-user-content"><div className="yc-user-editable">{message.content.split('\n').map((line, index) => <p key={index}><span className="yc-user-leaf"><span>{line || <br />}</span></span></p>)}</div></div></div>
|
||||
{!!message.skills?.length && <div className="wb-composer__resources" aria-label="使用的技能">{message.skills.map(skill => <span className="wb-composer__resource" data-skill-uri={skill.uri} key={skill.id}>{skill.name}</span>)}</div>}
|
||||
{message.attachments?.map((attachment) => <div className="msg__attachment" key={attachment.id}>
|
||||
{attachment.previewUrl ? <MarkdownContent content={`![${attachment.name.replace(/[\[\]]/g, '')}](${attachment.previewUrl})`} messageId={attachment.id} darkMode={darkMode} /> : <span>{attachment.name}</span>}
|
||||
</div>)}
|
||||
</div></div>
|
||||
<MessageActionToolbar message={message} {...actions} />
|
||||
</div></div>
|
||||
)
|
||||
}
|
||||
|
||||
const streaming = message.status === 'streaming'
|
||||
const parts = getMessageParts(message)
|
||||
const tools = (message.tools ?? []).map(tool => tool.metadata?.importedHistory ? projectWorkBuddyHistoryTool(tool)
|
||||
: tool.metadata?.capability === 'mcp' ? { ...tool, name: 'mcp_call_tool', args: { serverName: tool.metadata.serverName, toolName: tool.metadata.toolName, arguments: tool.args } } : tool)
|
||||
const inlineApprovals = new Map(tools.flatMap(tool => {
|
||||
const kind = getCapturedToolKind(tool.name)
|
||||
const request = findToolApproval(message.interactions, tool.id)
|
||||
if (!onRespond || !request || !['write-file', 'command'].includes(kind ?? '') ||
|
||||
!toolApprovalOption(request.options, 'allow') || !toolApprovalOption(request.options, 'reject')) return []
|
||||
return [[tool.id, request] as const]
|
||||
}))
|
||||
const inlineApprovalIds = new Set([...inlineApprovals.values()].map(request => request.id))
|
||||
const classifyTool = (part: Extract<MessagePart, { type: 'tool' }>): CompletedFoldToolCallClassification | undefined => {
|
||||
const name = tools.find(tool => tool.id === part.toolId)?.name ?? ''
|
||||
if (['show_widget', 'visualize:show_widget', 'visualizer:show_widget'].includes(name)) return { role: 'show-widget' }
|
||||
if (['ask_followup_question', 'ask_user_question', 'askfollowupquestion', 'askuserquestion'].includes(name)) return { role: 'inline-question' }
|
||||
return undefined
|
||||
}
|
||||
const renderTool = (id: string): React.ReactNode => {
|
||||
const tool = tools.find((item) => item.id === id)
|
||||
if (!tool) return null
|
||||
const nativeResult = tool.result ?? tool.output ?? tool.summary
|
||||
const result = nativeResult && typeof nativeResult === 'object' && 'result' in nativeResult ? nativeResult : { result: nativeResult }
|
||||
const request = inlineApprovals.get(id)
|
||||
const activeRequest = request && isActiveToolApproval(request) ? request : undefined
|
||||
const approval: ToolApprovalControl | undefined = activeRequest && onRespond ? {
|
||||
status: activeRequest.status as ToolApprovalControl['status'],
|
||||
onDecision: decision => {
|
||||
const option = toolApprovalOption(activeRequest.options, decision)
|
||||
if (option) perform(() => onRespond(activeRequest.id, { kind: 'approval', optionId: option.optionId }))
|
||||
}
|
||||
} : undefined
|
||||
const view: ConversationToolCall = { id: tool.id, name: tool.name, status: activeRequest ? 'pending' : tool.status === 'ok' ? 'success' : tool.status,
|
||||
intent: tool.intent,
|
||||
args: tool.args && typeof tool.args === 'object' ? tool.args as Record<string, unknown> : { input: tool.argsRaw },
|
||||
result, output: tool.metadata?.importedHistory && ['askuserquestion', 'todo_write'].includes(tool.name) ? undefined : tool.output,
|
||||
metaData: activeRequest ? sourcePermissionMetadata(tool.metadata, activeRequest) : tool.metadata,
|
||||
isFinished: tool.status !== 'running', isLastContent: parts.at(-1)?.type === 'tool' && (parts.at(-1) as { toolId: string }).toolId === id }
|
||||
const plan = planFromTool(tool.result, tool.args, tool.id, tool.name)
|
||||
const decideTask = async (index: number, toolCallId: string, data: { toolInfo?: Array<{ permissionRequestId?: string }> } | undefined, approve: boolean): Promise<void> => {
|
||||
const binding = data?.toolInfo?.[index]?.permissionRequestId || toolCallId
|
||||
const interaction = message.interactions?.find((item) => item.kind === 'approval' && [item.id, item.requestId, item.toolCallId].includes(binding))
|
||||
if (interaction?.kind !== 'approval' || !onRespond) throw new Error('审批请求已结束或未连接')
|
||||
const option = interaction.options.find((item) => approve ? item.kind.startsWith('allow') : item.kind.startsWith('reject'))
|
||||
if (!option) throw new Error('审批选项不可用')
|
||||
await onRespond(interaction.id, { kind: 'approval', optionId: option.optionId })
|
||||
}
|
||||
const hasApproval = !!onRespond && message.interactions?.some((item) => item.kind === 'approval' && ['pending', 'error', 'responding'].includes(item.status))
|
||||
return <ToolCallRenderer key={id} tool={view} approval={approval} sessionId={sessionId ?? undefined} renderMarkdown={(text) => <MarkdownContent content={text} darkMode={darkMode} />}
|
||||
onOpenFile={(request) => request.artifact
|
||||
? openArtifactPreview({ kind: 'artifact', path: request.filePath, name: request.title || request.artifact.title || request.filePath, content: request.content ?? '', artifact: request.artifact })
|
||||
: request.artifactType && request.content !== undefined
|
||||
? openArtifactPreview({ kind: 'artifact', path: request.filePath, name: request.title || request.filePath, content: request.content, artifactType: request.artifactType })
|
||||
: openPathInHost(request.filePath, 'file', request.startLine ? { start: request.startLine, end: request.endLine ?? request.startLine } : undefined)}
|
||||
onOpenLink={(url) => perform(() => window.ycode.resources.openExternal(url))}
|
||||
onTaskApprove={hasApproval ? (index, toolCallId, data) => decideTask(index, toolCallId, data, true) : undefined}
|
||||
onTaskReject={hasApproval ? (index, toolCallId, data) => decideTask(index, toolCallId, data, false) : undefined}
|
||||
onPlanTaskClick={(request) => viewPlan(plan, request.taskId)} onPlanView={() => viewPlan(plan)} onPlanDownload={() => downloadPlan(plan)}
|
||||
onPlanBuild={onBuildPlan ? (request) => perform(() => onBuildPlan({ id: request.planId, title: request.planName, steps: request.tasks.map((task) => ({ id: String(task.id), text: task.title, status: 'pending' })) })) : undefined}
|
||||
isBuildingPlan={buildingPlanId === plan.id} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="msg--assistant cb-assistant-message cr-frame cr-frame--left"><div className="cr-frame__content"><div className="cr-agent">
|
||||
{/* 助手头部:24px 圆形标识 + 名称,间距 10px,行高 24.5px */}
|
||||
<div className="msg__header cr-agent__header">
|
||||
<div className="cr-agent__logo" aria-hidden>{message.assistantDisplay?.avatarUrl ? <img src={message.assistantDisplay.avatarUrl} alt="" style={{ width: '100%', height: '100%', borderRadius: '50%', objectFit: 'cover' }} /> : <SourceAgentAvatar />}</div>
|
||||
<div className="msg__name cr-agent__name">{message.assistantDisplay?.profession || message.assistantDisplay?.name || 'YCode'}</div>
|
||||
</div>
|
||||
<div className="cr-agent__body-row"><div className="cr-agent__body">
|
||||
{streaming && typeof message.turnStartedAt === 'number' && Number.isFinite(message.turnStartedAt) && <StreamingTurnDuration startedAt={message.turnStartedAt} />}
|
||||
<div className="cr-agent__content">
|
||||
<CompletedFold parts={parts} foldId={message.id}
|
||||
turnFinished={message.status === 'done' || message.status === 'cancelled' || message.status === 'error'}
|
||||
turnCancelled={message.status === 'cancelled'} turnDurationMs={message.turnDurationMs}
|
||||
expandFinishedEvents={liveEventsExpanded}
|
||||
classifyToolCall={classifyTool}
|
||||
renderGroup={(group, children) => {
|
||||
if (!group.isRunning) return <Fragment key={group.key}>{children}</Fragment>
|
||||
const groupedTools = group.units.flatMap(unit => {
|
||||
const part = unit.part as MessagePart
|
||||
return part.type === 'tool' ? tools.filter(tool => tool.id === part.toolId) : []
|
||||
})
|
||||
if (groupedTools.some(tool => {
|
||||
const request = inlineApprovals.get(tool.id)
|
||||
return !!request && isActiveToolApproval(request)
|
||||
})) return <Fragment key={group.key}>{children}</Fragment>
|
||||
|
||||
const latestProgressIndex = parts.reduce((latest, part, index) => part.type === 'tool' || part.type === 'reasoning' ? index : latest, -1)
|
||||
if (!group.units.some(unit => unit.index === latestProgressIndex)) return null
|
||||
|
||||
const latestPart = parts[latestProgressIndex]
|
||||
const latestTool = latestPart?.type === 'tool' ? tools.find(tool => tool.id === latestPart.toolId) : undefined
|
||||
const toolArgs = latestTool?.args && typeof latestTool.args === 'object' ? latestTool.args as Record<string, unknown> : {}
|
||||
const summaryArgs = latestTool && ['list_dir', 'list_files'].includes(latestTool.name) && !toolArgs.target_directory && !toolArgs.root && typeof toolArgs.path === 'string'
|
||||
? { ...toolArgs, target_directory: toolArgs.path }
|
||||
: toolArgs
|
||||
const pendingConfirmation = !!latestTool && inlineApprovals.has(latestTool.id) && isActiveToolApproval(inlineApprovals.get(latestTool.id)!)
|
||||
const summaryStatus = !latestTool ? 'success' : pendingConfirmation ? 'pending'
|
||||
: latestTool.status === 'ok' ? 'executed'
|
||||
: ['running', 'executing'].includes(latestTool.status) ? 'stream_executing'
|
||||
: latestTool.status
|
||||
const actionSummary = latestTool ? computeSourceFoldSummary([{ type: 'tool-call', tool: {
|
||||
name: latestTool.name, args: summaryArgs, result: latestTool.result ?? latestTool.output,
|
||||
pendingConfirmation, status: summaryStatus
|
||||
} }], { isRunning: true,
|
||||
t: (key, params) => sourceConversationLabel(key as SourceConversationLabelKey, params) }) : undefined
|
||||
const actionText = actionSummary?.text || conversationLabel('message.loading.thinking')
|
||||
const activityIcon = latestTool ? resolveSourceToolIcon(latestTool.name) : undefined
|
||||
const liveEventParts: Array<{ part: Extract<MessagePart, { type: 'tool' | 'reasoning' }>; index: number }> = []
|
||||
parts.forEach((part, eventIndex) => {
|
||||
if (part.type === 'reasoning') {
|
||||
if (part.text.trim()) liveEventParts.push({ part, index: eventIndex })
|
||||
return
|
||||
}
|
||||
if (part.type !== 'tool') return
|
||||
const tool = tools.find(item => item.id === part.toolId)
|
||||
if (!tool || classifyTool(part)?.role) return
|
||||
const approvalRequest = inlineApprovals.get(tool.id)
|
||||
if (approvalRequest && isActiveToolApproval(approvalRequest)) return
|
||||
liveEventParts.push({ part, index: eventIndex })
|
||||
})
|
||||
const activityHeader = <div className="cr-captured-tool cr-streaming-activity" role="status" aria-live="polite" data-activity-tool-id={latestTool?.id}>
|
||||
<SourceToolHeader icon={activityIcon} showIconWhileExecuting
|
||||
isExecuting={actionSummary?.status === 'running'} primaryLoading={actionSummary?.status === 'running'}
|
||||
primaryContent={actionText} />
|
||||
</div>
|
||||
return <LiveEventDisclosure id={`live-events-${encodeURIComponent(message.id)}`}
|
||||
header={activityHeader} expanded={liveEventsExpanded} onExpandedChange={setLiveEventsExpanded}>
|
||||
{liveEventParts.map(({ part, index: eventIndex }) => part.type === 'tool'
|
||||
? <div className="cr-live-event-list__item" data-event-type="tool" key={`tool:${part.toolId}`}>
|
||||
{renderTool(part.toolId)}
|
||||
</div>
|
||||
: <div className="cr-live-event-list__item" data-event-type="reasoning" key={`reasoning:${part.id}`}>
|
||||
<ThinkingBlock text={part.text} isFinished={part.finished || !streaming}
|
||||
isLastContent={eventIndex === parts.length - 1} title={sourceConversationLabel('reasoning.title')}
|
||||
renderMarkdown={text => <MarkdownContent content={text} darkMode={darkMode} />} />
|
||||
</div>)}
|
||||
</LiveEventDisclosure>
|
||||
}}
|
||||
renderUnit={(candidate, index) => {
|
||||
const part = candidate as MessagePart
|
||||
if (part.type === 'tool') return renderTool(part.toolId)
|
||||
if (part.type === 'reasoning') return <ThinkingBlock key={part.id} text={part.text} isFinished={part.finished || !streaming} isLastContent={index === parts.length - 1} title={sourceConversationLabel('reasoning.title')} renderMarkdown={text => <MarkdownContent content={text} darkMode={darkMode} />} />
|
||||
if (part.type === 'plan' && message.tools?.some((tool) => ['todo_write', 'TodoWrite'].includes(tool.name) && tool.result && typeof tool.result === 'object' && (tool.result as { id?: string }).id === part.plan.id)) return null
|
||||
if (part.type === 'plan') return <PlanCreateBlock key={part.plan.id} status="success" data={{ id: part.plan.id, name: part.plan.title,
|
||||
todolist: part.plan.steps.map((step) => ({ id: step.id, content: step.text, status: step.status })) }}
|
||||
onTaskClick={(id) => viewPlan(part.plan, id)} onViewPlan={() => viewPlan(part.plan)} onDownloadPlan={() => downloadPlan(part.plan)}
|
||||
onBuildPlan={onBuildPlan ? () => perform(() => onBuildPlan(part.plan)) : undefined} isBuilding={buildingPlanId === part.plan.id} />
|
||||
if (part.type === 'task') return <TaskBlock key={part.task.id} status={part.task.status} data={{ description: part.task.title,
|
||||
prompt: part.task.description, finalResult: part.task.summary }} renderMarkdown={(text) => <MarkdownContent content={text} darkMode={darkMode} />} />
|
||||
if (part.type === 'artifact') return <button type="button" key={part.artifact.id} className="msg__artifact"
|
||||
onClick={() => part.artifact.preview
|
||||
? openArtifactPreview({ kind: 'artifact', path: part.artifact.path, name: part.artifact.name, content: part.artifact.preview.content ?? '', artifact: part.artifact.preview })
|
||||
: openPathInHost(part.artifact.path, 'file')}>{part.artifact.name}</button>
|
||||
if (part.type === 'image') return <MarkdownContent key={part.id} content={`![${(part.alt ?? '').replace(/[\[\]]/g, '')}](${part.url})`} messageId={part.id} darkMode={darkMode} />
|
||||
return (
|
||||
<div className={`msg__content cr-text-block${index > 0 ? ' cr-text-block--spaced-top' : ''}`} key={`text-${index}`}>
|
||||
<MarkdownContent
|
||||
content={part.text}
|
||||
messageId={message.id}
|
||||
darkMode={darkMode}
|
||||
streaming={streaming && index === parts.length - 1}
|
||||
onQuote={onQuote}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}} />
|
||||
|
||||
{message.interactions?.map((interaction) => interaction.kind === 'approval' ? inlineApprovalIds.has(interaction.id)
|
||||
? interaction.error && !actionError ? <p key={interaction.id} className="msg__status msg__status--error" role="alert">{interaction.error}</p> : null
|
||||
: <PermissionConfirm key={interaction.id}
|
||||
pendingId={interaction.id} toolName={interaction.toolCallId} title={interaction.title} options={interaction.options}
|
||||
status={interaction.status} error={interaction.error}
|
||||
labels={{ confirm: '允许一次', alwaysAllow: '始终允许', reject: '拒绝', working: '提交中', approved: '已允许', rejected: '已拒绝', cancelled: '已取消', failed: '提交失败' }}
|
||||
resolve={(id, decision) => onRespond?.(id, { kind: 'approval', optionId: decision.optionId })}
|
||||
reject={(id) => {
|
||||
const option = interaction.options.find((item) => item.kind.startsWith('reject'))
|
||||
return option ? onRespond?.(id, { kind: 'approval', optionId: option.optionId }) : undefined
|
||||
}} /> : interaction.answer && !message.tools?.some((tool) => tool.id === interaction.toolCallId && tool.metadata?.questionAnswer) ? <QuestionAnswerTranscript key={interaction.id} questionAnswer={interaction.answer} /> : null)}
|
||||
|
||||
{streaming && <StreamingFooter />}
|
||||
|
||||
{message.status === 'error' && message.error ? (
|
||||
<div className="msg__status msg__status--error">{message.error}</div>
|
||||
) : null}
|
||||
|
||||
{message.status === 'cancelled' ? <div className="cr-cancelled-indicator">{message.interrupted ? '上次运行已中断,可重新发送' : sourceConversationLabel('message.userCancelled')}</div> : null}
|
||||
</div>
|
||||
{!streaming && <div className="conversation-finished-footer">
|
||||
{!!artifactCount && onShowArtifacts && <SourceArtifactLink count={artifactCount} onClick={onShowArtifacts} />}
|
||||
<div className="_assistantFeedback_1jhpu_1">
|
||||
<MessageActionToolbar message={message} {...actions} />
|
||||
<AssistantMessageDetails message={message} />
|
||||
</div>
|
||||
</div>}
|
||||
{actionError && <p className="msg__status msg__status--error" role="alert">{actionError}</p>}
|
||||
</div></div></div></div></div>
|
||||
)
|
||||
}
|
||||
|
||||
export function planMarkdown(plan: PlanContent, selectedTaskId?: string | number): string {
|
||||
return [`# ${plan.title}`, '', ...plan.steps.map((step) => `- [${step.status === 'completed' ? 'x' : ' '}] ${String(selectedTaskId) === step.id ? `**${step.text}**` : step.text}`)].join('\n')
|
||||
}
|
||||
function planFromTool(result: unknown, args: unknown, id: string, title: string): PlanContent {
|
||||
const record = (value: unknown): Record<string, unknown> => value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
|
||||
if (typeof result === 'string') { try { result = JSON.parse(result) } catch { /* Text-only result */ } }
|
||||
const base = record(result)
|
||||
const envelope = record(base.result ?? result)
|
||||
let candidate = envelope.data ?? envelope.plan ?? envelope
|
||||
if (typeof candidate === 'string') { try { candidate = JSON.parse(candidate) } catch { /* Text-only plan */ } }
|
||||
const data = record(candidate)
|
||||
const input = record(args)
|
||||
const source = data.todolist ?? data.todos ?? data.steps ?? input.todolist ?? input.todos ?? input.steps
|
||||
return { id: String(data.id ?? input.id ?? id), title: String(data.name ?? data.title ?? input.name ?? input.title ?? title),
|
||||
filePath: typeof data.filePath === 'string' ? data.filePath : undefined,
|
||||
steps: (Array.isArray(source) ? source : []).map((item, index) => { const step = record(item); return { id: String(step.id ?? index), text: String(step.content ?? step.text ?? step.title ?? item), status: step.status === 'completed' ? 'completed' : step.status === 'in_progress' ? 'in_progress' : 'pending' } }) }
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
||||
import type { CustomModelInput, CustomModelProfile, ModelTestResult } from '@shared/models'
|
||||
import { modelLabel as t, sourceModelCatalog, sourceModelIcons } from './model-settings-source'
|
||||
import SourceSelect, { type SourceSelectGroup } from './source-select/SourceSelect'
|
||||
import SourceProviderIcon from './source-select/SourceProviderIcon'
|
||||
import './model-settings.css'
|
||||
|
||||
export interface ModelSettingsPanelProps {
|
||||
models: CustomModelProfile[]
|
||||
createRequest?: number
|
||||
onCreateHandled?(): void
|
||||
onSave(input: CustomModelInput, overwrite?: boolean): Promise<void>
|
||||
onDelete(id: string): Promise<void>
|
||||
onTest(input: CustomModelInput): Promise<ModelTestResult>
|
||||
}
|
||||
interface Provider {
|
||||
id: string; displayName: { zh: string; en: string }; groupId: string; vendor: string; icon?: string; iconUrl?: string
|
||||
authMode: string; endpointMode: string; endpoint?: string; modelInputMode: string
|
||||
advancedConfigMode: string; savePolicy: string; showCapabilitiesInForm: boolean
|
||||
defaultSupportsToolCall?: boolean; defaultSupportsImages?: boolean; defaultSupportsReasoning?: boolean
|
||||
namePrefix?: { zh: string; en: string }; disabledInRegions?: string[]; forceVisible?: boolean
|
||||
}
|
||||
interface ModelOption {
|
||||
providerId: string; model: string; displayName?: string; recommended?: boolean
|
||||
supportsTools?: boolean; supportsImages?: boolean; supportsReasoning?: boolean
|
||||
reasoning?: { defaultEffort?: string; supportedEfforts?: string[]; canDisableThinking?: boolean }
|
||||
}
|
||||
const providers: Provider[] = sourceModelCatalog.providers.filter((p) => !('disabledInRegions' in p) || !p.disabledInRegions?.includes('domestic'))
|
||||
const options: ModelOption[] = sourceModelCatalog.modelOptions
|
||||
const efforts = ['low', 'medium', 'high', 'xhigh', 'max']
|
||||
const effortNames: Record<string, string> = Object.fromEntries(efforts.map((value) => [value, t('settings.models.efforts.' + value)]))
|
||||
const prefix = 'models-settings-panel__'
|
||||
const cx = (name: string): string => prefix + name
|
||||
const message = (error: unknown): string => error instanceof Error ? error.message : String(error)
|
||||
const label = (name: string): string => t('settings.models.' + name)
|
||||
export function sanitizeModelApiKey(value: string): string { return value.replace(/\s+/gu, '') }
|
||||
export function isModelEndpointValid(value: string): boolean {
|
||||
if (!value.trim()) return true
|
||||
try { return ['http:', 'https:'].includes(new URL(value.trim()).protocol) } catch { return false }
|
||||
}
|
||||
export function parseModelTokenLimit(value: string): number | undefined {
|
||||
if (!value.trim()) return undefined
|
||||
const number = Number.parseInt(value, 10)
|
||||
return Number.isFinite(number) ? number : undefined
|
||||
}
|
||||
function defaultInput(providerId = 'tencent-token-plan'): CustomModelInput {
|
||||
const provider = providers.find((item) => item.id === providerId) ?? providers.find((item) => item.id === 'custom')!
|
||||
const candidates = options.filter((item) => item.providerId === provider.id)
|
||||
const option = candidates.find((item) => item.recommended) ?? candidates[0]
|
||||
return { id: '', name: '', providerId: provider.id, model: option?.model ?? '', baseUrl: provider.endpoint ?? '',
|
||||
authMode: provider.authMode === 'none' ? 'none' : 'api-key', apiKey: '',
|
||||
supportsToolCall: option?.supportsTools ?? provider.defaultSupportsToolCall ?? false,
|
||||
supportsImages: option?.supportsImages ?? provider.defaultSupportsImages ?? false,
|
||||
supportsReasoning: option?.supportsReasoning ?? provider.defaultSupportsReasoning ?? false,
|
||||
onlyReasoning: false, canDisableThinking: true, defaultEffort: option?.reasoning?.defaultEffort ?? '',
|
||||
supportedEfforts: option?.reasoning?.supportedEfforts ?? [], useCustomProtocol: false }
|
||||
}
|
||||
function Icon({ kind }: { kind: 'close' | 'edit' | 'delete' | 'link' | 'eye' | 'eyeClosed' | 'model' | 'down' | 'check' | 'loading' }): ReactNode {
|
||||
const source = sourceModelIcons[kind]
|
||||
if (source) return <svg {...source.props} width={kind === 'close' ? 12 : 16} height={kind === 'close' ? 12 : 16} aria-hidden="true" className={kind === 'loading' ? 'model-ui-spin' : undefined}>{source.paths.map((props, index) => <path {...props} key={index} />)}</svg>
|
||||
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.2" aria-hidden="true"><path d={kind === 'down' ? 'm4 6 4 4 4-4' : 'm3 8 3 3 7-7'} /></svg>
|
||||
}
|
||||
function Button({ children, variant = 'secondary', small, icon, className = '', ...props }: React.ButtonHTMLAttributes<HTMLButtonElement> & { variant?: string; small?: boolean; icon?: boolean }): React.JSX.Element {
|
||||
return <button type="button" {...props} className={['wb-button', 'wb-button--' + variant, small ? 'wb-button--small' : 'wb-button--medium', icon ? 'wb-button--icon-only' : '', className].join(' ')}>{children}</button>
|
||||
}
|
||||
function Input({ suffix, invalid, ...props }: React.InputHTMLAttributes<HTMLInputElement> & { suffix?: ReactNode; invalid?: boolean }): React.JSX.Element {
|
||||
return <span className={'wb-input wb-input--medium' + (invalid ? ' wb-input--error' : '')}><input {...props} className="wb-input__inner" aria-invalid={invalid || undefined} />{suffix}</span>
|
||||
}
|
||||
function Checkbox(props: React.InputHTMLAttributes<HTMLInputElement>): React.JSX.Element {
|
||||
return <span className="wb-checkbox wb-checkbox--medium"><span className="wb-checkbox__control"><input {...props} type="checkbox" className="wb-checkbox__input wb-checkbox__former" /><span className="wb-checkbox__box"><svg width="12" height="12" viewBox="0 0 12 12" className="wb-checkbox__icon" aria-hidden="true"><path d="m2.5 6 2.3 2.3 4.7-4.6" stroke="currentColor" strokeWidth="1.2" fill="none" /></svg></span></span></span>
|
||||
}
|
||||
|
||||
function Field({ name, children }: { name: string; children: ReactNode }): React.JSX.Element { return <div className={cx('field')}><label className={cx('label')}>{label('fields.' + name)}</label>{children}</div> }
|
||||
|
||||
export default function ModelSettingsPanel({ models, createRequest = 0, onCreateHandled, onSave, onDelete, onTest }: ModelSettingsPanelProps): React.JSX.Element {
|
||||
const [editing, setEditing] = useState<CustomModelProfile | null>(null), [open, setOpen] = useState(false)
|
||||
const [input, setInput] = useState<CustomModelInput>(() => defaultInput()), [inputTokens, setInputTokens] = useState(''), [outputTokens, setOutputTokens] = useState('')
|
||||
const [showKey, setShowKey] = useState(false), [saving, setSaving] = useState(false), [testing, setTesting] = useState(false), [deleting, setDeleting] = useState(false)
|
||||
const [rowTesting, setRowTesting] = useState<string | null>(null), [failed, setFailed] = useState<Set<string>>(new Set())
|
||||
const [error, setError] = useState(''), [testError, setTestError] = useState(''), [notice, setNotice] = useState(''), [deleteTarget, setDeleteTarget] = useState<CustomModelProfile | null>(null), [overwrite, setOverwrite] = useState<CustomModelInput | null>(null)
|
||||
const actionLock = useRef(false), listTestLock = useRef(false)
|
||||
const provider = providers.find((item) => item.id === input.providerId) ?? providers.find((item) => item.id === 'custom')!
|
||||
const keyInvalid = !!input.apiKey && !/^[\x21-\x7E]+$/.test(input.apiKey)
|
||||
const activeOptions = options.filter((item) => item.providerId === provider.id)
|
||||
const modelOptions = activeOptions.map((item) => ({ value: item.model, label: item.displayName || item.model }))
|
||||
const currentModel = input.model.trim()
|
||||
if (currentModel && !modelOptions.some((item) => item.value === currentModel)) modelOptions.unshift({ value: currentModel, label: currentModel })
|
||||
const providerOptions: SourceSelectGroup[] = [...sourceModelCatalog.providerGroups].sort((a, b) => a.order - b.order).flatMap((group) => {
|
||||
const members = providers.filter((item) => item.groupId === group.id)
|
||||
return members.length ? [{ key: group.id, label: group.label.zh, options: members.map((item) => ({ value: item.id,
|
||||
label: <span className={cx('provider-option')}><SourceProviderIcon provider={item} />{item.displayName.zh}</span>,
|
||||
searchText: item.displayName.zh + ' ' + item.displayName.en })) }] : []
|
||||
})
|
||||
const busy = saving || testing
|
||||
const update = (patch: Partial<CustomModelInput>): void => { setInput((current) => ({ ...current, ...patch })); setError(''); setTestError('') }
|
||||
const close = (): void => { if (busy || actionLock.current) return; setOpen(false); setOverwrite(null) }
|
||||
useEffect(() => { if (!open && !deleteTarget) return; const key = (event: KeyboardEvent): void => { if (event.key === 'Escape' && !event.defaultPrevented && !actionLock.current) { if (overwrite) setOverwrite(null); else if (deleteTarget) setDeleteTarget(null); else setOpen(false) } }; document.addEventListener('keydown', key); return () => document.removeEventListener('keydown', key) }, [open, deleteTarget, overwrite])
|
||||
useEffect(() => { if (!notice) return; const timer = window.setTimeout(() => setNotice(''), 3500); return () => clearTimeout(timer) }, [notice])
|
||||
const begin = (model?: CustomModelProfile): void => {
|
||||
setEditing(model ?? null); setInput(model ? { ...model, apiKey: '', previousId: model.id } : defaultInput()); setInputTokens(model?.maxInputTokens !== undefined ? String(model.maxInputTokens) : ''); setOutputTokens(model?.maxOutputTokens !== undefined ? String(model.maxOutputTokens) : '')
|
||||
setShowKey(false); setError(''); setTestError(''); setOverwrite(null); setOpen(true)
|
||||
if (model) setFailed((current) => { const next = new Set(current); next.delete(model.id); return next })
|
||||
}
|
||||
const changeProvider = (id: string): void => { setEditing(null); setInput(defaultInput(id)); setInputTokens(''); setOutputTokens(''); setShowKey(false); setError(''); setTestError('') }
|
||||
useEffect(() => { if (createRequest > 0) { begin(); onCreateHandled?.() } }, [createRequest])
|
||||
const changeModel = (value: string): void => {
|
||||
const option = activeOptions.find((item) => item.model === value)
|
||||
update({ model: value, supportsToolCall: option?.supportsTools ?? input.supportsToolCall, supportsImages: option?.supportsImages ?? input.supportsImages,
|
||||
supportsReasoning: option?.supportsReasoning ?? input.supportsReasoning, ...(option?.reasoning ? { defaultEffort: option.reasoning.defaultEffort ?? '', supportedEfforts: option.reasoning.supportedEfforts ?? [], canDisableThinking: option.reasoning.canDisableThinking ?? true } : {}) })
|
||||
}
|
||||
const build = (): CustomModelInput | null => {
|
||||
const model = input.model.trim(), baseUrl = (provider.endpointMode === 'readonly' ? provider.endpoint : input.baseUrl)?.trim() || '', apiKey = sanitizeModelApiKey(input.apiKey ?? '')
|
||||
const fail = (key: string): null => { setError(label('toast.' + key)); return null }
|
||||
if (!provider) return fail('selectProvider')
|
||||
if (!model) return fail('modelRequired')
|
||||
if (!baseUrl) return fail('endpointRequired')
|
||||
if (provider.endpointMode === 'editable' && !isModelEndpointValid(baseUrl)) return fail('endpointInvalid')
|
||||
if (provider.authMode !== 'none' && !apiKey && !editing?.hasApiKey) return fail('apiKeyRequired')
|
||||
if (provider.authMode !== 'none' && apiKey && !/^[\x21-\x7E]+$/.test(apiKey)) return fail('apiKeyInvalid')
|
||||
const option = activeOptions.find((item) => item.model === model)
|
||||
const display = option?.displayName || model
|
||||
const result: CustomModelInput = { ...input, id: model, name: provider.namePrefix?.zh ? provider.namePrefix.zh + ' / ' + display : display, providerId: provider.id, model, baseUrl,
|
||||
authMode: provider.authMode === 'none' ? 'none' : 'api-key', apiKey: provider.authMode === 'none' ? undefined : apiKey || undefined,
|
||||
clearApiKey: provider.authMode === 'none' || undefined, previousId: editing?.id,
|
||||
maxInputTokens: parseModelTokenLimit(inputTokens), maxOutputTokens: parseModelTokenLimit(outputTokens) }
|
||||
if (provider.savePolicy === 'autoFromModel') { result.supportsToolCall = option?.supportsTools ?? true; result.supportsImages = option?.supportsImages ?? false; result.supportsReasoning = option?.supportsReasoning ?? false; result.defaultEffort = option?.reasoning?.defaultEffort; result.supportedEfforts = option?.reasoning?.supportedEfforts }
|
||||
if (!result.supportsReasoning) { result.onlyReasoning = false; result.defaultEffort = undefined; result.supportedEfforts = undefined; result.canDisableThinking = true }
|
||||
return result
|
||||
}
|
||||
const save = async (data: CustomModelInput, replace = false): Promise<void> => {
|
||||
if (actionLock.current) return
|
||||
actionLock.current = true; setSaving(true); setError('')
|
||||
try { await onSave(data, replace); setOpen(false); setEditing(null); setOverwrite(null) } catch (reason) { setError(message(reason)); setOverwrite(null) } finally { actionLock.current = false; setSaving(false) }
|
||||
}
|
||||
const submit = (): void => { if (actionLock.current) return; const data = build(); if (!data) return; if (models.some((model) => model.id === data.id && model.id !== editing?.id)) { setOverwrite(data); return } void save(data) }
|
||||
const test = async (): Promise<void> => {
|
||||
if (actionLock.current) return
|
||||
const data = build(); if (!data) return
|
||||
actionLock.current = true; setTesting(true); setTestError('')
|
||||
try { const result = await onTest(data); if (result.success) setNotice(label('toast.testConnectionSuccess')); else { setTestError(result.message); setNotice(label('toast.testConnectionFailed')) } } catch (reason) { setTestError(message(reason)) } finally { actionLock.current = false; setTesting(false) }
|
||||
}
|
||||
const testRow = async (model: CustomModelProfile): Promise<void> => {
|
||||
if (listTestLock.current) return
|
||||
listTestLock.current = true; setRowTesting(model.id)
|
||||
try { const result = await onTest({ ...model, previousId: model.id }); setFailed((current) => { const next = new Set(current); if (result.success) next.delete(model.id); else next.add(model.id); return next }); setNotice(result.success ? label('toast.testConnectionSuccess') : result.message || label('toast.testConnectionFailed')) } catch (reason) { setFailed((current) => new Set(current).add(model.id)); setNotice(message(reason)) } finally { listTestLock.current = false; setRowTesting(null) }
|
||||
}
|
||||
const remove = async (): Promise<void> => {
|
||||
if (!deleteTarget || actionLock.current) return
|
||||
actionLock.current = true; setDeleting(true); setError('')
|
||||
try { await onDelete(deleteTarget.id); setDeleteTarget(null) } catch (reason) { setError(message(reason)) } finally { actionLock.current = false; setDeleting(false) }
|
||||
}
|
||||
const checkbox = (key: 'supportsToolCall' | 'supportsImages' | 'supportsReasoning' | 'onlyReasoning' | 'canDisableThinking' | 'useCustomProtocol', name: string): ReactNode => <label className={cx('checkbox-item')} title={label('capability.' + name + 'Description')}><Checkbox checked={!!input[key]} onChange={(event) => update({ [key]: event.target.checked, ...(key === 'supportsReasoning' && !event.target.checked ? { onlyReasoning: false, canDisableThinking: true, defaultEffort: '', supportedEfforts: [] } : {}) })} /><span className={cx('checkbox-title')}>{label('capability.' + name)}</span></label>
|
||||
return <div className="models-settings-panel" data-model-settings>
|
||||
{notice && <div className="model-ui-toast" role="status">{notice}</div>}
|
||||
{!open && !deleteTarget && error && <div className="model-ui-error" role="alert">{error}</div>}
|
||||
<section className={cx('section')}><div className={cx('card') + ' ' + cx('model-card')}>
|
||||
<div className={cx('card-row')}><div className={cx('card-left')}><div className={cx('card-label')}>{label('title')}</div><div className={cx('card-desc')}>管理本机自定义模型,配置保存在本地。</div></div><div className={cx('card-right')}><Button className={cx('add-button')} onClick={() => begin()}>{label('add')}</Button></div></div>
|
||||
{models.length > 0 && <><hr className={cx('divider')} /><div className={cx('list')}>{[...models].sort((a, b) => a.providerId.localeCompare(b.providerId) || a.name.localeCompare(b.name)).map((model) => <div className={cx('row')} key={model.id}>
|
||||
<div className={cx('row-main')}><div className={cx('row-icon')}><SourceProviderIcon provider={providers.find((item) => item.id === model.providerId)} /></div><div className={cx('row-text')}><span className={cx('row-name')}>{model.name || model.model}</span><span className={cx('row-provider')}>{providers.find((item) => item.id === model.providerId)?.displayName.zh || model.providerId}</span>{failed.has(model.id) && <span className={cx('connection-failed-badge')}>{label('toast.testConnectionFailed')}</span>}</div></div>
|
||||
<div className={cx('actions')}><Button variant="ghost" small icon className={cx('action-button')} title={t('common.edit')} aria-label={t('common.edit')} onClick={() => begin(model)}><Icon kind="edit" /></Button><Button variant="ghost" small icon className={cx('action-button')} title={label('actions.testConnection')} aria-label={label('actions.testConnection')} disabled={rowTesting !== null} onClick={() => void testRow(model)}><Icon kind={rowTesting === model.id ? 'loading' : 'link'} /></Button><Button variant="ghost" small icon className={cx('action-button')} title={t('common.delete')} aria-label={t('common.delete')} onClick={() => { setError(''); setDeleteTarget(model) }}><Icon kind="delete" /></Button></div>
|
||||
</div>)}</div></>}
|
||||
</div></section>
|
||||
{open && <div className={cx('editor-overlay')}><section className={cx('editor')} role="dialog" aria-modal="true" aria-label={label(editing ? 'modal.editTitle' : 'modal.addTitle')}>
|
||||
<div className={cx('editor-header')}><div className={cx('editor-title-group')}><div className={cx('editor-title')}>{label(editing ? 'modal.editTitle' : 'modal.addTitle')}</div></div><Button variant="ghost" small icon className={cx('editor-close')} disabled={busy} aria-label={t('common.close')} onClick={close}><Icon kind="close" /></Button></div>
|
||||
<div className={cx('editor-body')}>
|
||||
{error && <div className="model-ui-error" role="alert">{error}</div>}
|
||||
<div className={cx('field-grid') + ' ' + cx('field-grid--provider')}><Field name="provider"><SourceSelect key={'provider-' + (editing?.id ?? 'new') + '-' + provider.id} value={provider.id} onChange={changeProvider} options={providerOptions} showSearch={providers.length > 6} fullWidth placeholder={label('fields.providerPlaceholder')} ariaLabel={label('fields.provider')} className={cx('provider-select')} /></Field>
|
||||
{provider.endpointMode === 'editable' && <Field name="endpoint"><Input aria-label={label('fields.endpoint')} value={input.baseUrl} invalid={!isModelEndpointValid(input.baseUrl)} onChange={(event) => update({ baseUrl: event.target.value })} placeholder={label('fields.endpointPlaceholder')} /></Field>}
|
||||
</div>
|
||||
<div className={cx('model-form-fields')}>
|
||||
{provider.authMode !== 'none' ? <div className={cx('field') + ' ' + cx('field--api-key')}><label className={cx('label')}>{label('fields.apiKey')}</label><div className={cx('api-key-row')}><Input aria-label={label('fields.apiKey')} value={input.apiKey ?? ''} type={showKey ? 'text' : 'password'} invalid={keyInvalid} autoComplete="off" spellCheck={false} onChange={(event) => update({ apiKey: sanitizeModelApiKey(event.target.value) })} placeholder={editing?.hasApiKey ? '已配置,留空保持不变' : label('fields.apiKeyPlaceholder')} suffix={<Button variant="ghost" small icon className={cx('input-icon-button')} aria-label={label(showKey ? 'actions.hide' : 'actions.show')} onClick={() => setShowKey(!showKey)}><Icon kind={showKey ? "eye" : "eyeClosed"} /></Button>} /><Button className={cx('editor-test-connection')} disabled={busy} onClick={() => void test()}>{testing && <Icon kind="loading" />}{label(testing ? 'actions.testConnectionTesting' : 'actions.testConnection')}</Button></div><div className={cx('api-key-feedback')} role={keyInvalid || testError ? 'alert' : undefined}>{keyInvalid ? label('validation.apiKeyCharacters') : testError}</div></div> : <div className={cx('hint')}>{label('fields.noApiKeyNeeded')}</div>}
|
||||
<Field name="modelId">{provider.modelInputMode === 'selectOrInput' && activeOptions.length ? <SourceSelect key={provider.id} value={input.model} onChange={changeModel} options={modelOptions} showSearch={activeOptions.length > 6} fullWidth placeholder={label('fields.modelIdPlaceholder')} searchPlaceholder={label('modal.modelSearchPlaceholder')} viewportPadding={{ top: 56 }} ariaLabel={label('fields.modelId')} className={cx('model-select')} /> : <Input aria-label={label('fields.modelId')} value={input.model} onChange={(event) => changeModel(event.target.value)} placeholder={label('fields.modelIdPlaceholder')} />}</Field>
|
||||
{provider.advancedConfigMode !== 'hidden' && <div className={cx('advanced')}><div className={cx('advanced-heading')}>{label('fields.advanced')}</div><div className={cx('advanced-body')}>
|
||||
{provider.showCapabilitiesInForm && <div className={cx('checkbox-grid') + ' ' + cx('checkbox-grid--inline')}>{checkbox('supportsToolCall', 'tools')}{checkbox('supportsImages', 'images')}{checkbox('supportsReasoning', 'reasoning')}{input.supportsReasoning && <>{checkbox('onlyReasoning', 'onlyReasoning')}{checkbox('canDisableThinking', 'canDisableThinking')}</>}{checkbox('useCustomProtocol', 'customProtocol')}</div>}
|
||||
{provider.showCapabilitiesInForm && input.supportsReasoning && <>{!input.supportedEfforts?.length && <div className={cx('hint')}>{label('fields.supportedEffortsEmptyHint')}</div>}<Field name="defaultEffort"><SourceSelect fullWidth value={input.defaultEffort || ''} onChange={(value) => update({ defaultEffort: value })} options={[{ value: '', label: label('fields.defaultEffortAuto') }, ...efforts.map((value) => ({ value, label: effortNames[value] }))]} ariaLabel={label('fields.defaultEffort')} className={cx('effort-select')} /></Field><Field name="supportedEfforts"><div className={cx('checkbox-grid')}>{efforts.map((value) => <label className={cx('checkbox-item')} key={value}><Checkbox checked={input.supportedEfforts?.includes(value) ?? false} onChange={(event) => update({ supportedEfforts: event.target.checked ? [...(input.supportedEfforts ?? []), value] : input.supportedEfforts?.filter((item) => item !== value) })} /><span className={cx('checkbox-title')}>{effortNames[value]}</span></label>)}</div></Field></>}
|
||||
<div className={cx('number-grid')}>{[{ name: 'inputTokensShort', value: inputTokens, set: setInputTokens, presets: [32768, 65536, 131072, 262144] }, { name: 'outputTokensShort', value: outputTokens, set: setOutputTokens, presets: [8192, 16384, 32768, 65536] }].map((item) => <Field name={item.name} key={item.name}><Input type="number" min="0" step="1" inputMode="numeric" aria-label={label('fields.' + item.name)} value={item.value} onChange={(event) => item.set(event.target.value)} placeholder={label('fields.tokenLimitPlaceholder')} /><div className={cx('quick-values')}>{item.presets.map((value) => <Button small variant={item.value === String(value) ? 'secondary' : 'ghost'} className={cx('quick-value')} key={value} onClick={() => item.set(String(value))}>{value / 1024}K</Button>)}</div></Field>)}</div>
|
||||
</div></div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('editor-footer')}><Button className={cx('editor-cancel')} disabled={busy} onClick={close}>{t('common.cancel')}</Button><Button variant="primary" className={cx('editor-save')} disabled={busy} aria-busy={saving} onClick={submit}>{saving && <Icon kind="loading" />}{t('common.save')}</Button></div>
|
||||
</section></div>}
|
||||
{(deleteTarget || overwrite) && <div className={cx('delete-overlay')} onClick={() => { if (!deleting && !saving) { setDeleteTarget(null); setOverwrite(null) } }}><section className={cx('delete-dialog')} role="dialog" aria-modal="true" aria-label={label(overwrite ? 'overwrite.title' : 'delete.title')} onClick={(event) => event.stopPropagation()}>
|
||||
<div className={cx('delete-title')}>{label(overwrite ? 'overwrite.title' : 'delete.title')}</div><div className={cx('delete-content')}>{t('settings.models.' + (overwrite ? 'overwrite.description' : 'delete.description'), { model: overwrite?.model || deleteTarget?.name || '' })}</div>{error && deleteTarget && <div className="model-ui-error" role="alert">{error}</div>}
|
||||
<div className={cx('delete-footer')}><Button className={cx('delete-cancel')} disabled={deleting || saving} onClick={() => { setDeleteTarget(null); setOverwrite(null) }}>{t('common.cancel')}</Button><Button variant="primary" className={cx('delete-action')} disabled={deleting || saving} onClick={() => overwrite ? void save(overwrite, true) : void remove()}>{label(overwrite ? 'overwrite.confirm' : 'delete.title')}</Button></div>
|
||||
</section></div>}
|
||||
</div>
|
||||
}
|
||||
|
||||
export { ModelSettingsPanel }
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { IconClose } from './icons'
|
||||
import { MOCK_NOTIFICATIONS } from '../mock'
|
||||
|
||||
interface Props {
|
||||
onClose(): void
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知面板。
|
||||
*
|
||||
* ⚠️ 数据来自 `mock.ts`(MOCK_NOTIFICATIONS),**不是真实通知**。
|
||||
* 面板底部明确标注 mock,避免误认为已接入消息中心。
|
||||
*/
|
||||
export default function NotificationsPanel({ onClose }: Props): React.JSX.Element {
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const onDown = (e: MouseEvent): void => {
|
||||
if (!rootRef.current?.contains(e.target as Node)) onClose()
|
||||
}
|
||||
const onKey = (e: KeyboardEvent): void => {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
document.addEventListener('mousedown', onDown)
|
||||
document.addEventListener('keydown', onKey)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onDown)
|
||||
document.removeEventListener('keydown', onKey)
|
||||
}
|
||||
}, [onClose])
|
||||
|
||||
return (
|
||||
<div className="notif-panel" ref={rootRef}>
|
||||
<div className="notif-panel__head">
|
||||
<span className="notif-panel__title">通知</span>
|
||||
<button className="btn-icon" title="关闭" onClick={onClose}>
|
||||
<IconClose />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="notif-panel__body">
|
||||
{MOCK_NOTIFICATIONS.map((n) => (
|
||||
<div className="notif-item" key={n.id} data-unread={n.unread}>
|
||||
<div className="notif-item__head">
|
||||
<span className="notif-item__title">{n.title}</span>
|
||||
<span className="notif-item__time">{n.time}</span>
|
||||
</div>
|
||||
<div className="notif-item__body">{n.body}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="notif-panel__foot">示例数据 · 尚未接入消息中心</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useResourceStore } from '../stores/resources'
|
||||
import { ImagePreview } from '../workbuddy/images/ImagePreview'
|
||||
import '../styles/resource-preview.css'
|
||||
import { ArtifactHostPreview } from '../workbuddy/artifacts/ArtifactHostPreview'
|
||||
|
||||
/** YCode host adapter for the restored path-click contract; original file-editor UI remains unverified. */
|
||||
export default function ResourcePreview({ onClose }: { onClose(): void }): React.JSX.Element {
|
||||
const { resource, path, loading, error, open, cacheArtifactContent } = useResourceStore()
|
||||
const highlighted = useRef<HTMLDivElement>(null)
|
||||
const [imageOpen, setImageOpen] = useState(false)
|
||||
useEffect(() => { setImageOpen(false) }, [path])
|
||||
useEffect(() => { highlighted.current?.scrollIntoView({ block: 'center' }) }, [resource])
|
||||
return <aside className="right-sidebar resource-preview" aria-label="文件预览">
|
||||
<header className="right-panel__top">
|
||||
<span className="resource-preview__title" title={path ?? ''}>{resource?.name ?? '文件预览'}</span>
|
||||
<button className="right-panel__icon-btn" title="关闭文件预览" aria-label="关闭文件预览" onClick={onClose}>×</button>
|
||||
</header>
|
||||
<div className={`resource-preview__body${resource?.kind === 'artifact' ? ' resource-preview__body--artifact' : ''}`}>
|
||||
{loading ? <p role="status">正在读取…</p> : null}
|
||||
{error ? <p role="alert">{error}</p> : null}
|
||||
{resource?.kind === 'artifact' ? <ArtifactHostPreview key={resource.artifact?.id || resource.path}
|
||||
artifact={resource.artifact ?? { id: resource.path, type: resource.artifactType || 'overview', title: resource.name, uri: resource.path, content: resource.content }}
|
||||
darkMode={document.documentElement.dataset.theme === 'dark'} onCacheUpdate={cacheArtifactContent} /> : null}
|
||||
{resource?.kind === 'text' ? <>
|
||||
<div className="resource-preview__path" title={resource.path}>{resource.path}</div>
|
||||
<pre className="resource-preview__text">{resource.content.split('\n').map((line, index) => {
|
||||
const lineNumber = resource.firstLine + index
|
||||
const selected = !!resource.range && lineNumber >= resource.range.start && lineNumber <= (resource.range.end ?? resource.range.start)
|
||||
return <div key={lineNumber} ref={lineNumber === resource.range?.start ? highlighted : undefined} className="resource-preview__line" data-line={lineNumber} data-highlighted={selected || undefined}>
|
||||
<span className="resource-preview__line-number" aria-hidden="true">{lineNumber}</span><span>{line || ' '}</span>
|
||||
</div>
|
||||
})}</pre>
|
||||
{resource.truncated ? <p className="resource-preview__note">共 {resource.totalLines} 行,当前显示 {resource.firstLine}–{resource.firstLine + resource.content.split('\n').length - 1} 行</p> : null}
|
||||
</> : null}
|
||||
{resource?.kind === 'directory' ? <>
|
||||
<div className="resource-preview__path" title={resource.path}>{resource.path}</div>
|
||||
<div className="resource-preview__entries">{resource.entries.map((entry) => <button type="button" key={entry.path} onClick={() => void open(entry.path)}>{entry.type === 'directory' ? '▸ ' : ''}{entry.name}{entry.type === 'directory' ? '/' : ''}</button>)}</div>
|
||||
{!resource.entries.length ? <p>空目录</p> : null}
|
||||
{resource.truncated ? <p>仅显示前 500 项</p> : null}
|
||||
</> : null}
|
||||
{resource?.kind === 'image' ? <button type="button" className="resource-preview__image" onClick={() => setImageOpen(true)} aria-label={`预览 ${resource.name}`}><img src={resource.url} alt={resource.name} /></button> : null}
|
||||
</div>
|
||||
{imageOpen && resource?.kind === 'image' ? <ImagePreview urlList={[resource.url]} initialIndex={0} onClose={() => setImageOpen(false)} onDownload={async () => { await window.ycode.resources.downloadImage(resource.url, resource.name) }} /> : null}
|
||||
</aside>
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { SourceWorkbench, type SourceWorkbenchProps } from '../workbuddy/workbench/SourceWorkbench'
|
||||
|
||||
/** Original SidebarNext shell with YCode resources and host services. */
|
||||
export default function RightSidebar(props: SourceWorkbenchProps): React.JSX.Element {
|
||||
return <SourceWorkbench {...props} />
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
import ModelSettingsPanel from './ModelSettingsPanel'
|
||||
import SourceSettingsNavigation from './SourceSettingsNavigation'
|
||||
import { useSharedTheme } from '../useSharedTheme'
|
||||
import './settings-shell-source.css'
|
||||
|
||||
interface Props {
|
||||
theme: 'light' | 'dark'
|
||||
onThemeChange(theme: 'light' | 'dark'): void
|
||||
onClose(): void
|
||||
nativeWindow?: boolean
|
||||
}
|
||||
|
||||
export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWindow = false }: Props): React.JSX.Element {
|
||||
const settings = useSettingsStore()
|
||||
const [section, setSection] = useState(new URLSearchParams(location.search).get('section') ?? 'model')
|
||||
const [createRequest, setCreateRequest] = useState(() => new URLSearchParams(location.search).get('action') === 'new' ? 1 : 0)
|
||||
const [error, setError] = useState('')
|
||||
useEffect(() => {
|
||||
if (!nativeWindow) return
|
||||
return window.ycode.app.onSettingsNavigate?.((next, action) => {
|
||||
setError(''); setSection(next === 'models' ? 'model' : next)
|
||||
if (action === 'new') setCreateRequest(value => value + 1)
|
||||
})
|
||||
}, [nativeWindow])
|
||||
useEffect(() => {
|
||||
const escape = (event: KeyboardEvent): void => {
|
||||
if (event.key === 'Escape' && !document.querySelector('.models-settings-panel__editor-overlay')) onClose()
|
||||
}
|
||||
window.addEventListener('keydown', escape)
|
||||
return () => window.removeEventListener('keydown', escape)
|
||||
}, [onClose])
|
||||
const modal = <section className={`settings-modal${nativeWindow ? ' settings-modal--window' : ''}`} role="dialog" aria-label="设置" aria-modal={!nativeWindow}>
|
||||
<div className="settings-modal__nav"><SourceSettingsNavigation activeTab={section} supportedTabs={['models', 'appearance']}
|
||||
onTabChange={(id) => { setError(''); setSection(id === 'models' ? 'model' : id) }} onUnavailable={() => setError('该设置尚未接入')} /></div>
|
||||
<div className="settings-modal__content"><div className="settings-modal__panel">
|
||||
{error && <p role="alert">{error}</p>}
|
||||
{section === 'model' && <ModelSettingsPanel models={settings.models} createRequest={createRequest} onCreateHandled={() => setCreateRequest(0)} onSave={settings.saveModel} onDelete={settings.deleteModel} onTest={(input) => window.ycode.models.test(input)} />}
|
||||
{section === 'workspace' && <div className="settings-window-panel"><h2>工作空间</h2>{settings.workspaces.map((space) => <button className="btn" key={space.id} title={space.root} onClick={() => void window.ycode.workspace.open(space.id).catch((error) => setError(String(error.message ?? error)))}>{space.name}</button>)}<button className="btn" onClick={() => void settings.attachWorkspace().catch((error) => setError(String(error.message ?? error)))}>打开已有文件夹</button></div>}
|
||||
{section === 'appearance' && <div className="settings-window-panel"><h2>外观</h2><div className="segmented">{(['light', 'dark'] as const).map((value) => <button key={value} className="segmented__item" data-active={theme === value} onClick={() => onThemeChange(value)}>{value === 'light' ? '浅色' : '深色'}</button>)}</div></div>}
|
||||
{section === 'about' && <div className="settings-window-panel"><h2>YCode</h2><p>{settings.appInfo?.version}</p></div>}
|
||||
</div></div>
|
||||
</section>
|
||||
if (nativeWindow) return <main className="settings-window-root"><div className="settings-window-chrome settings-window-chrome--windows"><button className="settings-window-close" aria-label="关闭设置" onClick={onClose}>×</button></div>{modal}</main>
|
||||
return <div className="overlay" onClick={onClose}><div onClick={(event) => event.stopPropagation()}>{modal}</div></div>
|
||||
}
|
||||
|
||||
export function SettingsWindow(): React.JSX.Element {
|
||||
const [theme, setTheme] = useSharedTheme()
|
||||
const [error, setError] = useState('')
|
||||
useEffect(() => { void useSettingsStore.getState().load().catch((error) => setError(String(error.message ?? error))) }, [])
|
||||
return <>{error && <p role="alert">{error}</p>}<SettingsDialog nativeWindow theme={theme} onThemeChange={setTheme} onClose={() => void window.ycode.window.close()} /></>
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import type { SessionMeta } from '@shared/types'
|
||||
import type { WorkspaceRecord } from '@shared/workspaces'
|
||||
import { useChatStore } from '../stores/chat'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
import {
|
||||
IconBell,
|
||||
IconChevronDown,
|
||||
IconClose,
|
||||
IconCompact,
|
||||
IconDiscover,
|
||||
IconMiniprogram,
|
||||
IconNavAssistant,
|
||||
IconNavExperts,
|
||||
IconNavLibrary,
|
||||
IconNavMore,
|
||||
IconNavScheduled,
|
||||
IconNewTask,
|
||||
IconSectionAdd
|
||||
} from './icons'
|
||||
import { IconSearch } from './icons-extra'
|
||||
import NotificationsPanel from './NotificationsPanel'
|
||||
import WorkspaceDialog from './WorkspaceDialog'
|
||||
import { MOCK_ACCOUNT, MOCK_UNREAD_COUNT, NAV_ITEMS } from '../mock'
|
||||
import './workspace-source.css'
|
||||
import './sidebar-shell-source.css'
|
||||
|
||||
interface WorkspacePreferences { tasksOpen: boolean; spacesOpen: boolean; collapsed: Record<string, boolean>; visible: Record<string, number> }
|
||||
const PREFS_KEY = 'ycode.sidebar.workspace-groups.v1'
|
||||
function readPreferences(): WorkspacePreferences {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(PREFS_KEY) ?? '{}') as Partial<WorkspacePreferences>
|
||||
return { tasksOpen: saved.tasksOpen !== false, spacesOpen: saved.spacesOpen !== false,
|
||||
collapsed: saved.collapsed && typeof saved.collapsed === 'object' ? saved.collapsed : {},
|
||||
visible: saved.visible && typeof saved.visible === 'object' ? saved.visible : {} }
|
||||
} catch { return { tasksOpen: true, spacesOpen: true, collapsed: {}, visible: {} } }
|
||||
}
|
||||
|
||||
export function groupWorkspaceSessions(sessions: SessionMeta[], workspaces: WorkspaceRecord[]): { independent: SessionMeta[]; groups: Map<string, SessionMeta[]> } {
|
||||
const groups = new Map(workspaces.map(space => [space.id, [] as SessionMeta[]]))
|
||||
const independent: SessionMeta[] = []
|
||||
for (const session of sessions) {
|
||||
const owned = session.workspaceId ? groups.get(session.workspaceId) : undefined
|
||||
if (owned) owned.push(session)
|
||||
else independent.push(session)
|
||||
}
|
||||
return { independent, groups }
|
||||
}
|
||||
|
||||
function relTime(ts: number): string {
|
||||
const diff = Date.now() - ts
|
||||
const min = Math.floor(diff / 60000)
|
||||
if (min < 1) return '刚刚'
|
||||
if (min < 60) return `${min}分钟前`
|
||||
const hour = Math.floor(min / 60)
|
||||
if (hour < 24) return `${hour}小时前`
|
||||
const day = Math.floor(hour / 24)
|
||||
if (day < 30) return `${day}天前`
|
||||
return new Date(ts).toLocaleDateString()
|
||||
}
|
||||
|
||||
/** 导航项图标映射 */
|
||||
const NAV_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
'new-task': IconNewTask,
|
||||
assistant: IconNavAssistant,
|
||||
experts: IconNavExperts,
|
||||
scheduled: IconNavScheduled,
|
||||
library: IconNavLibrary,
|
||||
more: IconNavMore
|
||||
}
|
||||
|
||||
interface Props {
|
||||
activeRoute?: 'chat' | 'capabilities'
|
||||
onNavigate?(route: 'chat' | 'capabilities'): void
|
||||
collapsed: boolean
|
||||
onToggleCollapse(): void
|
||||
}
|
||||
|
||||
export default function Sidebar({ collapsed, onToggleCollapse, activeRoute = 'chat', onNavigate }: Props): React.JSX.Element {
|
||||
const sessions = useChatStore((s) => s.sessions)
|
||||
const currentId = useChatStore((s) => s.currentSessionId)
|
||||
const runs = useChatStore((s) => s.runs)
|
||||
const sendingSessions = useChatStore((s) => s.sendingSessions)
|
||||
const selectSession = useChatStore((s) => s.selectSession)
|
||||
const startTask = useChatStore((s) => s.startTask)
|
||||
const removeSession = useChatStore((s) => s.removeSession)
|
||||
const workspaces = useSettingsStore((s) => s.workspaces)
|
||||
const selectedWorkspaceId = useSettingsStore((s) => s.selectedWorkspaceId)
|
||||
const selectedModelId = useSettingsStore((s) => s.selectedModelId)
|
||||
const createWorkspace = useSettingsStore((s) => s.createWorkspace)
|
||||
const selectWorkspace = useSettingsStore((s) => s.selectWorkspace)
|
||||
const renameWorkspace = useSettingsStore((s) => s.renameWorkspace)
|
||||
const removeWorkspace = useSettingsStore((s) => s.removeWorkspace)
|
||||
|
||||
const [query, setQuery] = useState('')
|
||||
const [searchVisible, setSearchVisible] = useState(false)
|
||||
const [preferences, setPreferences] = useState(readPreferences)
|
||||
const { tasksOpen, spacesOpen } = preferences
|
||||
const [workspaceDialog, setWorkspaceDialog] = useState<{ mode: 'create' | 'rename' | 'remove'; space?: WorkspaceRecord } | null>(null)
|
||||
const [spaceMenu, setSpaceMenu] = useState<{ space: WorkspaceRecord; top: number; left: number; anchorTop: number } | null>(null)
|
||||
const spaceMenuRef = useRef<HTMLDivElement>(null)
|
||||
const taskStartLock = useRef(false)
|
||||
const [notifOpen, setNotifOpen] = useState(false)
|
||||
const [unread, setUnread] = useState(MOCK_UNREAD_COUNT)
|
||||
const [hint, setHint] = useState<string | null>(null)
|
||||
const hintTimer = useRef<number | null>(null)
|
||||
const searchRef = useRef<HTMLInputElement>(null)
|
||||
useEffect(() => { try { localStorage.setItem(PREFS_KEY, JSON.stringify(preferences)) } catch { /* Optional view preference. */ } }, [preferences])
|
||||
useEffect(() => {
|
||||
if (!spaceMenu) return
|
||||
const close = (event: KeyboardEvent): void => { if (event.key === 'Escape') setSpaceMenu(null) }
|
||||
window.addEventListener('keydown', close)
|
||||
return () => window.removeEventListener('keydown', close)
|
||||
}, [spaceMenu])
|
||||
useLayoutEffect(() => {
|
||||
if (!spaceMenu || !spaceMenuRef.current) return
|
||||
const rect = spaceMenuRef.current.getBoundingClientRect()
|
||||
const left = Math.max(4, Math.min(spaceMenu.left, window.innerWidth - rect.width - 4))
|
||||
const top = rect.bottom > window.innerHeight - 4 ? Math.max(4, spaceMenu.anchorTop - rect.height - 4) : spaceMenu.top
|
||||
if (left !== spaceMenu.left || top !== spaceMenu.top) setSpaceMenu({ ...spaceMenu, left, top })
|
||||
}, [spaceMenu])
|
||||
const toggleTasks = (): void => setPreferences(value => ({ ...value, tasksOpen: !value.tasksOpen }))
|
||||
const toggleSpaces = (): void => setPreferences(value => ({ ...value, spacesOpen: !value.spacesOpen }))
|
||||
const toggleGroup = (id: string): void => setPreferences(value => ({ ...value, collapsed: { ...value.collapsed, [id]: !value.collapsed[id] }, visible: { ...value.visible, [id]: 5 } }))
|
||||
const newTask = async (space?: WorkspaceRecord): Promise<void> => {
|
||||
if (!space && activeRoute === 'capabilities' && currentId === null) { onNavigate?.('chat'); return }
|
||||
if (taskStartLock.current) return
|
||||
taskStartLock.current = true
|
||||
try {
|
||||
if (space) await selectWorkspace(space.id)
|
||||
startTask({ ...(space ? { workspaceId: space.id, cwd: space.root } : {}), ...(selectedModelId ? { modelId: selectedModelId } : {}) })
|
||||
onNavigate?.('chat')
|
||||
} catch (error) { setHint(error instanceof Error ? error.message : '无法新建任务') }
|
||||
finally { taskStartLock.current = false }
|
||||
}
|
||||
const openSpaceMenu = (space: WorkspaceRecord, rect: { left: number; bottom: number; top: number }, moreButton = false): void => {
|
||||
setSpaceMenu({ space, left: rect.left - (moreButton ? 16 : 0), top: rect.bottom + 4, anchorTop: rect.top })
|
||||
}
|
||||
|
||||
// 标题栏「搜索」按钮 → 聚焦本侧栏搜索框
|
||||
useEffect(() => {
|
||||
const onFocusSearch = (): void => {
|
||||
setSearchVisible(true)
|
||||
requestAnimationFrame(() => { searchRef.current?.focus(); searchRef.current?.select() })
|
||||
}
|
||||
window.addEventListener('ycode:focus-search', onFocusSearch)
|
||||
return () => window.removeEventListener('ycode:focus-search', onFocusSearch)
|
||||
}, [])
|
||||
|
||||
// 全局「尚未实现」提示(标题栏等外围入口共用同一反馈通道)
|
||||
useEffect(() => {
|
||||
const onNotice = (e: Event): void => {
|
||||
const detail = (e as CustomEvent<string>).detail
|
||||
setHint(detail ? `「${detail}」尚未实现` : '该功能尚未实现')
|
||||
}
|
||||
window.addEventListener('ycode:not-implemented', onNotice)
|
||||
return () => window.removeEventListener('ycode:not-implemented', onNotice)
|
||||
}, [])
|
||||
|
||||
// 临时提示(未实现入口的诚实反馈),2 秒后自动消失
|
||||
useEffect(() => {
|
||||
if (!hint) return
|
||||
if (hintTimer.current) window.clearTimeout(hintTimer.current)
|
||||
hintTimer.current = window.setTimeout(() => setHint(null), 2000)
|
||||
return () => {
|
||||
if (hintTimer.current) window.clearTimeout(hintTimer.current)
|
||||
}
|
||||
}, [hint])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.trim().toLowerCase()
|
||||
if (!q) return sessions
|
||||
return sessions.filter((s) => s.title.toLowerCase().includes(q))
|
||||
}, [sessions, query])
|
||||
const grouped = useMemo(() => groupWorkspaceSessions(filtered, workspaces), [filtered, workspaces])
|
||||
const allExpanded = workspaces.length > 0 && workspaces.every(space => !preferences.collapsed[space.id])
|
||||
const visibleSpaces = workspaces.filter(space => !query.trim() || space.name.toLowerCase().includes(query.trim().toLowerCase()) || (grouped.groups.get(space.id)?.length ?? 0) > 0)
|
||||
const renderSession = (session: SessionMeta, child = false): React.JSX.Element => (
|
||||
<div key={session.id} className={`sidebar__item workspace-source-task conversation-agent-card${child ? ' conversation-agent-card--group-child' : ' conversation-agent-card--standalone'}${session.id === currentId ? ' selected' : ''}`}
|
||||
data-selected={session.id === currentId} data-session-id={session.id} role="button" tabIndex={0}
|
||||
onClick={() => { onNavigate?.('chat'); void selectSession(session.id) }} onKeyDown={(event) => { if (event.target === event.currentTarget && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); onNavigate?.('chat'); void selectSession(session.id) } }}>
|
||||
<span className="sidebar__item-title" title={session.title}>{session.title}</span>
|
||||
<span className="sidebar__item-time">{Object.values(runs).some(run => run.sessionId === session.id) ? '运行中' : sendingSessions.includes(session.id) ? '发送中' : relTime(session.updatedAt)}</span>
|
||||
<button className="sidebar__item-action" title="删除对话" onClick={(event) => { event.stopPropagation(); void removeSession(session.id).catch(error => setHint(error instanceof Error ? error.message : '删除失败')) }}><IconClose /></button>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (collapsed) {
|
||||
return (
|
||||
<aside className="sidebar sidebar--collapsed">
|
||||
<div className="sidebar__rail">
|
||||
<button className="btn-icon" title="展开侧栏" onClick={onToggleCollapse}>
|
||||
<IconNavMore />
|
||||
</button>
|
||||
<button
|
||||
className="sidebar__rail-avatar"
|
||||
title={`${MOCK_ACCOUNT.name}(mock 数据)`}
|
||||
onClick={onToggleCollapse}
|
||||
>
|
||||
{MOCK_ACCOUNT.initial}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="sidebar workspace-source conversation-list">
|
||||
{/* ---- 品牌区(实测:行高 64、标题 14/700、版本徽标 10)---- */}
|
||||
<div className="sidebar__header conversation-list-header">
|
||||
<div className="sidebar__brand-row conversation-list-logo-row conversation-list-logo-row--workbuddy">
|
||||
<div className="sidebar__brand-block conversation-list-brand-block">
|
||||
<div className="sidebar__brand-line conversation-list-brand-line"><span className="conversation-list-logo"><span className="logo-workbuddy-title">YCode</span></span></div>
|
||||
<div className="sidebar__version-row conversation-list-version-row">
|
||||
<span className="sidebar__version-badge conversation-list-version-badge">MVP</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="sidebar__discover"
|
||||
title="发现应用(尚未实现)"
|
||||
onClick={() => setHint('「发现应用」尚未实现')}
|
||||
>
|
||||
<IconDiscover className="sidebar__discover-icon" />
|
||||
<span>发现应用</span>
|
||||
<IconChevronDown className="sidebar__discover-chevron" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ---- 导航项(实测:215×32、padding 5px 12px、圆角 8、图标 16、文字 14)---- */}
|
||||
<div className="sidebar__tabs conversation-list-tabs" role="tablist">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const Icon = NAV_ICONS[item.id]
|
||||
const isNewTask = item.id === 'new-task'
|
||||
const selected = item.id === 'experts' ? activeRoute === 'capabilities' : isNewTask && !currentId && activeRoute === 'chat'
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
className={`sidebar__tab conversation-list-tab-button conversation-list-tab-button-box${selected ? ' active' : ''}`}
|
||||
role="tab"
|
||||
aria-selected={selected}
|
||||
data-implemented={item.implemented}
|
||||
title={item.implemented ? item.label : `${item.label}(尚未实现)`}
|
||||
onClick={() => {
|
||||
if (isNewTask) {
|
||||
void newTask()
|
||||
} else if (item.id === 'experts') {
|
||||
onNavigate?.('capabilities')
|
||||
} else {
|
||||
setHint(`「${item.label}」尚未实现`)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{Icon ? <Icon className="sidebar__tab-icon" /> : null}
|
||||
<span className="sidebar__tab-label conversation-list-tab-button-label">{item.label}{item.badge ? <span className="sidebar__tab-beta conversation-list-tab-beta">{item.badge}</span> : null}</span>
|
||||
{item.sub ? <span className="sidebar__tab-sub conversation-list-tab-button-sub">{item.sub}</span> : null}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ---- 搜索(过滤下方任务列表,真实可用)---- */}
|
||||
{searchVisible && <div className="sidebar__search">
|
||||
<IconSearch className="sidebar__search-icon" />
|
||||
<input
|
||||
ref={searchRef}
|
||||
className="sidebar__search-input"
|
||||
value={query}
|
||||
placeholder="搜索任务"
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={(event) => { if (event.key === 'Escape') { setQuery(''); setSearchVisible(false) } }}
|
||||
/>
|
||||
{query ? (
|
||||
<button className="sidebar__search-clear" title="清除" onClick={() => setQuery('')}>
|
||||
<IconClose />
|
||||
</button>
|
||||
) : null}
|
||||
</div>}
|
||||
|
||||
<div className="sidebar__content conversation-list-content">
|
||||
<section className="sidebar__section conversation-section conversation-section-tasks">
|
||||
<div className="conversation-section-label conversation-section-label-tasks" role="button" tabIndex={0} aria-expanded={tasksOpen}
|
||||
onClick={toggleTasks} onKeyDown={(event) => { if (event.target === event.currentTarget && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); toggleTasks() } }}>
|
||||
<span className="conversation-section-label-text">任务 ({grouped.independent.length})</span>
|
||||
<span className={`conversation-section-chevron${tasksOpen ? ' conversation-section-chevron-expanded' : ''}`}><IconChevronDown /></span>
|
||||
</div>
|
||||
{tasksOpen && <div className="sidebar__section-body conversation-tasks-independent-block">
|
||||
{grouped.independent.length ? grouped.independent.map(session => renderSession(session)) : <div className="conversation-group-empty-placeholder">{query ? '无匹配任务' : '暂无任务'}</div>}
|
||||
</div>}
|
||||
</section>
|
||||
<section className="sidebar__section conversation-section conversation-section-groups conversation-section-label-groups conversation-section-grouped-space">
|
||||
<div className="conversation-section-label conversation-section-label-groups" role="button" tabIndex={0} aria-expanded={spacesOpen}
|
||||
onClick={toggleSpaces} onKeyDown={(event) => { if (event.target === event.currentTarget && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); toggleSpaces() } }}>
|
||||
<span className="conversation-section-label-text">空间 ({visibleSpaces.length})</span>
|
||||
<span className={`conversation-section-chevron${spacesOpen ? ' conversation-section-chevron-expanded' : ''}`}><IconChevronDown /></span>
|
||||
<button type="button" className="conversation-section-compact-btn" title={allExpanded ? '收起全部' : '展开全部'} aria-label={allExpanded ? '收起全部空间' : '展开全部空间'}
|
||||
onClick={(event) => { event.stopPropagation(); setPreferences(value => ({ ...value, collapsed: Object.fromEntries(workspaces.map(space => [space.id, allExpanded])), visible: {} })) }}><WorkspaceSourceIcon name={allExpanded ? "collapseAll" : "expandAll"} /></button>
|
||||
<button type="button" className="conversation-section-add-btn" title="新建工作空间" aria-label="新建工作空间" onClick={(event) => { event.stopPropagation(); setWorkspaceDialog({ mode: 'create' }) }}><WorkspaceSourceIcon name="addWorkspace" /></button>
|
||||
</div>
|
||||
{spacesOpen && <div className="sidebar__section-body conversation-section-content">
|
||||
{!visibleSpaces.length && <div className="conversation-group-empty-placeholder">{query ? '无匹配空间' : '暂无空间'}</div>}
|
||||
{visibleSpaces.map(space => {
|
||||
const tasks = grouped.groups.get(space.id) ?? []
|
||||
const open = !preferences.collapsed[space.id]
|
||||
const count = preferences.visible[space.id] ?? 5
|
||||
return <div key={space.id} className={`_collapsibleSection_13lf4_1 collapsible-section${selectedWorkspaceId === space.id ? ' workspace-group-selected' : ''}`} data-workspace-id={space.id}>
|
||||
<div className="_header_13lf4_11 _headerClickable_13lf4_20 _headerTopPadding_13lf4_44 collapsible-section-header workspace-group-header" role="button" tabIndex={0} aria-expanded={open} title={space.root}
|
||||
onClick={() => toggleGroup(space.id)} onKeyDown={(event) => { if (event.target === event.currentTarget && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); toggleGroup(space.id) } }}
|
||||
onContextMenu={(event) => { event.preventDefault(); openSpaceMenu(space, { left: event.clientX, top: event.clientY, bottom: event.clientY }) }}>
|
||||
<div className="_titleWithIcon_13lf4_48 collapsible-section-title">
|
||||
<span className="_titleIcon_13lf4_65 collapsible-section-icon"><WorkspaceFolderIcon open={open} /></span>
|
||||
<span className="_title_13lf4_48 collapsible-section-label">{space.name}</span>
|
||||
</div>
|
||||
<div className="_headerRight_13lf4_130"><div className="_customButton_13lf4_31 workspace-group-actions" onClick={(event) => event.stopPropagation()}>
|
||||
<button type="button" className="workspace-ui-button workspace-more-button" aria-label={`${space.name}更多`} title="更多" onClick={(event) => openSpaceMenu(space, event.currentTarget.getBoundingClientRect(), true)}><WorkspaceMoreIcon /></button>
|
||||
<button type="button" className="workspace-ui-button workspace-new-task-button" aria-label={`在${space.name}中新建任务`} title="新建任务" onClick={() => { void newTask(space) }}><WorkspaceSourceIcon name="newTask" /></button>
|
||||
</div></div>
|
||||
</div>
|
||||
<div className={`_contentWrap_13lf4_164 collapsible-section-motion${open ? '' : ' _collapsed_13lf4_172'}`} aria-hidden={!open} inert={!open}>
|
||||
<div className="_content_13lf4_164 collapsible-section-content">
|
||||
{tasks.length ? tasks.slice(0, count).map(session => renderSession(session, true)) : <div className="conversation-group-empty-placeholder">暂无任务</div>}
|
||||
{tasks.length > 5 && <button type="button" className="conversation-show-more-button workspace-attach-row" onClick={() => setPreferences(value => ({ ...value, visible: { ...value.visible, [space.id]: count >= tasks.length ? 5 : Math.min(tasks.length, count + 5) } }))}>{count >= tasks.length ? '收起' : '显示更多'}</button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
})}
|
||||
</div>}
|
||||
</section>
|
||||
</div>
|
||||
{workspaceDialog && <WorkspaceDialog mode={workspaceDialog.mode} initialName={workspaceDialog.space?.name}
|
||||
taskCount={workspaceDialog.space ? sessions.filter(session => session.workspaceId === workspaceDialog.space!.id).length : 0}
|
||||
onClose={() => setWorkspaceDialog(null)} onConfirm={async (name) => {
|
||||
if (workspaceDialog.mode === 'create') await createWorkspace(name)
|
||||
else if (workspaceDialog.mode === 'rename' && workspaceDialog.space) await renameWorkspace(workspaceDialog.space.id, name)
|
||||
else if (workspaceDialog.mode === 'remove' && workspaceDialog.space) await removeWorkspace(workspaceDialog.space.id)
|
||||
}} />}
|
||||
{spaceMenu && createPortal(<div className="workspace-source"><div className="workspace-more-popover-overlay" onClick={() => setSpaceMenu(null)} onContextMenu={(event) => { event.preventDefault(); setSpaceMenu(null) }}>
|
||||
<div className="workspace-more-popover" ref={spaceMenuRef} role="menu" style={{ position: 'fixed', top: spaceMenu.top, left: spaceMenu.left }} onClick={(event) => event.stopPropagation()} onContextMenu={(event) => event.stopPropagation()}>
|
||||
<ul className="workspace-more-popover__list" role="menu">
|
||||
<li role="none"><button type="button" role="menuitem" className="workspace-more-popover__item" onClick={() => {
|
||||
const id = spaceMenu.space.id; setSpaceMenu(null); void window.ycode.workspace.open(id).catch(error => setHint(error instanceof Error ? error.message : '无法打开文件夹'))
|
||||
}}><span className="workspace-more-popover__icon"><WorkspaceFolderIcon /></span><span className="workspace-more-popover__label">打开文件夹</span></button></li>
|
||||
<li role="none"><button type="button" role="menuitem" className="workspace-more-popover__item" onClick={() => { setWorkspaceDialog({ mode: 'rename', space: spaceMenu.space }); setSpaceMenu(null) }}><span className="workspace-more-popover__icon"><WorkspaceEditIcon /></span><span className="workspace-more-popover__label">重命名</span></button></li>
|
||||
<li role="none"><button type="button" role="menuitem" className="workspace-more-popover__item workspace-menu-remove" onClick={() => { setWorkspaceDialog({ mode: 'remove', space: spaceMenu.space }); setSpaceMenu(null) }}><span className="workspace-more-popover__icon"><WorkspaceRemoveIcon /></span><span className="workspace-more-popover__label">删除工作空间</span></button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div></div>, document.body)}
|
||||
|
||||
{hint ? <div className="sidebar__hint">{hint}</div> : null}
|
||||
|
||||
{/* ---- 底部账号区(实测:57 高、padding 8 12 8 8、border-top 1px #e6e6e6)----
|
||||
数据为 mock;收起侧栏入口也放这里 ---- */}
|
||||
<div className="sidebar__footer conversation-list-footer">
|
||||
<div className="user-menu">
|
||||
<button
|
||||
className="user-menu__trigger"
|
||||
title={`${MOCK_ACCOUNT.name}(账号为 mock 数据)`}
|
||||
onClick={() => setHint('账号为 mock 数据,尚未接入登录')}
|
||||
>
|
||||
<span className="user-menu__avatar">{MOCK_ACCOUNT.initial}</span>
|
||||
<span className="user-menu__name">{MOCK_ACCOUNT.name}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="user-menu__icon-btn"
|
||||
title={unread > 0 ? `通知(${unread} 条未读,mock 数据)` : '通知'}
|
||||
onClick={() => {
|
||||
setNotifOpen((v) => !v)
|
||||
setUnread(0)
|
||||
}}
|
||||
>
|
||||
<IconBell />
|
||||
{unread > 0 ? <span className="msg-center-badge">{unread}</span> : null}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="user-menu__icon-btn"
|
||||
title="小程序(尚未实现)"
|
||||
onClick={() => setHint('「小程序」尚未实现')}
|
||||
>
|
||||
<IconMiniprogram />
|
||||
</button>
|
||||
|
||||
<button className="user-menu__icon-btn" title="收起侧栏" onClick={onToggleCollapse}>
|
||||
<IconCollapseRail />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{notifOpen ? <NotificationsPanel onClose={() => setNotifOpen(false)} /> : null}
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
// Exact SVG geometry from fixed REA runtime Evidence ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb.
|
||||
const WORKSPACE_SOURCE_ICONS: Record<string, { viewBox: string; fill?: string; paths: Record<string, string>[] }> = {
|
||||
"folder": {
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "currentColor",
|
||||
"paths": [
|
||||
{
|
||||
"transform": "matrix(1 0 0 1 0.900391 0.90039)",
|
||||
"d": "M4.4893 0C4.8371 0 5.0591 -0.0008 5.2754 0.0244C6.3094 0.1455 7.0485 0.6989 7.6641 1.3145L8.6494 2.2998L6.9521 2.2998L6.8154 2.1631C6.2812 1.6289 5.7808 1.2913 5.1367 1.2158C5.0059 1.2005 4.8651 1.1992 4.4893 1.1992C3.3369 1.1992 2.5394 1.4806 2.0293 1.9717C1.5234 2.4588 1.1992 3.2499 1.1992 4.4893L1.1992 8.0996C1.1992 9.0452 1.1999 9.7144 1.2471 10.2353C1.2935 10.7481 1.382 11.0583 1.5215 11.2998C1.8929 11.9429 2.4455 12.2436 3.1484 12.3867C3.519 12.4622 3.9095 12.4906 4.3184 12.5L9.2217 12.5C9.8441 12.4975 10.462 12.477 11.0312 12.3564C11.7703 12.1999 12.3358 11.8919 12.6777 11.2998C12.8172 11.0583 12.9057 10.7481 12.9521 10.2353C12.9914 9.8018 12.9958 9.2656 12.9971 8.5527L12.9961 8.1064C12.9832 6.8588 12.9095 6.3058 12.6777 5.9043C12.3358 5.3123 11.7703 5.0042 11.0312 4.8477C10.462 4.7271 9.8441 4.7066 9.2217 4.7041L2.5996 4.7041L2.5996 3.5049L8.1572 3.5049L8.1514 3.499L9.8486 3.499L9.8652 3.5156C10.3318 3.533 10.8171 3.5747 11.2803 3.6729C12.2112 3.8701 13.1449 4.3124 13.7178 5.3047C14.1305 6.02 14.1839 6.8921 14.1963 8.0938L14.1963 8.0996L14.1992 8.0996C14.1992 8.2186 14.1974 8.3345 14.1973 8.4473L14.1992 9.1025L14.1982 9.1035L14.1992 9.1045L14.1934 9.1045C14.1885 9.5777 14.1788 9.9874 14.1465 10.3437C14.0925 10.9395 13.9801 11.4449 13.7178 11.8994C13.1449 12.8917 12.2112 13.334 11.2803 13.5312C10.3688 13.7243 9.3719 13.6992 8.5996 13.6992L5.5996 13.6992C4.8004 13.6992 3.8041 13.7447 2.9092 13.5625C1.9757 13.3724 1.0723 12.9228 0.4814 11.8994C0.2191 11.4449 0.1067 10.9395 0.0527 10.3437C-0.0005 9.7561 0 9.0232 0 8.0996L0 4.4893C0 3.0495 0.3791 1.8952 1.1973 1.1074C2.0113 0.3238 3.1594 0 4.4893 0Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"folderExpanded": {
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "currentColor",
|
||||
"paths": [
|
||||
{
|
||||
"fillRule": "evenodd",
|
||||
"transform": "matrix(1 0 0 1 0.89978 0.900023)",
|
||||
"d": "M0 4.4895L0.0002 5.1L0.0002 8.1L0.001 8.7576C0.0033 9.3809 0.0129 9.9034 0.0529 10.3441C0.1069 10.94 0.2201 11.4454 0.4825 11.9C1.0734 12.9235 1.9759 13.3729 2.9094 13.563C3.6605 13.7159 4.4832 13.7088 5.2007 13.7025C5.3381 13.7013 5.4717 13.7001 5.6002 13.7001L8.4127 13.7001C8.5497 13.7001 8.6943 13.702 8.8442 13.704C9.5052 13.7128 10.2695 13.7229 10.942 13.5764C11.8268 13.3837 12.6501 12.9044 13.0705 11.8163C13.3985 10.9672 13.783 9.9165 14.0647 8.9379C14.2055 8.4488 14.3237 7.9673 14.3956 7.5317C14.4658 7.1067 14.5001 6.6797 14.4428 6.3186C14.3732 5.879 14.2838 5.6354 14.0928 5.3047C13.9111 4.9899 13.6813 4.7279 13.4192 4.512C12.7245 3.9398 11.8176 3.7023 10.9764 3.5957C10.6035 3.5485 10.2254 3.5251 9.8629 3.5138L9.849 3.4999L8.152 3.4999L8.1567 3.5046L7.8445 3.5046L7.8083 3.5046C6.9801 3.5046 6.3029 3.5046 5.7507 3.5742C5.1704 3.6474 4.6738 3.8026 4.2253 4.1476C3.7809 4.4895 3.5052 4.9164 3.2889 5.4429C3.0813 5.9483 2.9132 6.5867 2.707 7.3637C2.5124 8.0974 2.3399 8.7774 2.2162 9.2737C2.1543 9.5219 2.1045 9.7244 2.0702 9.865C2.053 9.9353 2.0397 9.9901 2.0306 10.0275C2.0261 10.0461 2.0227 10.0605 2.0204 10.0702C2.0194 10.0743 2.0186 10.0776 2.018 10.08L3.253 10.08C3.2856 9.9468 3.3287 9.7716 3.3805 9.564C3.5033 9.0716 3.6741 8.3978 3.8668 7.6714C4.0809 6.8645 4.228 6.3149 4.3989 5.8988C4.5612 5.5038 4.7275 5.2753 4.957 5.0988C5.1826 4.9252 5.453 4.8212 5.9009 4.7648C6.3689 4.7058 6.9697 4.7047 7.8445 4.7047L8.6002 4.7047C8.6774 4.7047 8.7557 4.7045 8.835 4.7044C9.4659 4.703 10.1568 4.7014 10.8256 4.7861C11.5835 4.8821 12.2211 5.0798 12.6562 5.4382C12.8147 5.5688 12.9483 5.7221 13.0537 5.9047C13.1676 6.102 13.2097 6.205 13.2576 6.5065C13.2846 6.6767 13.275 6.9529 13.2117 7.3363C13.1501 7.7093 13.0451 8.142 12.9115 8.606C12.6445 9.5336 12.275 10.5454 11.9511 11.3838C11.712 12.0026 11.2861 12.2734 10.6866 12.404C10.162 12.5182 9.576 12.5114 8.9257 12.5039C8.759 12.502 8.588 12.5 8.4127 12.5L5.6002 12.5C5.4491 12.5 5.3 12.5011 5.1528 12.5022C4.4347 12.5075 3.7643 12.5124 3.1489 12.3871C2.4459 12.2439 1.8931 11.9434 1.5217 11.3001C1.3823 11.0585 1.2945 10.7485 1.248 10.2358C1.2008 9.7149 1.2002 9.0456 1.2002 8.1L1.2 5.1L1.2 4.4895C1.2 3.2502 1.524 2.4595 2.0299 1.9723C2.54 1.4811 3.337 1.2 4.4895 1.2C4.8656 1.2 5.0059 1.2009 5.1367 1.2162C5.7811 1.2916 6.2812 1.6291 6.8156 2.1635L6.952 2.2999L8.649 2.2999L7.6641 1.3149C7.0484 0.6993 6.3103 0.1453 5.2761 0.0243C5.0679 -0.0001 4.8546 -0 4.5286 0L4.4895 0C3.1595 0 2.0116 0.324 1.1976 1.1078C0.3794 1.8956 0 3.0498 0 4.4895Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"edit": {
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "none",
|
||||
"paths": [
|
||||
{
|
||||
"fill": "currentColor",
|
||||
"fillRule": "evenodd",
|
||||
"transform": "matrix(1 0 0 1 0.999868 1.49989)",
|
||||
"d": "M0.3011 12.4122Q0.6022 12.7102 1.0178 12.6286L1.9012 12.4553Q4.4733 11.9508 6.3267 10.0973L11.9021 4.5219Q12.678 3.7461 12.678 2.6489Q12.678 1.5517 11.9021 0.7758Q11.1263 0 10.0291 0Q8.9319 0 8.1561 0.7758L2.6125 6.3194Q0.7248 8.2071 0.2379 10.8318L0.0773 11.6977Q0 12.1142 0.3011 12.4122ZM11.0536 3.6734L10.7156 4.0114L8.6671 1.9618L9.0046 1.6244Q9.429 1.2 10.0291 1.2Q10.6292 1.2 11.0536 1.6244Q11.478 2.0487 11.478 2.6489Q11.478 3.249 11.0536 3.6734ZM1.3645 11.3378L1.6702 11.2778Q3.8834 10.8436 5.4782 9.2488L9.8671 4.86L7.8186 2.8103L3.461 7.1679Q1.8368 8.7922 1.4178 11.0507L1.3645 11.3378ZM6.0001 11.3L13.0001 11.3L13.0001 12.5L6.0001 12.5L6.0001 11.3Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"remove": {
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "currentColor",
|
||||
"paths": [
|
||||
{
|
||||
"transform": "matrix(1 0 0 1 1.03834 1.20291)",
|
||||
"fillRule": "evenodd",
|
||||
"d": "M4.6312 0L9.6312 0L9.6312 1.2L4.6312 1.2L4.6312 0ZM0 2.9061L13.9633 2.9061L13.9633 4.1061L2.7311 4.1061L2.7311 8.2934Q2.7311 10.2897 2.8934 10.7529Q3.3549 12.0696 4.6717 12.5311Q5.1348 12.6934 7.1311 12.6934Q9.1274 12.6934 9.5906 12.5311Q10.9074 12.0696 11.3688 10.7529Q11.5311 10.2897 11.5311 8.2934L11.5311 5.0392L12.7311 5.0392L12.7311 8.2934Q12.7311 10.4939 12.5013 11.1497Q11.8489 13.0112 9.9875 13.6636Q9.3316 13.8934 7.1311 13.8934Q4.9306 13.8934 4.2748 13.6636Q2.4133 13.0112 1.7609 11.1497Q1.5311 10.4939 1.5311 8.2934L1.5311 4.1061L0 4.1061L0 2.9061ZM7.7312 11.2934L7.7312 5.2934L6.5312 5.2934L6.5312 11.2934L7.7312 11.2934Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"newTask": {
|
||||
"viewBox": "0 0 14.4531 14.6597",
|
||||
"fill": "currentColor",
|
||||
"paths": [
|
||||
{
|
||||
"transform": "matrix(1 0 0 1 0 0.0102059)",
|
||||
"d": "M7.2266 0.0001C7.9263 0.0001 8.9357 -0.0101 9.9521 0.17C10.9684 0.3502 12.061 0.7315 12.8926 1.5636C14.2855 2.9577 14.4531 4.8385 14.4531 6.2804C14.4531 7.7222 14.2853 9.6021 12.8926 10.9962C12.094 11.7954 11.0936 12.1824 10.1406 12.3721C9.1916 12.5611 8.2453 12.5626 7.5312 12.5606C7.2983 12.56 6.9785 12.7013 6.5684 13.0948C6.17 13.4771 5.7651 14.0227 5.3662 14.6495L4.3535 14.005C4.7714 13.3484 5.2367 12.709 5.7373 12.2286C6.2261 11.7597 6.8348 11.3587 7.5342 11.3604C8.2533 11.3624 9.0891 11.3581 9.9062 11.1954C10.7192 11.0335 11.4695 10.7233 12.0439 10.1485C13.073 9.1185 13.2539 7.6783 13.2539 6.2804C13.2539 4.8825 13.0729 3.4423 12.0439 2.4122C11.4564 1.8243 10.6378 1.5103 9.7432 1.3517C8.8486 1.1931 7.9467 1.1993 7.2266 1.1993C6.5064 1.1993 5.6045 1.1931 4.71 1.3517C3.8153 1.5103 2.9967 1.8242 2.4092 2.4122C1.3801 3.4423 1.1992 4.8825 1.1992 6.2804C1.1993 7.6783 1.3802 9.1185 2.4092 10.1485C3.0494 10.7892 3.9083 11.1001 4.8252 11.2442L4.7324 11.837L4.6387 12.4298C3.5897 12.2648 2.45 11.8863 1.5605 10.9962C0.1678 9.6021 0 7.7222 0 6.2804C0 4.8385 0.1676 2.9577 1.5605 1.5636C2.392 0.7316 3.4838 0.3502 4.5 0.17C5.5166 -0.0102 6.5267 0.0001 7.2266 0.0001Z"
|
||||
},
|
||||
{
|
||||
"transform": "matrix(1 0 0 1 4.52539 3.5943)",
|
||||
"d": "M3.2959 2.0967L5.3994 2.0967L5.3994 3.2959L3.2959 3.2959L3.2959 5.4023L2.0967 5.4023L2.0967 3.2959L0 3.2959L0 2.0967L2.0967 2.0967L2.0967 0L3.2959 0L3.2959 2.0967Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"more": {
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "currentColor",
|
||||
"paths": [
|
||||
{
|
||||
"fill": "currentColor",
|
||||
"transform": "matrix(1 0 0 1 2 7)",
|
||||
"d": "M0 1C0 0.4477 0.4477 0 1 0C1.5523 0 2 0.4477 2 1C2 1.5523 1.5523 2 1 2C0.4477 2 0 1.5523 0 1ZM5 1C5 0.4477 5.4477 0 6 0C6.5523 0 7 0.4477 7 1C7 1.5523 6.5523 2 6 2C5.4477 2 5 1.5523 5 1ZM10 1C10 0.4477 10.4477 0 11 0C11.5523 0 12 0.4477 12 1C12 1.5523 11.5523 2 11 2C10.4477 2 10 1.5523 10 1Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"collapseAll": {
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "currentColor",
|
||||
"paths": [
|
||||
{
|
||||
"transform": "matrix(-1 0 0 -1 14 7.5)",
|
||||
"d": "M4.835 5.5L4.835 2.9794L4.835 2.959Q4.835 2.0527 4.8055 1.6349L1.2202 5.2202L0.2798 4.2798L3.8651 0.6945Q3.4474 0.665 2.541 0.665L2.5 0.665L0 0.665L0 -0.665L2.5 -0.665L2.541 -0.665Q3.7967 -0.665 4.2438 -0.604Q5.0703 -0.4912 5.5307 -0.0309Q5.9911 0.4293 6.104 1.2561Q6.165 1.7032 6.165 2.959L6.165 2.9795L6.165 5.5L4.835 5.5Z",
|
||||
"fillRule": "evenodd"
|
||||
},
|
||||
{
|
||||
"transform": "matrix(-1 0 0 -1 7.5 14)",
|
||||
"d": "M0.665 0L0.665 2.5L0.665 2.541Q0.665 3.4473 0.6945 3.8651L4.2798 0.2798L5.2202 1.2202L1.6349 4.8055Q2.0526 4.835 2.959 4.835L3 4.835L5.5 4.835L5.5 6.165L3 6.165L2.959 6.165Q1.7033 6.165 1.2562 6.104Q0.4297 5.9912 -0.0307 5.5309Q-0.4911 5.0707 -0.604 4.2439Q-0.665 3.7968 -0.665 2.541L-0.665 2.5L-0.665 0L0.665 0Z",
|
||||
"fillRule": "evenodd"
|
||||
}
|
||||
]
|
||||
},
|
||||
"expandAll": {
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "currentColor",
|
||||
"paths": [
|
||||
{
|
||||
"transform": "matrix(1 0 0 1 8.17 2.33)",
|
||||
"d": "M4.835 5.5L4.835 2.9794L4.835 2.959Q4.835 2.0528 4.8055 1.635L1.3003 5.1403L0.3599 4.1998L3.8652 0.6945Q3.4475 0.665 2.541 0.665L2.5 0.665L0 0.665L0 -0.665L2.5 -0.665L2.541 -0.665Q3.7967 -0.665 4.2438 -0.604Q5.0703 -0.4912 5.5307 -0.0309Q5.9911 0.4293 6.104 1.2561Q6.165 1.7032 6.165 2.959L6.165 2.9795L6.165 5.5L4.835 5.5Z",
|
||||
"fillRule": "evenodd"
|
||||
},
|
||||
{
|
||||
"transform": "matrix(1 0 0 1 2.33 8.17)",
|
||||
"d": "M0.665 0L0.665 2.5L0.665 2.541Q0.665 3.4472 0.6945 3.865L4.1997 0.3597L5.1401 1.3002L1.6348 4.8055Q2.0525 4.835 2.959 4.835L3 4.835L5.5 4.835L5.5 6.165L3 6.165L2.959 6.165Q1.7033 6.165 1.2562 6.104Q0.4297 5.9912 -0.0307 5.5309Q-0.4911 5.0707 -0.604 4.2439Q-0.665 3.7968 -0.665 2.541L-0.665 2.5L-0.665 0L0.665 0Z",
|
||||
"fillRule": "evenodd"
|
||||
}
|
||||
]
|
||||
},
|
||||
"addWorkspace": {
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "currentColor",
|
||||
"paths": [
|
||||
{
|
||||
"transform": "matrix(1 0 0 1 1.00327 1.0033)",
|
||||
"d": "M5.1578 0.8849L5.5895 1.3166Q6.146 1.8731 6.2991 1.9366Q6.4523 2 7.2394 2L9.3813 2Q11.0408 1.9999 11.6222 2.0793Q12.651 2.2197 13.2124 2.781Q13.7737 3.3424 13.9141 4.3712Q13.9934 4.9525 13.9934 6.6121L13.9934 8.3949Q13.9935 10.458 13.8958 11.1734Q13.7279 12.4039 13.0659 13.0658Q12.4039 13.7278 11.1735 13.8958Q10.4581 13.9934 8.395 13.9933L5.5985 13.9933Q3.5354 13.9934 2.8199 13.8957Q1.5895 13.7278 0.9276 13.0658Q0.2656 12.4039 0.0976 11.1734Q0 10.458 0.0001 8.3949L0.0001 3.0213Q0.0001 1.6715 0.2138 1.2276Q0.5433 0.5432 1.2277 0.2137Q1.6715 0 3.0214 0Q3.6198 0 3.858 0.0544Q4.2164 0.1363 4.5278 0.3318Q4.7347 0.4618 5.1578 0.8849ZM4.215 1.8277Q3.8982 1.5109 3.8186 1.461Q3.6989 1.3858 3.561 1.3543Q3.4694 1.3333 3.0214 1.3333Q1.9758 1.3333 1.8061 1.415Q1.5422 1.5421 1.4151 1.806Q1.3334 1.9757 1.3334 3.0213L1.3334 8.395Q1.3333 10.3674 1.4187 10.9931Q1.5259 11.7786 1.8704 12.123Q2.2148 12.4675 3.0003 12.5747Q3.6259 12.6601 5.5984 12.66L8.395 12.66Q10.3675 12.6601 10.9932 12.5747Q11.7787 12.4675 12.1231 12.123Q12.4675 11.7786 12.5747 10.9931Q12.6601 10.3675 12.6601 8.395L12.6601 6.612Q12.6601 5.0431 12.593 4.5515Q12.5133 3.9676 12.2695 3.7239Q12.0257 3.4801 11.4419 3.4004Q10.9503 3.3333 9.3814 3.3333L7.2394 3.3333Q6.1871 3.3333 5.7889 3.1684Q5.3907 3.0035 4.6467 2.2594L4.215 1.8277ZM7.6617 4.6967L7.6617 7.0317L9.9967 7.0317L9.9967 8.3617L7.6617 8.3617L7.6617 10.6967L6.3317 10.6967L6.3317 8.3617L3.9967 8.3617L3.9967 7.0317L6.3317 7.0317L6.3317 4.6967L7.6617 4.6967Z",
|
||||
"fillRule": "evenodd"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
function WorkspaceSourceIcon({ name }: { name: string }): React.JSX.Element {
|
||||
const icon = WORKSPACE_SOURCE_ICONS[name];
|
||||
return <svg width="16" height="16" viewBox={icon.viewBox} fill={icon.fill} xmlns="http://www.w3.org/2000/svg" aria-hidden="true">{icon.paths.map((path, index) => <path key={index} {...path} />)}</svg>
|
||||
}
|
||||
function WorkspaceFolderIcon({ open = false }: { open?: boolean }): React.JSX.Element { return <WorkspaceSourceIcon name={open ? "folderExpanded" : "folder"} /> }
|
||||
function WorkspaceMoreIcon(): React.JSX.Element { return <WorkspaceSourceIcon name="more" /> }
|
||||
function WorkspaceEditIcon(): React.JSX.Element { return <WorkspaceSourceIcon name="edit" /> }
|
||||
function WorkspaceRemoveIcon(): React.JSX.Element { return <WorkspaceSourceIcon name="remove" /> }
|
||||
|
||||
/** 收起侧栏(自绘,可长期保留) */
|
||||
function IconCollapseRail(): React.JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.2"
|
||||
>
|
||||
<rect x="1.6" y="2.6" width="12.8" height="10.8" rx="2" />
|
||||
<path d="M6.2 2.6v10.8" />
|
||||
<path d="M9.6 6.4 8 8l1.6 1.6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
Loaded 100 of 1118 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user