diff --git a/.project-docs/30-worklog/tasks/20260907-project-config-plugin-panel-c84e71a2.md b/.project-docs/30-worklog/tasks/20260907-project-config-plugin-panel-c84e71a2.md new file mode 100644 index 0000000..d4d8c0c --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260907-project-config-plugin-panel-c84e71a2.md @@ -0,0 +1,59 @@ +# Task: Align plugin drawer and project configuration modules + +## Identity + +- Task ID: 20260907-project-config-plugin-panel-c84e71a2 +- Mode: Feature +- Branch: codex/20260907-project-config-plugin-panel-c84e71a2-project-config-plugin-panel +- Worktree: /Users/inmanx/Documents/makelore-plugin-panel +- Base commit: 918f8f80dca61b0f561f96b168e4ef541734d414 +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Keep the Project Configuration resource entries in one four-column top row. +- Present Plugins in the same narrow right-side drawer system as Models, Skills, and Knowledge. +- Remove Plugin filters and manual refresh, and expose a direct Skill-like capability list with details and the applicable primary action. +- Keep Plugin details user-facing and capability-oriented, without package/runtime/technical implementation fields. +- Show each configured model's exact name and its supported reasoning strengths in the Models drawer. + +## Intent And Constraints + +- Preserve `/project-config/plugins` as the canonical deep link and retain all compatibility redirects. +- Preserve the existing unified Plugin controller, Main/store authority boundaries, acquisition, installation, project enablement, assignment, and billing semantics. +- Keep the single Makelore light visual system and update focused unit/Electron E2E coverage. + +## Outcome + +- Project Configuration now keeps Models, Skills, Knowledge, and Plugins in one four-column resource row. +- The Plugin workspace uses the same 430px right drawer width and shared drawer chrome as the other resources while retaining `/project-config/plugins` deep-link behavior. +- The Plugin drawer now presents one compact list without filters or manual refresh; each row exposes capability copy, details, and the honest next lifecycle action or settled state. +- Plugin details now describe introduction, human-facing capabilities, usage, actions, and necessary settings while omitting package, version, permission-id, runtime, policy, and billing implementation fields. +- The Models drawer now makes the exact model id primary and shows selectable thinking strengths from server-owned capabilities or verified local model profiles; unknown capabilities remain explicitly unknown. +- README describes the updated presentation; shared canonical project-memory updates remain an Integration Gate promotion candidate. + +## Verification + +- `corepack pnpm exec vitest run tests/unit/plugins-page.test.tsx tests/unit/plugin-workspace-controller.test.ts tests/unit/coding-feature-ui.test.tsx tests/unit/agent-creation-dialog.test.tsx tests/unit/pi-product-artifact.test.ts --maxWorkers=4` — 46 passed. +- `corepack pnpm run typecheck` — passed. +- `corepack pnpm run lint:check` — passed with 5 pre-existing warnings in `src/pages/Home/index.tsx` and `src/pages/Makelore/index.tsx`; no errors. +- `corepack pnpm run build:vite` — passed; existing dynamic-import and chunk-size warnings only. +- `corepack pnpm exec playwright test tests/e2e/plugin-marketplace.spec.ts tests/e2e/project-plugins.spec.ts tests/e2e/project-configuration-skills.spec.ts` — 5 passed; asserts the four-card row, equal drawer widths, direct Plugin list, capability-only details, and unchanged acquisition/device/project authorities. +- `corepack pnpm test` — 1,913 passed and 3 skipped across the normal and pressure suites. +- Isolated Electron screenshots visually confirmed the direct Plugin list, two-action card composition, new description, and capability-only detail hierarchy. + +## Follow-ups + +- None recorded. + +## Promotion Candidates + +### Project resource drawer presentation + +- Target canonical documents: `.project-docs/30-worklog/current-state.md`, `.project-docs/20-architecture/system-overview.md`, `.project-docs/20-architecture/module-map.md`, `.project-docs/20-architecture/data-flow.md`, and `.project-docs/40-domain/business-rules.md`. +- Proposal: record Project Configuration's single four-card resource row and same-width drawers; describe Plugins as a direct unfiltered capability list with contextual acquisition, installation, project-addition, local-enable, or settled-state actions and capability-only details; describe Models as exact configured model ids plus trusted selectable thinking strengths. +- Evidence: `src/pages/ProjectConfiguration/index.tsx`, `src/pages/Plugins/index.tsx`, `src/pages/Plugins/PluginDetails.tsx`, `src/lib/coding-model-options.ts`, focused unit coverage, five Electron E2E scenarios, typecheck, lint, and production compilation. +- Future impact: future resource settings work must not restore the wide/filter-heavy Plugin workspace or expose plugin implementation metadata in user-facing details, and must not guess unknown model reasoning levels. +- Semantic conflicts: the current canonical documents still describe a wide/filter Plugin workspace and earlier model capability presentation; this proposal supersedes only that presentation while preserving separate acquisition, device-installation, project-enablement, assignment, and Main/store authority boundaries. +- Human confirmation required: no; the user explicitly requested this presentation. diff --git a/README.md b/README.md index 17449cb..b7769c7 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Makelore 是一个面向软件、视觉创作与智能机器人的 AI 桌面工 - AI 绘画生成:服务端从当前规格编译专业图片或视频指令并返回不可变 Quote;客户端只展示创作提示词、参考图绑定、媒介、画幅、数量、格式、时长、警告和设计点,不展示或改写供应商 Prompt、模型、价格原子或存储地址。修改方案会先提交规格并重新核价,确认时只提交 Quote 身份;确认后的方案不再常驻,而是进入时间线中的可展开制作记录。参考图行只负责缩略图、`@图片N` 状态、替换和删除,保留什么、改变什么等语义统一由创作提示词表达。任务与资产始终属于 Workspace。 - AI 绘画健壮性:Renderer 为每次命令生成稳定 operation id,网络结果未知时保留原命令供原样重试,不把未知写入当失败或创建第二次生成。Main 负责 Token 刷新、Agent Gateway REST 提交和有界 Run 查询,并将可恢复事件流投影为 Host API SSE;断线后按事件游标续接并重新读取权威 Workspace。Canvas 只使用 Works Square 云端 V2 契约,没有本地语义适配器或降级路径,上游不可用时明确报错。 - AI 绘画右侧全高作品栏只展示 Workspace,不再在项目下创建独立设计会话。删除时必须完整输入项目名称;删除后项目、Living Form、任务、参考图和生成作品会从账户中隐藏且无法访问,不影响用户已另存到磁盘的副本。删除当前项目后自动打开最近更新的剩余项目,删除最后一个项目后进入空状态。 -- 项目插件设置:Code 侧栏不再单列“插件”;配置 Agent、Skill、模型与知识的项目配置页内提供“插件”卡片,并在同页宽抽屉中投影官方目录、账号 Library、本机 Device Packages 与当前项目状态。深链使用 `/project-config/plugins` 打开该项目页抽屉;旧 `/plugins`、Marketplace、My Plugins、Project Plugins URL 只做保留查询条件的确定性重定向。来源身份保持 `official:`、`local:`、`retained:` 分离;账号获取、官方设备交付和项目启用仍分别写入既有 authority,不会自动推进下一步。Data Service、Game Resource 与 Project Scaffold 三个代码所有的官方插件在项目启用后对项目内所有父 Agent 生效,不提供伙伴分配;确实采用 assignment scope 的其他 Marketplace 插件继续由伙伴分配控制。任一来源失败不会清空其他来源;本机 Skill/Pi 插件仍只能通过对话安装,页面只管理已安装包并明确其本机全局生效范围。 +- 项目资源设置:项目配置页顶部以一横排四张资源卡提供模型、Skill、知识与插件入口,并统一使用同宽右侧抽屉。模型列表完整显示具体模型名称,以及服务端或已验证模型档案声明的可选思考强度;未知能力不做猜测。插件抽屉直接展示能力列表,不提供筛选或手动刷新控件;每项提供“查看详情”和当前真实生命周期对应的添加、安装、添加到项目、启用或状态按钮。详情只展示插件介绍、主要能力、使用方式和必要设置,不暴露版本、包、权限标识、运行方式或计费实现字段。深链仍使用 `/project-config/plugins`;旧 `/plugins`、Marketplace、My Plugins、Project Plugins URL 只做保留查询条件的确定性重定向。来源身份保持 `official:`、`local:`、`retained:` 分离,账号获取、官方设备交付和项目启用仍分别写入既有 authority,不会自动推进下一步;本机 Skill/Pi 插件仍只能通过对话安装,页面只管理已安装包。 - 官方插件运行架构:Renderer 只调用 Main-owned Marketplace facade;Main 负责账号、请求 deadline、签名/摘要校验、不可变 Release、current selection 与原子回滚。Pi parent worker 使用冻结的 effective snapshot,将每个有效 Skill 与已验证 Package Store root 成对传给 resource loader、Extension Host 和 CLI;普通 `skill_only` 下载包不依赖运行时 Policy,也不得执行分发包中的任意代码。Project Scaffold 的 `.mjs` 是代码所有的 bundled 例外,只能从签名客户端固定资源根通过应用自带 Node 执行,不能扩展为 Marketplace 下载 artifact 的脚本权限。正式激活仍等待官方 Ed25519 公钥(production key activation HOLD);生产私钥只能来自部署 secret,测试使用注入的临时密钥。 - 原生 Web Search 是冻结 selected model/provider/credential 的 parent-only 模型工具;只有 Works `model_capabilities` 明确声明支持时才出现,使用当前模型的正常计费链,不依赖 Marketplace 获取、Release、Admission、Hosted Provider 或 `agent_browser` 回退。 - 本机 Device Packages 只由 Conversation 工具发起安装,支持 npm、Git、绝对本地 Plugin 目录和 loose `SKILL.md`。Main 在预览和独立确认后提交不可变本机 generation,禁用生命周期脚本;确认界面明确提示 Pi extension 以桌面当前用户权限执行。新建/空闲 parent 会同时加载所有显式安装且当前启用的 Skill,以及必需 Makelore 扩展之外的全部 Pi extension,active parent 在 turn settled 后切换,child 不继承这些资源,也不会扫描 ambient 项目或用户目录;Renderer 不提供安装源选择器。 @@ -126,7 +126,7 @@ Pi 正式包必须继续运行 `pnpm run verify:artifact:pi`、`pnpm run smoke:p - 新项目默认没有智能体;用户在项目内创建智能体时必须填写名称、预设头像或本地头像、职责说明和精确的 Provider 账号与默认模型。该选择以 `{ accountId, modelId, thinkingLevel }` 保存,不依赖运行时私有 Provider id。本地头像会自动裁剪为 256×256,并优先压缩为 WebP 后随项目配置保存;系统指令与技能属于高级设置。 - Makelore 为每个项目智能体附加平台管理的中文语言规则:自然语言默认使用简体中文,包括可见的思考过程、进度说明、工具调用说明和最终回复;代码、命令、路径、日志、错误原文与必要引用保留原始语言。用户或项目智能体系统指令明确要求其他语言时按该要求执行;模型仍不遵循时视为 Provider 模型行为。 - 有效项目可以直接进入对话工作区;`initialized` 只作旧 schema 兼容,不参与导航。尚无可用智能体时,聊天页保留品牌引导并显示非阻塞的设置入口,不伪造智能体或 Conversation;用户可以稍后进入项目配置完成设置。 -- 项目配置页的新增与已有智能体编辑统一使用居中弹窗;编辑保存先更新当前页面草稿,点击底部“保存项目配置”后统一持久化。“可用模型”抽屉只展示已配置模型、提供方和文本/多模态能力,不提供项目级选择。智能体的默认模型只作为新对话的初始值;核心聊天页可为当前对话独立切换模型和思考强度,切换不会改写智能体默认值或其他对话。 +- 项目配置页的新增与已有智能体编辑统一使用居中弹窗;编辑保存先更新当前页面草稿,点击底部“保存项目配置”后统一持久化。“可用模型”抽屉只展示已配置模型的具体名称和可信的可选思考强度,不提供项目级选择;无法在配置阶段确认的强度明确提示进入对话后查看。智能体的默认模型只作为新对话的初始值;核心聊天页可为当前对话独立切换模型和思考强度,切换不会改写智能体默认值或其他对话。 - 项目智能体只保存自身的精确默认模型;未完成模型选择的智能体保持待配置状态,不从项目级 `defaultModel` 或运行时私有配置回退。 - “可用技能”入口使用模块图标;点击技能后先展示目录结构,再展示主文件 `SKILL.md` 原文,并支持返回技能列表。 - 一个智能体可以拥有多条互相独立的 Pi Session;智能体和对话元数据分别由 `.makelore/project.json` 与 `.makelore/conversations.json` 保存,稳定的智能体与对话 id 保持本地历史连续。 diff --git a/scripts/lib/pi-product-artifact.mjs b/scripts/lib/pi-product-artifact.mjs index 4c2977e..a573be5 100644 --- a/scripts/lib/pi-product-artifact.mjs +++ b/scripts/lib/pi-product-artifact.mjs @@ -67,8 +67,10 @@ const MARKETPLACE_ARTIFACT_MARKERS = Object.freeze({ renderer: Object.freeze([ '/api/coding/plugin-marketplace/catalog', '/api/coding/plugin-marketplace/library', - '免费获取', - '获取、项目启用与伙伴分配彼此独立', + '为你的智能体添加插件,拓展更多能力。', + 'acquire', + 'install_stable', + 'enable_project', '/project-config/plugins', ]), }); diff --git a/src/lib/coding-model-options.ts b/src/lib/coding-model-options.ts index ab64996..630cf42 100644 --- a/src/lib/coding-model-options.ts +++ b/src/lib/coding-model-options.ts @@ -1,11 +1,44 @@ import type { ProviderAccount, ProviderVendorInfo } from '@/lib/providers'; -import type { ProductModelRef } from '@/types/coding-conversation'; +import type { ConversationThinkingLevel, ProductModelRef } from '@/types/coding-conversation'; +import { getImportedModelProfile } from '../../shared/imported-model-profile'; export interface CodingModelOption { key: string; accountId: string; modelId: string; label: string; + availableThinkingLevels: ConversationThinkingLevel[] | null; +} + +const STANDARD_THINKING_LEVELS: readonly ConversationThinkingLevel[] = [ + 'off', + 'minimal', + 'low', + 'medium', + 'high', +]; +const SERVER_REASONING_LEVELS = ['low', 'high', 'max'] as const; + +function availableThinkingLevels( + account: ProviderAccount, + modelId: string, +): ConversationThinkingLevel[] | null { + const serverCapability = account.metadata?.worksSquareModelCapabilities?.[modelId]; + if (serverCapability) { + return [ + ...(serverCapability.reasoningCanDisable ? ['off' as const] : []), + ...SERVER_REASONING_LEVELS.filter((level) => serverCapability.reasoningEfforts.includes(level)), + ]; + } + + const profile = getImportedModelProfile(modelId)?.pi; + if (!profile) return null; + if (!profile.reasoning) return ['off']; + if (!profile.thinkingLevelMap) return [...STANDARD_THINKING_LEVELS]; + return [ + ...STANDARD_THINKING_LEVELS.filter((level) => profile.thinkingLevelMap?.[level] !== null), + ...(typeof profile.thinkingLevelMap.max === 'string' ? ['max' as const] : []), + ]; } export function codingModelKey(model: Pick): string { @@ -51,6 +84,7 @@ export function buildCodingModelOptions( accountId: account.id, modelId, label: `${account.label}${vendor && vendor !== account.label ? ` · ${vendor}` : ''} / ${modelId}`, + availableThinkingLevels: availableThinkingLevels(account, modelId), }); } } diff --git a/src/pages/Plugins/PluginDetails.tsx b/src/pages/Plugins/PluginDetails.tsx index 054e7f2..760fa56 100644 --- a/src/pages/Plugins/PluginDetails.tsx +++ b/src/pages/Plugins/PluginDetails.tsx @@ -1,7 +1,6 @@ import { useState } from 'react'; -import { AlertTriangle, ExternalLink, ShieldCheck, X } from 'lucide-react'; +import { CheckCircle2, ExternalLink, Puzzle, X } from 'lucide-react'; import { DataServicePluginSettings } from '@/components/plugins/data-service-settings'; -import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Dialog, @@ -12,157 +11,91 @@ import { DialogTitle, } from '@/components/ui/dialog'; import type { DataServiceInstanceState } from '../../../shared/data-service'; -import { - isBundledOfficialPlugin, - isProjectWideOfficialPlugin, - type PluginWorkspaceCommand, - type PluginWorkspaceItem, +import type { + PluginWorkspaceCommand, + PluginWorkspaceItem, } from './plugin-workspace-model'; +import { isBundledOfficialPlugin } from './plugin-workspace-model'; -interface DetailOperation { - capabilityId: string; - operationId: string; - executionMode: 'synchronous' | 'job' | null; - enabled: boolean | null; - mutation: 'read' | 'write' | 'destructive' | null; - description: string | null; - billing: { - mode: 'included' | 'platform_metered' | 'external_account'; - availability: 'available' | 'unavailable' | null; - status: 'billing_unavailable' | null; - notice: string; - entitlementScope: string | null; - unitName: string | null; - unitSize: number | null; - ratePoints: string | null; - minimumChargePoints: string | null; - roundingMode: 'ceil' | null; - pricingVersion: string | number | null; - }; +interface PluginCapabilityDescription { + title: string | null; + description: string; } -function detailOperations(item: PluginWorkspaceItem): DetailOperation[] { - const operations = new Map(); - for (const operation of item.official?.detail?.operations ?? []) { - operations.set(`${operation.capabilityId}:${operation.operation}`, { - capabilityId: operation.capabilityId, - operationId: operation.operation, - executionMode: operation.executionMode, - enabled: operation.enabled, - mutation: null, - description: null, - billing: { - ...operation.billing, - availability: null, - }, - }); - } +function capabilityDescriptions(item: PluginWorkspaceItem): PluginCapabilityDescription[] { + const seen = new Set(); + const descriptions: PluginCapabilityDescription[] = []; for (const capability of item.official?.project?.capabilities ?? []) { for (const operation of capability.operations) { - const key = `${capability.id}:${operation.id}`; - const catalogOperation = operations.get(key); - operations.set(key, { - capabilityId: capability.id, - operationId: operation.id, - executionMode: catalogOperation?.executionMode ?? null, - enabled: catalogOperation?.enabled ?? null, - mutation: operation.tool?.mutation ?? null, - description: operation.tool?.description ?? null, - billing: { - mode: operation.billing.mode, - availability: operation.billing.availability, - status: null, - notice: operation.billing.notice, - entitlementScope: null, - unitName: operation.billing.unitName ?? null, - unitSize: operation.billing.unitSize ?? null, - ratePoints: operation.billing.ratePoints ?? null, - minimumChargePoints: operation.billing.minimumChargePoints ?? null, - roundingMode: operation.billing.roundingMode ?? null, - pricingVersion: operation.billing.pricingVersion ?? null, - }, - }); + const title = operation.tool?.label.trim() || null; + const description = operation.tool?.description.trim(); + if (!description) continue; + const key = `${title ?? ''}:${description}`; + if (seen.has(key)) continue; + seen.add(key); + descriptions.push({ title, description }); } } - return [...operations.values()]; -} - -function operationGroups(operations: readonly DetailOperation[]): Array<{ - capabilityId: string; - operations: DetailOperation[]; -}> { - const groups = new Map(); - for (const operation of operations) { - const group = groups.get(operation.capabilityId) ?? []; - group.push(operation); - groups.set(operation.capabilityId, group); + if (descriptions.length) return descriptions; + if (item.source === 'local') { + return [{ title: null, description: '为智能体提供可在本机使用的扩展能力。' }]; } - return [...groups].map(([capabilityId, groupOperations]) => ({ - capabilityId, - operations: groupOperations, - })); + if (item.source === 'retained') { + return [{ title: null, description: '保留此前添加到项目的能力,恢复可用后可继续使用。' }]; + } + return [{ + title: null, + description: item.summary || '为智能体补充更多处理任务的能力。', + }]; } -function operationBillingMode(mode: DetailOperation['billing']['mode']): string { - if (mode === 'included') return '包含'; - if (mode === 'platform_metered') return '按 Token Point'; - return '外部账号'; +function pluginIntroduction(item: PluginWorkspaceItem): string { + if (item.source === 'local') return '这是一项已安装在本机的插件能力,可供智能体在适用任务中使用。'; + if (item.source === 'retained') return '这项插件能力仍保留在项目中,目前暂时无法使用。'; + return item.official?.detail?.descriptionMarkdown + || item.summary + || '为智能体补充更多处理任务的能力。'; } -const SOURCE_TEXT = { - official: '官方', - local: '本机', - retained: '配置保留', -} as const; +function usageDescription(item: PluginWorkspaceItem): string { + if (item.unavailable) return '这项能力目前暂不可用,恢复后即可继续添加或使用。'; + if (item.projectState === 'enabled') return '已添加到当前项目,项目中的智能体可以按配置使用这些能力。'; + if (item.source === 'local') { + return item.localEnabled + ? '已在本机启用,智能体可在适用任务中使用这些能力。' + : '已安装在本机,启用后智能体即可使用这些能力。'; + } + if (isBundledOfficialPlugin(item)) return '此插件随应用提供,添加到项目后即可使用。'; + if (item.delivery === 'account_acquired') return '已添加到你的插件库,安装或添加到项目后即可使用。'; + return '添加这项插件后,即可为智能体拓展对应能力。'; +} -const DELIVERY_TEXT = { - system_included: '随 MakeLore 提供', - account_acquired: '账号已获取', - account_removed: '已从账号移除', - account_unknown: '账号状态未知', - local_installed: '本机已安装', - not_acquired: '尚未获取', - retained: '仅保留项目配置', -} as const; - -const PROJECT_TEXT = { - enabled: '当前项目已启用', - disabled: '当前项目未启用', - unavailable: '当前项目暂不可用', - unknown: '当前项目状态未知', - not_applicable: '不属于项目启用范围', -} as const; - -const PROJECT_POLICY_NOTICE = { - stale: '当前项目策略使用缓存,所示能力与价格可能不是最新状态。', - unavailable: '当前项目策略不可用,无法确认最新能力与价格。', -} as const; - -function billingText(item: PluginWorkspaceItem): string { - if (item.billing === 'included') return '按平台包含,不按单次插件调用扣点。'; - if (item.billing === 'token_point') return '按 Token Point 实际用量计费,价格与回执以服务端为准。'; - if (item.billing === 'mixed') return '部分能力包含,部分能力按 Token Point 用量计费。'; - if (item.billing === 'none') return '无 MakeLore 平台插件计费投影;外部服务或模型调用可能另行计费。'; - return '当前没有可信的插件计费投影。'; +function commandText(command: PluginWorkspaceCommand): string { + switch (command.kind) { + case 'acquire': return '添加'; + case 'reacquire': return '重新添加'; + case 'remove_from_library': return '从插件库移除'; + case 'install_stable': return '安装'; + case 'install_beta': return '安装尝鲜版'; + case 'update_official': return '更新'; + case 'remove_official_device_package': return '从本机卸载'; + case 'enable_project': return '添加到项目'; + case 'disable_project': return '从项目移除'; + case 'enable_local': return '启用'; + case 'disable_local': return '停用'; + case 'remove_local': return '卸载'; + case 'sign_in': return '登录后添加'; + case 'open_agent_assignment': return '分配给智能体'; + case 'open_settings': return '打开设置'; + } } function commandLabel(command: PluginWorkspaceCommand, title: string): string { switch (command.kind) { - case 'acquire': return `免费获取${title}`; - case 'reacquire': return `重新免费获取${title}`; - case 'remove_from_library': return `从账号移除${title}`; - case 'install_stable': return `下载${title}稳定版`; - case 'install_beta': return `安装${title} Beta`; - case 'update_official': return `更新${title}设备包`; - case 'remove_official_device_package': return `删除${title}官方设备包`; - case 'enable_project': return `启用${title}到当前项目`; - case 'disable_project': return `从当前项目禁用${title}`; - case 'enable_local': return `本机全局启用${title}`; - case 'disable_local': return `本机全局停用${title}`; - case 'remove_local': return `移除本机包${title}`; - case 'sign_in': return `登录后免费获取${title}`; - case 'open_agent_assignment': return `分配${title}给伙伴`; - case 'open_settings': return `查看${title}插件设置`; + case 'enable_project': return `将${title}添加到项目`; + case 'open_agent_assignment': return `将${title}分配给智能体`; + case 'open_settings': return `打开${title}设置`; + default: return `${commandText(command)}${title}`; } } @@ -181,27 +114,27 @@ function confirmationCopy( switch (command.kind) { case 'remove_from_library': return { - title: `从账号移除${item.title}?`, - description: '这会移除账号插件库记录,不会自动删除已经下载的设备包或项目配置。', - confirm: '确认从账号移除', + title: `从插件库移除${item.title}?`, + description: '它将不再保留在你的插件库中,已添加到项目的配置不会自动变化。', + confirm: '确认移除', }; case 'remove_official_device_package': return { - title: `删除${item.title}的官方设备包?`, - description: '这只删除当前设备上的官方包,不会移除账号插件库或项目配置。', - confirm: '确认删除官方设备包', + title: `从本机卸载${item.title}?`, + description: '插件将从当前设备卸载,你的插件库和项目配置仍会保留。', + confirm: '确认卸载', }; case 'disable_project': return { - title: `从当前项目禁用${item.title}?`, - description: `插件能力会从项目“${projectName ?? '当前项目'}”停止,但插件数据与账号记录会保留。`, - confirm: '确认从当前项目禁用', + title: `从当前项目移除${item.title}?`, + description: `项目“${projectName ?? '当前项目'}”中的智能体将不能继续使用这项插件能力。`, + confirm: '确认从项目移除', }; case 'remove_local': return { - title: `移除本机包${item.title}?`, - description: '这会从本机全局移除该包,新建或空闲的主伙伴与 worker 将不再加载它。', - confirm: '确认移除本机包', + title: `卸载${item.title}?`, + description: '插件会从本机移除,智能体将不能继续使用它。', + confirm: '确认卸载', }; default: return { title: '', description: '', confirm: '' }; @@ -225,15 +158,8 @@ export interface PluginDetailsProps { export function PluginDetails(props: PluginDetailsProps) { const [confirmation, setConfirmation] = useState(null); const project = props.item.official?.project ?? null; - const detail = props.item.official?.detail ?? null; - const installation = props.item.official?.installation ?? null; - const operations = detailOperations(props.item); - const projectPolicyNotice = props.item.projectPolicyStatus === 'stale' - || props.item.projectPolicyStatus === 'unavailable' - ? PROJECT_POLICY_NOTICE[props.item.projectPolicyStatus] - : null; - const showDataService = project?.enabled - && project.settingsSurface === 'data-service'; + const capabilities = capabilityDescriptions(props.item); + const showDataService = project?.enabled && project.settingsSurface === 'data-service'; const confirmCopy = confirmation ? confirmationCopy(confirmation, props.item, props.activeProjectName) : null; @@ -249,65 +175,52 @@ export function PluginDetails(props: PluginDetailsProps) { return ( <> { if (!open) props.onClose(); }}> - +
-
- {SOURCE_TEXT[props.item.source]} - {props.item.stale ? 缓存 : null} - {props.item.suspended ? 已暂停 : null} - {props.item.retired ? 已退役 : null} - {props.item.unavailable && !props.item.suspended && !props.item.retired - ? 暂不可用 : null} -
{props.item.title} - - {props.item.publisher ? `${props.item.publisher} · ` : ''} - {props.item.version ? `版本 ${props.item.version}` : '版本未知'} - + 了解它可以为智能体带来哪些能力。
-
+
-

介绍

+

插件介绍

- {detail?.descriptionMarkdown || props.item.summary || '暂无介绍。'} + {pluginIntroduction(props.item)}

-
-

账号、设备与项目状态

-
-
-
交付
-
{DELIVERY_TEXT[props.item.delivery]}
-
-
-
当前设备
-
- {props.item.source === 'local' - ? (props.item.localEnabled ? '本机全局已启用' : '本机全局已停用') - : installation?.version ? `官方包 ${installation.version}` : isBundledOfficialPlugin(props.item) ? '随应用提供' : '未下载官方包'} -
-
-
-
当前项目
-
{PROJECT_TEXT[props.item.projectState]}
-
-
- {props.item.deviceReason ? ( -

- {installation?.version ? '旧设备版本已保留' : '设备包不可用'}:{props.item.deviceReason} -

- ) : null} +
+

主要能力

+
+ {capabilities.map((capability, index) => ( +
+ +
+ {capability.title ?

{capability.title}

: null} +

{capability.description}

+
+
+ ))} +
+
+ +
+

使用方式

+

+

-

可用操作

+

添加与使用

{props.item.commands.length ? (
{props.item.commands.map((command) => ( @@ -322,102 +235,16 @@ export function PluginDetails(props: PluginDetailsProps) { > {command.kind === 'open_agent_assignment' || command.kind === 'open_settings' ? : null} - {commandLabel(command, props.item.title)} + {props.isCommandPending(command) ? '处理中…' : commandText(command)} ))}
- ) :

当前状态没有可执行操作。

} -
- -
-

能力与权限

- {operations.length ? ( -
- {operationGroups(operations).map((capability) => ( -
-

{capability.capabilityId}

-
    - {capability.operations.map((operation) => ( -
  • -
    - {operation.operationId} - {operation.mutation - ? {operation.mutation} - : operation.executionMode - ? {operation.executionMode === 'job' ? '异步任务' : '同步'} - : SDK} - {operation.enabled === false ? 服务端已停用 : null} - {operation.billing.availability === 'unavailable' - || operation.billing.status === 'billing_unavailable' - ? 计费暂不可用 : null} -
    - {operation.description ?

    {operation.description}

    : null} -

    {operation.billing.notice}

    -
    -
    计费方式:
    {operationBillingMode(operation.billing.mode)}
    - {operation.billing.entitlementScope ?
    权益范围:
    {operation.billing.entitlementScope}
    : null} - {operation.billing.unitName ?
    计费单位:
    {operation.billing.unitName}
    : null} - {operation.billing.unitSize !== null ?
    单位大小:
    {operation.billing.unitSize}
    : null} - {operation.billing.ratePoints !== null ?
    每单位 Token Point:
    {operation.billing.ratePoints}
    : null} - {operation.billing.minimumChargePoints !== null ?
    最低扣点:
    {operation.billing.minimumChargePoints}
    : null} - {operation.billing.pricingVersion !== null ?
    价格版本:
    {operation.billing.pricingVersion}
    : null} - {operation.billing.roundingMode === 'ceil' ?
    舍入方式:
    向上取整
    : null} -
    -
  • - ))} -
-
- ))} -
- ) : props.item.local ? ( -
-

{props.item.local.kind === 'skill-only' ? 'Skill' : props.item.local.kind === 'pi-extension' ? 'Pi extension' : 'Skill + Pi extension'}

-

{props.item.local.skillEntries.length} 个 Skill · {props.item.local.extensionEntries.length} 个 extension

- {props.item.local.confirmedExecutableCode ? ( -

- - {props.item.local.kind === 'skill-only' - ? '包含可执行 Skill 脚本;调用时可能使用当前桌面用户可用的文件、网络和进程权限。' - : '包含可执行代码,运行时拥有完整桌面权限。'} -

- ) : null} -
- ) :

当前没有可显示的能力投影。

} - {detail?.permissions.length ? ( -
- -

{detail.permissions.join('、')}

-
- ) : null} -
- -
-

Token Point 与计费

- {projectPolicyNotice ? ( -

- - {projectPolicyNotice} -

- ) : null} -

{billingText(props.item)}

-
- -
-

- {isProjectWideOfficialPlugin(props.item) ? '生效范围' : '伙伴分配'} -

- {isProjectWideOfficialPlugin(props.item) - ?

启用当前项目后,插件会自动提供给该项目的主伙伴,无需单独分配;子伙伴不会继承。

- : props.item.source === 'local' - ?

本机包全局生效,不作为项目 Agent Skill 分配项;符合条件的新建或空闲主伙伴及其 worker 会按既有生命周期加载。

- : props.item.assignedAgentNames.length - ?
{props.item.assignedAgentNames.map((name) => {name})}
- :

尚未分配给伙伴。

} + ) :

当前无需进行其他操作。

}
{showDataService ? (
-

插件专属设置

+

使用设置

= [ - { value: 'all', label: '全部状态' }, - { value: 'available', label: '可获取' }, - { value: 'mine', label: '我的' }, - { value: 'enabled', label: '已启用' }, - { value: 'update', label: '可更新' }, - { value: 'unavailable', label: '不可用' }, -]; - export type PluginWorkspaceSourceName = 'catalog' | 'library' | 'device' | 'project' | 'detail'; export interface PluginWorkspaceSourceError { @@ -80,81 +37,119 @@ const SOURCE_ERROR_TITLE: Record = { detail: '插件详情', }; -function cardSource(item: PluginWorkspaceItem): string { - if (item.source === 'official') return '官方'; - if (item.source === 'local') return '本机'; - return '配置保留'; +const PRIMARY_PLUGIN_ACTIONS: readonly PluginWorkspaceCommand['kind'][] = [ + 'sign_in', + 'acquire', + 'reacquire', + 'install_stable', + 'update_official', + 'enable_project', + 'enable_local', + 'install_beta', +]; + +function primaryPluginCommand(item: PluginWorkspaceItem): PluginWorkspaceCommand | null { + for (const kind of PRIMARY_PLUGIN_ACTIONS) { + const command = item.commands.find((candidate) => candidate.kind === kind); + if (command) return command; + } + return null; } -function agentText(item: PluginWorkspaceItem): string { - if (item.source === 'local') return '本机全局生效'; - if (isProjectWideOfficialPlugin(item)) return '随项目启用'; - const count = item.assignedAgentIds.length; - return count ? `已分配 ${count} 位伙伴` : '尚未分配伙伴'; +function primaryPluginActionText(command: PluginWorkspaceCommand): string { + switch (command.kind) { + case 'sign_in': return '登录后添加'; + case 'acquire': + case 'reacquire': return '添加'; + case 'install_stable': return '安装'; + case 'install_beta': return '安装尝鲜版'; + case 'update_official': return '更新'; + case 'enable_project': return '添加到项目'; + case 'enable_local': return '启用'; + default: return '使用'; + } } -function agentLabel(item: PluginWorkspaceItem): string { - return isProjectWideOfficialPlugin(item) ? '生效范围' : '伙伴'; +function primaryPluginActionLabel(command: PluginWorkspaceCommand, title: string): string { + if (command.kind === 'enable_project') return `将${title}添加到项目`; + return `${primaryPluginActionText(command)}${title}`; +} + +function settledPluginState(item: PluginWorkspaceItem): string { + if (item.unavailable) return '暂不可用'; + if (item.projectState === 'enabled') return '已添加到项目'; + if (item.source === 'local') return item.localEnabled ? '已安装' : '已停用'; + if (item.official?.installation?.version) return '已安装'; + if (isBundledOfficialPlugin(item)) return '随应用提供'; + if (item.delivery === 'account_acquired') return '已添加'; + if (item.delivery === 'retained') return '暂不可用'; + return '可添加'; +} + +function pluginCardSummary(item: PluginWorkspaceItem): string { + if (item.source === 'local') return '为智能体提供可在本机使用的扩展能力。'; + if (item.source === 'retained') return '此前添加的插件能力目前暂不可用。'; + return item.summary || '为智能体提供更多可用能力。'; } function PluginCard({ item, - grouped = false, onOpen, + onCommand, + isCommandPending, }: { item: PluginWorkspaceItem; - grouped?: boolean; onOpen(trigger: HTMLButtonElement): void; + onCommand(command: PluginWorkspaceCommand): void | Promise; + isCommandPending(command: PluginWorkspaceCommand): boolean; }) { - const Title = grouped ? 'h3' : 'h2'; + const primaryCommand = primaryPluginCommand(item); + const commandPending = primaryCommand ? isCommandPending(primaryCommand) : false; return ( -
-
- {cardSource(item)} - {DELIVERY_TEXT[item.delivery]} - {item.updateAvailable ? 可更新 : null} - {item.suspended ? 已暂停 : null} - {item.retired ? 已退役 : null} - {item.unavailable && !item.suspended && !item.retired ? 不可用 : null} - {item.stale ? 缓存 : null} +
+
+ +
+

{item.title}

+

+ {pluginCardSummary(item)} +

+
+
+
+ + {primaryCommand ? ( + + ) : ( + + )}
- {item.title} -

{item.summary}

-
-
当前项目
{PROJECT_TEXT[item.projectState]}
-
本机状态
{item.source === 'local' ? (item.localEnabled ? '本机全局已启用' : '本机全局已停用') : item.official?.installation?.version ? `官方包 ${item.official.installation.version}` : isBundledOfficialPlugin(item) ? '随应用提供' : '未下载官方包'}
-
{agentLabel(item)}
{agentText(item)}
-
计费
{BILLING_TEXT[item.billing]}
-
- {item.deviceReason ? ( -

- {item.official?.installation?.version ? '旧设备版本已保留' : '设备包不可用'}:{item.deviceReason} -

- ) : null} -
); } export interface PluginsViewProps { projection: PluginWorkspaceProjection; - filters: PluginWorkspaceFilters; activeProjectId: string | null; activeProjectName: string | null; sourceErrors: readonly PluginWorkspaceSourceError[]; sourceLoading: readonly PluginWorkspaceSourceName[]; dataService: DataServiceInstanceState | null; dataServicePending: Record; - onFiltersChange(filters: PluginWorkspaceFilters): void; - onRefresh(): void | Promise; onSelect(key: PluginWorkspaceItem['key'] | null): void; onCommand(command: PluginWorkspaceCommand): void | Promise; isCommandPending(command: PluginWorkspaceCommand): boolean; @@ -172,14 +167,6 @@ export function PluginsView(props: PluginsViewProps) { detailTriggerRef.current = trigger; props.onSelect(item.key); }; - const updateFilters = (patch: Partial) => { - props.onFiltersChange({ ...props.filters, ...patch, selectedKey: null }); - }; - const search = (event: FormEvent) => { - event.preventDefault(); - const data = new FormData(event.currentTarget); - updateFilters({ search: String(data.get('plugin-search') ?? '').trim() }); - }; const closeDetails = () => { const trigger = detailTriggerRef.current; props.onSelect(null); @@ -193,13 +180,7 @@ export function PluginsView(props: PluginsViewProps) { data-testid="plugins-page" className={cn('w-full text-foreground', !props.embedded && 'mx-auto max-w-7xl')} > - {props.embedded ? ( -
- -
- ) :
+ {!props.embedded ?
{props.onBackToProjectSettings ? (
- - } - -
-
- - - -
- -
-
- - -
- -
-
-
-
+ : null} {props.projection.notices.map((notice) => (

@@ -304,53 +212,38 @@ export function PluginsView(props: PluginsViewProps) {

))} {props.sourceLoading.length ? ( -

- 正在刷新 {props.sourceLoading.map((source) => SOURCE_ERROR_TITLE[source]).join('、')}… -

+

正在加载插件…

) : null} {props.sourceErrors.length ? ( -
+
{props.sourceErrors.map((error) => (

- {SOURCE_ERROR_TITLE[error.source]}刷新失败。 {error.message} + {SOURCE_ERROR_TITLE[error.source]}加载失败。 {error.message}

))}
) : null} {props.projection.items.length ? ( -
- {props.filters.scope === 'project' ? ( -
- {([ - { id: 'project', label: '当前项目插件', items: props.projection.items.filter((item) => item.source === 'official') }, - { id: 'local', label: '本机全局生效', items: props.projection.items.filter((item) => item.source === 'local') }, - { id: 'retained', label: '配置保留', items: props.projection.items.filter((item) => item.source === 'retained') }, - ] as const).map((group) => group.items.length ? ( -
-

{group.label}

-
- {group.items.map((item) => ( - openDetails(item, trigger)} /> - ))} -
-
- ) : null)} -
- ) : ( -
- {props.projection.items.map((item) => ( - openDetails(item, trigger)} /> - ))} -
- )} +
+
+ {props.projection.items.map((item) => ( + openDetails(item, trigger)} + onCommand={props.onCommand} + isCommandPending={props.isCommandPending} + /> + ))} +
) : ( -
+
-

没有符合筛选条件的插件

-

调整范围、来源、状态或搜索词后再试。

+

暂时没有可用插件

+

有新的插件能力时会显示在这里。

)} @@ -638,15 +531,6 @@ export function Plugins({ embedded = false }: { embedded?: boolean } = {}) { case 'open_settings': return false; } }; - const refresh = () => settlePluginWorkspaceLoads({ - catalog: async () => await pluginMarketplaceStore.getState().loadCatalog({ - limit: 24, - ...(filters.search ? { query: filters.search } : {}), - }), - ...(accountKey ? { library: async () => await pluginMarketplaceStore.getState().loadLibrary() } : {}), - device: async () => await devicePackageStore.getState().load(), - ...(activeProject ? { project: async () => await codingPluginsStore.getState().load(activeProject.id) } : {}), - }).then(() => undefined); const dispatch = (command: PluginWorkspaceCommand) => safe(dispatchPluginWorkspaceCommand(command, { marketplace: pluginMarketplaceStore.getState(), device: devicePackageStore.getState(), @@ -663,15 +547,12 @@ export function Plugins({ embedded = false }: { embedded?: boolean } = {}) { return ( ; - library?: () => Promise; - device(): Promise; - project?: () => Promise; -} - -export function settlePluginWorkspaceLoads( - loaders: PluginWorkspaceLoaders, -): Promise[]> { - const available = [loaders.catalog, loaders.library, loaders.device, loaders.project] - .filter((load): load is () => Promise => Boolean(load)); - return Promise.allSettled(available.map((load) => load())); -} diff --git a/src/pages/ProjectConfiguration/index.tsx b/src/pages/ProjectConfiguration/index.tsx index e917f08..ef130d5 100644 --- a/src/pages/ProjectConfiguration/index.tsx +++ b/src/pages/ProjectConfiguration/index.tsx @@ -35,6 +35,15 @@ type SkillInfo = { entries: SkillStructureEntry[]; }; +const THINKING_LEVEL_LABELS = { + off: '关闭', + minimal: '极简', + low: '低', + medium: '中等', + high: '高', + max: '最高', +} as const; + function createCustomAgent(index: number, modelKey: string): CodingProjectAgent { const now = new Date().toISOString(); const model = parseCodingModelKey(modelKey); @@ -58,8 +67,8 @@ function createCustomAgent(index: number, modelKey: string): CodingProjectAgent } function ResourceCard({ id, icon, title, subtitle, onClick }: { id: string; icon: React.ReactNode; title: string; subtitle: string; onClick: () => void }) { - return ; } @@ -72,11 +81,11 @@ function PluginWorkspaceSheet({ open, onClose }: { open: boolean; onClose: () => return { if (!nextOpen) onClose(); }}> @@ -116,17 +125,26 @@ function ModelCard({ model }: { model: CodingModelOption }) {
-
-

{model.label}

- 已配置 +

{model.modelId}

+
+ 可选思考强度 + {model.availableThinkingLevels === null + ? 进入对话后可查看 + : model.availableThinkingLevels.length === 0 + || model.availableThinkingLevels.every((level) => level === 'off') + ? 不支持调节 + : model.availableThinkingLevels.map((level) => ( + + {THINKING_LEVEL_LABELS[level]} + + ))}
-

{model.accountId}

; } -function ModelList({ models }: { models: CodingModelOption[] }) { +export function ModelList({ models }: { models: CodingModelOption[] }) { if (models.length === 0) { return
当前没有可用模型,请先在模型设置中完成配置。智能体的默认模型在智能体配置中设置。 @@ -317,7 +335,7 @@ export function ProjectConfiguration() {

项目配置

-
} title="可用模型" subtitle={`${modelOptions.length} 个可用模型`} onClick={() => setDrawerMode('models')} />} title="可用技能" subtitle={`${skills.length} 项可用技能`} onClick={() => { setActiveSkillId(null); setDrawerMode('skills'); }} />} title="知识文件" subtitle={`${knowledge.length} 个知识文件`} onClick={() => setDrawerMode('knowledge')} />} title="插件" subtitle="管理获取、启用与分配状态" onClick={openPluginWorkspace} />
+
} title="可用模型" subtitle={`${modelOptions.length} 个可用模型`} onClick={() => setDrawerMode('models')} />} title="可用技能" subtitle={`${skills.length} 项可用技能`} onClick={() => { setActiveSkillId(null); setDrawerMode('skills'); }} />} title="知识文件" subtitle={`${knowledge.length} 个知识文件`} onClick={() => setDrawerMode('knowledge')} />} title="插件" subtitle="为智能体拓展更多能力" onClick={openPluginWorkspace} />

项目智能体

智能体配置仅属于当前项目;每个智能体可以拥有多条独立对话。

{draft.agents.filter((agent) => !agent.archivedAt).map((agent) => { setActiveAgentId(agent.id); setPartnerDialogOpen(true); }} />)}{draft.agents.every((agent) => agent.archivedAt) ?
当前项目还没有智能体,点击右上角的 + 创建。
: null}
{draft.agents.some((agent) => agent.archivedAt) ?

已归档

{draft.agents.filter((agent) => agent.archivedAt).map((agent) => )}
: null}
@@ -349,7 +367,7 @@ export function ProjectConfiguration() { submitLabel={activeAgent ? '保存智能体' : '创建智能体'} /> { if (!open) { setDrawerMode(null); setActiveSkillId(null); } }}> - {drawerMode === 'models' ? setDrawerMode(null)}> : null} + {drawerMode === 'models' ? setDrawerMode(null)}> : null} {drawerMode === 'knowledge' ? setDrawerMode(null)}> { void handleKnowledge(event.target.files?.[0]); event.currentTarget.value = ''; }} />
{knowledge.length ? knowledge.map((file) =>
{file}
) :
当前还没有知识文件。
}
: null} {drawerMode === 'skills' && activeSkill ? setActiveSkillId(null)} closeLabel="返回技能列表" closeIcon="back"> : null} {drawerMode === 'skills' && !activeSkill ? setDrawerMode(null)}>
{skills.length > 0 ? skills.map((skill) => ) :
当前没有可查看的技能。
}
: null} diff --git a/tests/e2e/plugin-marketplace.spec.ts b/tests/e2e/plugin-marketplace.spec.ts index 3fa243e..7d014aa 100644 --- a/tests/e2e/plugin-marketplace.spec.ts +++ b/tests/e2e/plugin-marketplace.spec.ts @@ -93,17 +93,20 @@ test.describe('Unified plugin workspace', () => { await expect(page.getByTestId('project-plugins-sheet')).toBeVisible(); await expect(page.getByTestId('plugins-page')).toBeVisible(); await expect(page).toHaveURL(/\/project-config\/plugins\?scope=all/); - await expect(page.getByTestId('plugins-page').getByText('尚未获取')).toBeVisible(); + await expect(page.getByText('为你的智能体添加插件,拓展更多能力。')).toBeVisible(); + await expect(page.getByRole('button', { name: /刷新/ })).toHaveCount(0); + await expect(page.getByRole('searchbox')).toHaveCount(0); + await expect(page.getByRole('combobox')).toHaveCount(0); + await expect(page.getByRole('button', { name: '添加灵感笔记' })).toBeVisible(); await page.getByRole('button', { name: '查看灵感笔记详情' }).click(); await expect(page.getByRole('dialog', { name: '灵感笔记' })).toContainText('详细介绍'); - await page.getByRole('button', { name: '免费获取灵感笔记' }).click(); - await expect(page.getByRole('button', { name: '从账号移除灵感笔记' })).toBeVisible(); + await expect(page.getByRole('dialog', { name: '灵感笔记' })).not.toContainText('读取项目名称'); + await page.getByRole('button', { name: '添加灵感笔记' }).click(); + await expect(page.getByRole('button', { name: '从插件库移除灵感笔记' })).toBeVisible(); await page.getByRole('button', { name: '关闭插件详情' }).click(); - await page.getByRole('combobox', { name: '状态', exact: true }).selectOption('mine'); - await expect(page).toHaveURL(/state=mine/); await expect(page.getByRole('heading', { name: '灵感笔记' })).toBeVisible(); - await expect(page.getByText('不适用项目启用')).toBeVisible(); + await expect(page.getByRole('button', { name: '安装灵感笔记' })).toBeVisible(); const requests = await app.evaluate(() => ( (globalThis as typeof globalThis & { __pluginsE2E?: { requests: string[] } }).__pluginsE2E?.requests ?? [] )); @@ -157,17 +160,21 @@ test.describe('Unified plugin workspace', () => { await expect(page.getByTestId('project-configuration-empty-state')).toBeVisible(); await expect(page.getByTestId('project-plugins-sheet')).toBeVisible(); await expect(page.getByTestId('plugins-page')).toBeVisible(); - await expect(page.getByRole('alert')).toContainText('官方目录刷新失败'); + await expect(page.getByRole('alert')).toContainText('官方目录加载失败'); await expect(page.getByRole('heading', { name: 'Local Search' })).toBeVisible(); - await expect(page.getByText('本机全局已启用')).toBeVisible(); - await expect(page.getByText(/仅通过对话安装/)).toBeVisible(); + await expect(page.getByRole('button', { name: 'Local Search:已安装' })).toBeDisabled(); + await expect(page.getByRole('button', { name: /刷新/ })).toHaveCount(0); + await expect(page.getByRole('searchbox')).toHaveCount(0); + await expect(page.getByRole('combobox')).toHaveCount(0); await expect(page.getByRole('textbox', { name: /包|路径|source/i })).toHaveCount(0); await page.getByRole('button', { name: '查看Local Search详情' }).click(); - await expect(page.getByText(/完整桌面权限/)).toBeVisible(); - await page.getByRole('button', { name: '本机全局停用Local Search' }).click(); - await expect(page.getByRole('button', { name: '本机全局启用Local Search' })).toBeVisible(); - await page.getByRole('button', { name: '本机全局启用Local Search' }).click(); - await expect(page.getByRole('button', { name: '本机全局停用Local Search' })).toBeVisible(); + await expect(page.getByText('为智能体提供可在本机使用的扩展能力。')).toBeVisible(); + await expect(page.getByText(/完整桌面权限/)).toHaveCount(0); + await expect(page.getByText(/Pi extension|Skill 脚本/)).toHaveCount(0); + await page.getByRole('button', { name: '停用Local Search' }).click(); + await expect(page.getByRole('button', { name: '启用Local Search' })).toBeVisible(); + await page.getByRole('button', { name: '启用Local Search' }).click(); + await expect(page.getByRole('button', { name: '停用Local Search' })).toBeVisible(); const requests = await app.evaluate(() => ( (globalThis as typeof globalThis & { __pluginsE2E?: { requests: string[] } }).__pluginsE2E?.requests ?? [] @@ -291,24 +298,23 @@ test.describe('Unified plugin workspace', () => { await expect.poll(async () => app.evaluate(() => ( (globalThis as typeof globalThis & { __pluginsE2E?: { requests: string[] } }).__pluginsE2E?.requests ?? [] ))).toContain('GET /api/coding/plugin-marketplace/library'); - const bundledCard = page.getByRole('heading', { name: '游戏资源生成' }).locator('..'); - await expect(bundledCard).toContainText('账号已获取'); + await expect(page.getByRole('button', { name: '游戏资源生成:随应用提供' })).toBeDisabled(); await page.getByRole('button', { name: '查看开发数据服务详情' }).click(); - await expect(page.getByText('当前状态没有可执行操作。')).toBeVisible(); + await expect(page.getByText('当前无需进行其他操作。')).toBeVisible(); await page.getByRole('button', { name: '关闭插件详情' }).click(); await page.getByRole('button', { name: '查看游戏资源生成详情' }).click(); - await expect(page.getByRole('button', { name: '从账号移除游戏资源生成' })).toBeVisible(); - await expect(page.getByRole('button', { name: /下载.*游戏资源生成|删除.*游戏资源生成.*官方设备包/ })).toHaveCount(0); - await page.getByRole('button', { name: '从账号移除游戏资源生成' }).click(); - await page.getByRole('button', { name: '确认从账号移除' }).click(); + await expect(page.getByRole('button', { name: '从插件库移除游戏资源生成' })).toBeVisible(); + await expect(page.getByRole('button', { name: /安装.*游戏资源生成|从本机卸载.*游戏资源生成/ })).toHaveCount(0); + await page.getByRole('button', { name: '从插件库移除游戏资源生成' }).click(); + await page.getByRole('button', { name: '确认移除' }).click(); await page.getByRole('button', { name: '关闭插件详情' }).click(); await page.getByRole('button', { name: '查看Local Tool详情' }).click(); - await expect(page.getByRole('button', { name: '本机全局停用Local Tool' })).toBeVisible(); - await expect(page.getByRole('button', { name: /账号.*Local Tool|下载.*Local Tool/ })).toHaveCount(0); - await page.getByRole('button', { name: '本机全局停用Local Tool' }).click(); + await expect(page.getByRole('button', { name: '停用Local Tool' })).toBeVisible(); + await expect(page.getByRole('button', { name: /插件库.*Local Tool|安装.*Local Tool/ })).toHaveCount(0); + await page.getByRole('button', { name: '停用Local Tool' }).click(); const requests = await app.evaluate(() => ( (globalThis as typeof globalThis & { __pluginsE2E?: { requests: string[] } }).__pluginsE2E?.requests ?? [] diff --git a/tests/e2e/project-configuration-skills.spec.ts b/tests/e2e/project-configuration-skills.spec.ts index 4cca7ad..94e73d7 100644 --- a/tests/e2e/project-configuration-skills.spec.ts +++ b/tests/e2e/project-configuration-skills.spec.ts @@ -48,18 +48,40 @@ test.describe('Project configuration skills', () => { expect(actionBarBottomGap).toBeLessThan(40); await expect(page.getByRole('button', { name: '创建智能体' })).toBeVisible(); + const resourceCardGeometry = await page.getByTestId('project-resource-row').locator(':scope > button').evaluateAll((cards) => cards.map((card) => { + const rect = card.getBoundingClientRect(); + return { left: rect.left, top: rect.top, width: rect.width }; + })); + expect(resourceCardGeometry).toHaveLength(4); + expect(Math.max(...resourceCardGeometry.map(({ top }) => top)) - Math.min(...resourceCardGeometry.map(({ top }) => top))).toBeLessThanOrEqual(1); + expect(resourceCardGeometry.every(({ width }) => width > 0)).toBe(true); + expect(resourceCardGeometry.map(({ left }) => left)).toEqual([...resourceCardGeometry.map(({ left }) => left)].sort((a, b) => a - b)); + + const drawerWidths: number[] = []; for (const [resourceId, title] of [ ['resource-card-models', '可用模型'], - ['resource-card-knowledge', '知识文件'], ['resource-card-skills', '可用技能'], + ['resource-card-knowledge', '知识文件'], + ['resource-card-plugins', '插件'], ] as const) { await page.getByTestId(resourceId).click(); const drawer = page.getByRole('dialog', { name: title }); await expect(drawer).toBeVisible(); + const drawerBox = await drawer.boundingBox(); + if (!drawerBox) throw new Error(`未能读取 ${title} 抽屉的布局区域`); + drawerWidths.push(drawerBox.width); if (resourceId === 'resource-card-models') { await expect(drawer.getByRole('combobox')).toHaveCount(0); await expect(drawer.locator('[data-testid="project-model-list"], [data-testid="project-model-empty-state"]')).toHaveCount(1); } + if (resourceId === 'resource-card-plugins') { + await expect(page.getByTestId('project-plugins-sheet')).toBeVisible(); + await expect(drawer.getByTestId('plugins-page')).toBeVisible(); + await expect(drawer.getByText('为你的智能体添加插件,拓展更多能力。')).toBeVisible(); + await expect(drawer.getByRole('button', { name: /刷新/ })).toHaveCount(0); + await expect(drawer.getByRole('searchbox')).toHaveCount(0); + await expect(drawer.getByRole('combobox')).toHaveCount(0); + } const closeButton = drawer.getByRole('button', { name: '关闭' }); const closeGeometry = await closeButton.evaluate((button) => { const icon = button.querySelector('svg'); @@ -81,6 +103,8 @@ test.describe('Project configuration skills', () => { await closeButton.locator('svg').click(); await expect(page.getByRole('dialog', { name: title })).toHaveCount(0); } + expect(Math.max(...drawerWidths) - Math.min(...drawerWidths)).toBeLessThanOrEqual(1); + expect(Math.max(...drawerWidths)).toBeLessThanOrEqual(430); const now = new Date().toISOString(); await mkdir(path.join(parentPath, '.makelore'), { recursive: true }); diff --git a/tests/e2e/project-plugins.spec.ts b/tests/e2e/project-plugins.spec.ts index 5e54ce7..1c02c22 100644 --- a/tests/e2e/project-plugins.spec.ts +++ b/tests/e2e/project-plugins.spec.ts @@ -103,10 +103,11 @@ test.describe('Unified project plugin workspace', () => { includeHidden: true, })).toBeAttached(); await page.getByRole('button', { name: '查看开发数据服务详情' }).click(); - await expect(page.getByText('按平台包含,不按单次插件调用扣点。')).toBeVisible(); - await expect(page.getByRole('heading', { name: '生效范围' })).toBeVisible(); - await expect(page.getByText(/启用当前项目后.*无需单独分配/)).toBeVisible(); - await expect(page.getByRole('button', { name: '分配开发数据服务给伙伴' })).toHaveCount(0); + const pluginDialog = page.getByRole('dialog', { name: '开发数据服务' }); + await expect(pluginDialog).toContainText('为当前项目提供数据。'); + await expect(pluginDialog).not.toContainText('Token Point'); + await expect(pluginDialog).not.toContainText('data-service.control'); + await expect(page.getByRole('button', { name: '将开发数据服务分配给智能体' })).toHaveCount(0); const localProjectId = await app.evaluate(() => ( (globalThis as typeof globalThis & { __projectPluginE2E?: { localProjectId: string } }).__projectPluginE2E?.localProjectId )); @@ -114,15 +115,15 @@ test.describe('Unified project plugin workspace', () => { await expect(page.getByText(localProjectId as string)).toHaveCount(0); await expect(page.getByText('11111111-1111-4111-8111-111111111111')).toHaveCount(0); - await page.getByRole('button', { name: '启用开发数据服务到当前项目' }).click(); - await expect(page.getByRole('button', { name: '从当前项目禁用开发数据服务' })).toBeVisible(); - await expect(page.getByText('插件专属设置')).toBeVisible(); + await page.getByRole('button', { name: '将开发数据服务添加到项目' }).click(); + await expect(page.getByRole('button', { name: '从项目移除开发数据服务' })).toBeVisible(); + await expect(page.getByText('使用设置')).toBeVisible(); await expect(page.getByRole('button', { name: '创建开发数据空间' })).toBeDisabled(); - await page.getByRole('button', { name: '从当前项目禁用开发数据服务' }).click(); - await expect(page.getByRole('alertdialog')).toContainText('从当前项目禁用开发数据服务'); - await page.getByRole('button', { name: '确认从当前项目禁用' }).click(); - await expect(page.getByRole('button', { name: '启用开发数据服务到当前项目' })).toBeVisible(); + await page.getByRole('button', { name: '从项目移除开发数据服务' }).click(); + await expect(page.getByRole('alertdialog')).toContainText('从当前项目移除开发数据服务'); + await page.getByRole('button', { name: '确认从项目移除' }).click(); + await expect(page.getByRole('button', { name: '将开发数据服务添加到项目' })).toBeVisible(); const state = await app.evaluate(() => ( (globalThis as typeof globalThis & { diff --git a/tests/unit/agent-creation-dialog.test.tsx b/tests/unit/agent-creation-dialog.test.tsx index d566ed3..788d055 100644 --- a/tests/unit/agent-creation-dialog.test.tsx +++ b/tests/unit/agent-creation-dialog.test.tsx @@ -35,6 +35,7 @@ describe('AgentCreationDialog plugin Skill availability', () => { accountId: 'account-a', modelId: 'model-a', label: 'Account A / Model A', + availableThinkingLevels: null, }]} skills={[{ id: 'data-service', diff --git a/tests/unit/coding-feature-ui.test.tsx b/tests/unit/coding-feature-ui.test.tsx index 984b194..0daaee6 100644 --- a/tests/unit/coding-feature-ui.test.tsx +++ b/tests/unit/coding-feature-ui.test.tsx @@ -118,10 +118,67 @@ describe('PI-130 feature-complete Coding UI', () => { expect(options.map((option) => option.modelId)).toEqual(['model-a', 'model-b']); expect(options[0].label).toBe('Work account · Custom vendor / model-a'); + expect(options[0]).toMatchObject({ + availableThinkingLevels: null, + }); expect(parseCodingModelKey(options[0].key)).toEqual({ accountId: 'account-1', modelId: 'model-a' }); expect(JSON.stringify(options)).not.toMatch(/opencode|providerID/i); }); + it('projects only verified selectable thinking strengths for configured models', async () => { + const { buildCodingModelOptions } = await import('@/lib/coding-model-options'); + const account = { + id: 'account-reasoning', + vendorId: 'custom', + label: 'Reasoning account', + authMode: 'api_key', + model: 'server-model', + fallbackModels: ['deepseek-v4-pro', 'qwen3.8-max', 'unknown-model'], + enabled: true, + isDefault: true, + metadata: { + worksSquareModelCapabilities: { + 'server-model': { + reasoningEfforts: ['max', 'low'], + reasoningCanDisable: true, + }, + }, + }, + createdAt: '2026-08-24T00:00:00.000Z', + updatedAt: '2026-08-24T00:00:00.000Z', + } satisfies ProviderAccount; + + const options = buildCodingModelOptions([account], []); + + expect(Object.fromEntries(options.map((option) => [option.modelId, option.availableThinkingLevels]))).toEqual({ + 'server-model': ['off', 'low', 'max'], + 'deepseek-v4-pro': ['off', 'low', 'high', 'max'], + 'qwen3.8-max': ['off', 'low', 'medium', 'high'], + 'unknown-model': null, + }); + }); + + it('shows the exact configured model name and its selectable thinking strengths', async () => { + const { ModelList } = await import('@/pages/ProjectConfiguration'); + render(); + + const card = screen.getByTestId(/model-card-/); + expect(card).toHaveTextContent('qwen3.8-max'); + expect(card).toHaveTextContent('可选思考强度'); + for (const level of ['关闭', '低', '中等', '高']) { + expect(within(card).getByText(level)).toBeVisible(); + } + expect(card).not.toHaveTextContent('account-a'); + expect(card).not.toHaveTextContent('团队模型'); + expect(card).not.toHaveTextContent('已配置'); + }); + it('exposes steer/follow-up modes and queue waiting state while a turn runs', async () => { const onModeChange = vi.fn(); const onSubmit = vi.fn(); diff --git a/tests/unit/pi-product-artifact.test.ts b/tests/unit/pi-product-artifact.test.ts index 8f7ab20..cedf1b9 100644 --- a/tests/unit/pi-product-artifact.test.ts +++ b/tests/unit/pi-product-artifact.test.ts @@ -36,7 +36,7 @@ const MARKETPLACE_ARTIFACT_TEXT = [ 'plugin-marketplace\\/install\\/', 'plugin-marketplace\\/update\\/', 'effectiveSkillIds', 'pluginReleaseIds', '/api/coding/plugin-marketplace/catalog', '/api/coding/plugin-marketplace/library', - '免费获取', '获取、项目启用与伙伴分配彼此独立', '/project-config/plugins', + '为你的智能体添加插件,拓展更多能力。', 'acquire', 'install_stable', 'enable_project', '/project-config/plugins', ].join('\n'); async function createAsarFixture(source: string, archive: string) { @@ -389,7 +389,7 @@ describe('final Pi product artifact verification', () => { 'makelore-device-package.v1 /api/coding/device-packages device-parent-workers', '/api/coding/plugin-marketplace plugin-marketplace\\/install\\/ plugin-marketplace\\/update\\/', 'effectiveSkillIds pluginReleaseIds', - '/api/coding/plugin-marketplace/catalog /api/coding/plugin-marketplace/library 免费获取 获取、项目启用与伙伴分配彼此独立 /project-config/plugins', + '/api/coding/plugin-marketplace/catalog /api/coding/plugin-marketplace/library 为你的智能体添加插件,拓展更多能力。 acquire install_stable enable_project /project-config/plugins', ].join('\n')); const appAsar = path.join(root, 'app.asar'); await createAsarFixture(source, appAsar); @@ -407,7 +407,7 @@ describe('final Pi product artifact verification', () => { 'makelore.game-resource /api/plugins/v1/hosted/game-resource/generations', '/api/coding/plugin-marketplace plugin-marketplace\\/install\\/ plugin-marketplace\\/update\\/', 'effectiveSkillIds pluginReleaseIds', - '/api/coding/plugin-marketplace/catalog /api/coding/plugin-marketplace/library 免费获取 我的插件 /project-config/plugins', + '/api/coding/plugin-marketplace/catalog /api/coding/plugin-marketplace/library 为你的智能体添加插件,拓展更多能力。 acquire enable_project /project-config/plugins', ].join('\n')); const staleAsar = path.join(root, 'stale.asar'); await createAsarFixture(staleSource, staleAsar); diff --git a/tests/unit/plugin-workspace-controller.test.ts b/tests/unit/plugin-workspace-controller.test.ts index 33612a6..8958395 100644 --- a/tests/unit/plugin-workspace-controller.test.ts +++ b/tests/unit/plugin-workspace-controller.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; import { dispatchPluginWorkspaceCommand, - settlePluginWorkspaceLoads, type PluginWorkspaceDispatchDependencies, } from '@/pages/Plugins/plugin-workspace-controller'; import type { PluginWorkspaceCommand } from '@/pages/Plugins/plugin-workspace-model'; @@ -70,19 +69,4 @@ describe('plugin workspace controller', () => { ]); expect(deps.openSettings).toHaveBeenCalledExactlyOnceWith(); }); - - it('starts every available source load even when one source rejects', async () => { - const catalog = vi.fn().mockRejectedValue(new Error('catalog offline')); - const library = vi.fn().mockResolvedValue(undefined); - const device = vi.fn().mockResolvedValue(undefined); - const project = vi.fn().mockResolvedValue(undefined); - - const results = await settlePluginWorkspaceLoads({ catalog, library, device, project }); - - expect(catalog).toHaveBeenCalledOnce(); - expect(library).toHaveBeenCalledOnce(); - expect(device).toHaveBeenCalledOnce(); - expect(project).toHaveBeenCalledOnce(); - expect(results.map(({ status }) => status)).toEqual(['rejected', 'fulfilled', 'fulfilled', 'fulfilled']); - }); }); diff --git a/tests/unit/plugins-page.test.tsx b/tests/unit/plugins-page.test.tsx index 7260b7a..0b9873d 100644 --- a/tests/unit/plugins-page.test.tsx +++ b/tests/unit/plugins-page.test.tsx @@ -9,15 +9,10 @@ import { codingWorkspaceStore } from '@/stores/coding-workspace'; import { devicePackageStore } from '@/stores/device-packages'; import { pluginMarketplaceStore } from '@/stores/plugin-marketplace'; import type { - PluginWorkspaceFilters, PluginWorkspaceItem, PluginWorkspaceProjection, } from '@/pages/Plugins/plugin-workspace-model'; -const filters: PluginWorkspaceFilters = { - scope: 'project', source: 'all', state: 'all', search: '', selectedKey: null, -}; - const initialAuthState = useAuthStore.getInitialState(); const initialCodingPluginsState = codingPluginsStore.getInitialState(); const initialCodingWorkspaceState = codingWorkspaceStore.getInitialState(); @@ -181,15 +176,12 @@ function projection(selected: PluginWorkspaceItem | null = null): PluginWorkspac function props(selected: PluginWorkspaceItem | null = null) { return { projection: projection(selected), - filters: { ...filters, selectedKey: selected?.key ?? null }, activeProjectId: 'project-a', activeProjectName: 'Project A', sourceErrors: [], sourceLoading: [], dataService: null, dataServicePending: {}, - onFiltersChange: vi.fn(), - onRefresh: vi.fn(), onSelect: vi.fn(), onCommand: vi.fn(), isCommandPending: vi.fn().mockReturnValue(false), @@ -248,15 +240,21 @@ function preparePluginsRouteState() { } describe('PluginsView', () => { - it('renders as project-configuration drawer content without a duplicate standalone header', () => { + it('renders a direct Skill-like list in the project drawer without filters or refresh', () => { render(); expect(screen.getByTestId('plugins-page')).toBeVisible(); - expect(screen.getByRole('button', { name: '刷新全部来源' })).toBeVisible(); expect(screen.queryByRole('heading', { name: '插件', level: 1 })).not.toBeInTheDocument(); + expect(screen.getByTestId('plugin-list')).toHaveClass('space-y-3'); + expect(screen.queryByRole('button', { name: /刷新/ })).not.toBeInTheDocument(); + expect(screen.queryByRole('searchbox')).not.toBeInTheDocument(); + expect(screen.queryByRole('combobox')).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: '查看开发数据服务详情' })).toBeVisible(); + expect(screen.getByRole('button', { name: '开发数据服务:已添加到项目' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Pi Web Search:已安装' })).toBeDisabled(); }); - it('keeps the production URL, scope control, and fallback notice aligned across in-place changes', async () => { + it('keeps the production URL and fallback notice aligned without exposing query controls', async () => { preparePluginsRouteState(); render( @@ -267,7 +265,7 @@ describe('PluginsView', () => { await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent( '/project-config/plugins?scope=all&source=all&state=all', )); - expect(screen.getByLabelText('范围')).toHaveValue('all'); + expect(screen.queryByLabelText('范围')).not.toBeInTheDocument(); expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible(); const activeProject = { @@ -282,23 +280,23 @@ describe('PluginsView', () => { await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent( '/project-config/plugins?scope=project&source=all&state=all', )); - expect(screen.getByLabelText('范围')).toHaveValue('project'); + expect(screen.queryByLabelText('范围')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '打开非法插件地址' })); await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent( '/project-config/plugins?scope=project&source=all&state=all', )); - expect(screen.getByLabelText('范围')).toHaveValue('project'); + expect(screen.queryByLabelText('范围')).not.toBeInTheDocument(); act(() => codingWorkspaceStore.setState({ activeProjectId: null, activeProject: null })); await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent( '/project-config/plugins?scope=all&source=all&state=all', )); - expect(screen.getByLabelText('范围')).toHaveValue('all'); + expect(screen.queryByLabelText('范围')).not.toBeInTheDocument(); expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible(); }); - it('keeps old device and project rows visible and labels each affected snapshot cached after refresh errors', async () => { + it('keeps available rows visible when individual sources fail to load', async () => { const activeProject = { id: 'project-a', name: 'Project A', @@ -349,10 +347,10 @@ describe('PluginsView', () => { render(); for (const name of ['开发数据服务', 'Pi Web Search', 'makelore.retained']) { - const heading = await screen.findByRole('heading', { name }); - expect(heading.closest('article')).toHaveTextContent('缓存'); + expect(await screen.findByRole('heading', { name })).toBeVisible(); } expect(screen.getAllByRole('alert')).toHaveLength(2); + expect(screen.queryByText('缓存')).not.toBeInTheDocument(); }); it('attempts a rejected official detail load once per open and retries only after close and reopen', async () => { @@ -416,7 +414,7 @@ describe('PluginsView', () => { await Promise.resolve(); }); expect(loadDetail).toHaveBeenCalledTimes(1); - expect(screen.getByRole('alert', { hidden: true })).toHaveTextContent('插件详情刷新失败。 detail offline'); + expect(screen.getByRole('alert', { hidden: true })).toHaveTextContent('插件详情加载失败。 detail offline'); fireEvent.click(screen.getByRole('button', { name: '关闭插件详情' })); await waitFor(() => expect(screen.queryByRole('dialog', { name: 'Notes' })).not.toBeInTheDocument()); @@ -424,7 +422,7 @@ describe('PluginsView', () => { await waitFor(() => expect(loadDetail).toHaveBeenCalledTimes(2)); }); - it('renders the unified workspace and exposes URL-backed filter changes', () => { + it('renders the capability-first list with details and contextual actions', () => { const view = props(); view.sourceErrors = [ { source: 'catalog', message: 'catalog offline' }, @@ -433,27 +431,15 @@ describe('PluginsView', () => { render(); expect(screen.getByRole('heading', { name: '插件' })).toBeVisible(); - expect(screen.getByText(/MakeLore 运营发布官方插件/)).toBeVisible(); - expect(screen.getByText(/获取与项目启用彼此独立.*需要定向生效的插件还可分配伙伴/)).toBeVisible(); + expect(screen.getByText('为你的智能体添加插件,拓展更多能力。')).toBeVisible(); expect(screen.getByText('开发数据服务')).toBeVisible(); expect(screen.getByText('Pi Web Search')).toBeVisible(); - expect(screen.getByRole('heading', { name: '本机全局生效' })).toBeVisible(); - expect(screen.getByText('本机全局已启用')).toBeVisible(); - expect(screen.getByText('随 MakeLore 提供')).toBeVisible(); - expect(screen.getByText('随项目启用')).toBeVisible(); + expect(screen.getByRole('button', { name: '查看开发数据服务详情' })).toBeVisible(); + expect(screen.getByRole('button', { name: '开发数据服务:已添加到项目' })).toBeDisabled(); expect(screen.getAllByRole('alert')).toHaveLength(2); - expect(screen.queryByRole('textbox', { name: /包|路径|source/i })).not.toBeInTheDocument(); - expect(screen.getByText(/通过对话安装/)).toBeVisible(); - - fireEvent.change(screen.getByLabelText('范围'), { target: { value: 'all' } }); - expect(view.onFiltersChange).toHaveBeenCalledWith(expect.objectContaining({ scope: 'all' })); - fireEvent.change(screen.getByLabelText('来源'), { target: { value: 'local' } }); - expect(view.onFiltersChange).toHaveBeenCalledWith(expect.objectContaining({ source: 'local' })); - fireEvent.change(screen.getByLabelText('状态'), { target: { value: 'enabled' } }); - expect(view.onFiltersChange).toHaveBeenCalledWith(expect.objectContaining({ state: 'enabled' })); - fireEvent.change(screen.getByRole('searchbox', { name: '搜索插件' }), { target: { value: 'web' } }); - fireEvent.submit(screen.getByRole('search')); - expect(view.onFiltersChange).toHaveBeenCalledWith(expect.objectContaining({ search: 'web' })); + expect(screen.queryByRole('button', { name: /刷新/ })).not.toBeInTheDocument(); + expect(screen.queryByRole('searchbox')).not.toBeInTheDocument(); + expect(screen.queryByRole('combobox')).not.toBeInTheDocument(); }); it('labels an unacquired code-owned bundled plugin as supplied by MakeLore instead of not downloaded', () => { @@ -491,12 +477,11 @@ describe('PluginsView', () => { render(); - expect(screen.getAllByText('随应用提供')).toHaveLength(2); - expect(screen.queryByText('未下载官方包')).not.toBeInTheDocument(); - expect(screen.getByText('随项目启用')).toBeVisible(); - expect(screen.getByRole('heading', { name: '生效范围' })).toBeVisible(); - expect(screen.getByText(/启用当前项目后.*无需单独分配/)).toBeVisible(); - expect(screen.queryByRole('heading', { name: '伙伴分配' })).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'MakeLore 项目脚手架:随应用提供', hidden: true })).toBeDisabled(); + const dialog = screen.getByRole('dialog', { name: 'MakeLore 项目脚手架' }); + expect(dialog).toHaveTextContent('此插件随应用提供,添加到项目后即可使用。'); + expect(dialog).not.toHaveTextContent('未下载官方包'); + expect(dialog).not.toHaveTextContent('生效范围'); }); it('shows one dialog detail surface, confirms destructive commands, and embeds Data Service settings', () => { @@ -505,26 +490,25 @@ describe('PluginsView', () => { const dialog = screen.getByRole('dialog', { name: '开发数据服务' }); expect(dialog).toHaveTextContent('详细的数据服务介绍。'); - expect(dialog).toHaveTextContent('读取项目名称'); - expect(dialog).toHaveTextContent('data-service.documents'); - expect(dialog).toHaveTextContent('按平台包含'); - expect(dialog).toHaveTextContent('生效范围'); - expect(dialog).toHaveTextContent('无需单独分配'); - expect(dialog).not.toHaveTextContent('小明'); - expect(screen.queryByRole('button', { name: '分配开发数据服务给伙伴' })).not.toBeInTheDocument(); - expect(dialog).toHaveTextContent('插件专属设置'); + expect(dialog).toHaveTextContent('写入文档'); + expect(dialog).toHaveTextContent('写入项目数据'); + expect(dialog).not.toHaveTextContent('读取项目名称'); + expect(dialog).not.toHaveTextContent('data-service.documents'); + expect(dialog).not.toHaveTextContent('按平台包含'); + expect(dialog).not.toHaveTextContent('版本 1.0.0'); + expect(dialog).toHaveTextContent('使用设置'); expect(dialog).toHaveTextContent('创建开发数据空间'); - fireEvent.click(screen.getByRole('button', { name: '从当前项目禁用开发数据服务' })); + fireEvent.click(screen.getByRole('button', { name: '从项目移除开发数据服务' })); expect(view.onCommand).not.toHaveBeenCalled(); expect(screen.getByRole('alertdialog')).toHaveTextContent('Project A'); - fireEvent.click(screen.getByRole('button', { name: '确认从当前项目禁用' })); + fireEvent.click(screen.getByRole('button', { name: '确认从项目移除' })); expect(view.onCommand).toHaveBeenCalledWith({ kind: 'disable_project', projectId: 'project-a', pluginId: 'makelore.data-service', }); }); - it('describes executable Skill scripts without calling them Pi extensions', () => { + it('describes local plugins by capability without exposing implementation details', () => { const scriptSkill: PluginWorkspaceItem = { ...localItem, key: 'local:makelore-project-scaffold', @@ -547,11 +531,13 @@ describe('PluginsView', () => { render(); const dialog = screen.getByRole('dialog', { name: 'MakeLore 项目初始化' }); - expect(dialog).toHaveTextContent('包含可执行 Skill 脚本'); - expect(dialog).not.toHaveTextContent('包含可执行 Pi extension'); + expect(dialog).toHaveTextContent('为智能体提供可在本机使用的扩展能力。'); + expect(dialog).not.toHaveTextContent('Skill 脚本'); + expect(dialog).not.toHaveTextContent('Pi extension'); + expect(dialog).not.toHaveTextContent('完整桌面权限'); }); - it('shows cached, suspended, retired, and retained-device reason copy with an explicit sign-in action', () => { + it('shows a friendly unavailable message and keeps the applicable sign-in action', () => { const unavailableItem: PluginWorkspaceItem = { ...officialItem, title: 'Notes', @@ -580,19 +566,21 @@ describe('PluginsView', () => { render(); const dialog = screen.getByRole('dialog', { name: 'Notes' }); - expect(dialog).toHaveTextContent('缓存'); - expect(dialog).toHaveTextContent('已暂停'); - expect(dialog).toHaveTextContent('已退役'); - expect(dialog).toHaveTextContent('官方包 1.0.0'); - expect(dialog).toHaveTextContent('旧设备版本已保留:当前客户端与新版本不兼容'); - fireEvent.click(screen.getByRole('button', { name: '登录后免费获取Notes' })); + expect(dialog).toHaveTextContent('这项能力目前暂不可用'); + expect(dialog).not.toHaveTextContent('缓存'); + expect(dialog).not.toHaveTextContent('已暂停'); + expect(dialog).not.toHaveTextContent('已退役'); + expect(dialog).not.toHaveTextContent('1.0.0'); + expect(dialog).not.toHaveTextContent('当前客户端与新版本不兼容'); + fireEvent.click(screen.getByRole('button', { name: '登录后添加Notes' })); expect(view.onCommand).toHaveBeenCalledWith({ kind: 'sign_in' }); }); - it('shows Marketplace detail operations and server-owned pricing without an active project', () => { + it('keeps Marketplace details capability-focused without exposing operation or pricing fields', () => { const detailItem: PluginWorkspaceItem = { ...officialItem, title: 'Notes', + summary: '记录、整理并回顾项目笔记。', projectState: 'not_applicable', assignedAgentIds: [], assignedAgentNames: [], @@ -632,54 +620,60 @@ describe('PluginsView', () => { render(); const dialog = screen.getByRole('dialog', { name: 'Notes' }); - expect(dialog).toHaveTextContent('notes'); - expect(dialog).toHaveTextContent('write'); - expect(dialog).toHaveTextContent('每次由服务端结算'); - expect(dialog).toHaveTextContent('计费单位:request'); - expect(dialog).toHaveTextContent('单位大小:2'); - expect(dialog).toHaveTextContent('每单位 Token Point:3'); - expect(dialog).toHaveTextContent('最低扣点:1'); - expect(dialog).toHaveTextContent('价格版本:pricing-7'); + expect(dialog).toHaveTextContent('记录、整理并回顾项目笔记。'); + expect(dialog).not.toHaveTextContent('notes'); + expect(dialog).not.toHaveTextContent('write'); + expect(dialog).not.toHaveTextContent('每次由服务端结算'); + expect(dialog).not.toHaveTextContent('request'); + expect(dialog).not.toHaveTextContent('Token Point'); + expect(dialog).not.toHaveTextContent('pricing-7'); }); it.each([ - ['stale', '当前项目策略使用缓存,所示能力与价格可能不是最新状态。'], - ['unavailable', '当前项目策略不可用,无法确认最新能力与价格。'], - ] as const)('shows %s project policy status beside policy-owned capability and pricing', (projectPolicyStatus, copy) => { + ['stale'], + ['unavailable'], + ] as const)('does not expose the %s policy implementation state in details', (projectPolicyStatus) => { const policyItem: PluginWorkspaceItem = { ...officialItem, projectPolicyStatus }; render(); - expect(screen.getByRole('dialog', { name: '开发数据服务' })).toHaveTextContent(copy); - }); - - it('does not mark current project policy as stale or unavailable', () => { - render(); - const dialog = screen.getByRole('dialog', { name: '开发数据服务' }); expect(dialog).not.toHaveTextContent('当前项目策略使用缓存'); expect(dialog).not.toHaveTextContent('当前项目策略不可用'); }); - it('deduplicates operation identity while preferring the current project policy projection', () => { + it('deduplicates human-facing capability descriptions and hides their technical identity', () => { + const capabilityTool = { + name: 'notes_write', + label: '整理笔记', + description: '整理并保存项目笔记。', + mutation: 'write' as const, + permissions: ['project.notes.write'], + }; const projectPolicy = { ...projectPlugin, capabilities: [{ - id: 'notes', - operations: [{ - id: 'write', - billing: { - mode: 'platform_metered' as const, - availability: 'available' as const, - notice: '当前项目策略价格', - unitName: 'project-request', - unitSize: 1, - ratePoints: '7', - minimumChargePoints: '2', - pricingVersion: 9, - roundingMode: 'ceil' as const, + id: 'notes.internal', + operations: [ + { + id: 'write_internal', + billing: { + mode: 'platform_metered' as const, + availability: 'available' as const, + notice: '当前项目策略价格', + ratePoints: '7', + }, + tool: capabilityTool, }, - tool: null, - }], + { + id: 'write_duplicate', + billing: { + mode: 'included' as const, + availability: 'available' as const, + notice: '目录包含读取', + }, + tool: capabilityTool, + }, + ], }], }; const detailItem: PluginWorkspaceItem = { @@ -724,20 +718,21 @@ describe('PluginsView', () => { render(); const dialog = screen.getByRole('dialog', { name: 'Notes' }); - expect(screen.getAllByText('write')).toHaveLength(1); - expect(dialog).toHaveTextContent('read'); - expect(dialog).toHaveTextContent('当前项目策略价格'); + expect(screen.getAllByText('整理笔记')).toHaveLength(1); + expect(screen.getAllByText('整理并保存项目笔记。')).toHaveLength(1); + expect(dialog).not.toHaveTextContent('notes.internal'); + expect(dialog).not.toHaveTextContent('write_internal'); + expect(dialog).not.toHaveTextContent('project.notes.write'); + expect(dialog).not.toHaveTextContent('当前项目策略价格'); expect(dialog).not.toHaveTextContent('目录价格'); - expect(dialog).toHaveTextContent('每单位 Token Point:7'); - expect(dialog).toHaveTextContent('价格版本:9'); - expect(dialog).toHaveTextContent('目录包含读取'); + expect(dialog).not.toHaveTextContent('Token Point'); }); it('dispatches non-destructive detail commands directly and closes from the dialog control', () => { const view = props(officialItem); render(); - fireEvent.click(screen.getByRole('button', { name: '查看开发数据服务插件设置' })); + fireEvent.click(screen.getByRole('button', { name: '打开开发数据服务设置' })); expect(view.onCommand).toHaveBeenCalledWith({ kind: 'open_settings', projectId: 'project-a', pluginId: 'makelore.data-service', }); @@ -749,7 +744,7 @@ describe('PluginsView', () => { const viewA = props(officialItem); const { rerender } = render(); - fireEvent.click(screen.getByRole('button', { name: '从当前项目禁用开发数据服务' })); + fireEvent.click(screen.getByRole('button', { name: '从项目移除开发数据服务' })); expect(screen.getByRole('alertdialog')).toHaveTextContent('Project A'); const projectBItem: PluginWorkspaceItem = {