feat(agents): 展示云端通俗技能与工具文案
This commit is contained in:
1 parent
23aa927441
commit
821fd6ffbe
16 files changed
+118
-23
No files matched your search
@@ -0,0 +1,61 @@
|
||||
# Task: Discuss readable capability labels for Makelore
|
||||
|
||||
## Identity
|
||||
|
||||
- Task ID: 20261013-capability-copy-febf9c28
|
||||
- Mode: Feature
|
||||
- Branch: codex/20261013-capability-copy-febf9c28-capability-copy
|
||||
- Worktree: C:\Users\7brot\.codex\worktrees\makelore-20261013-capability-copy-febf9c28
|
||||
- Base commit: 23aa927441d90d7b71a482c91168a2ad26f53d6d
|
||||
- Owner: codex
|
||||
- Status: Ready for Integration
|
||||
|
||||
## Scope
|
||||
|
||||
- Implement the user-approved code-maintained plain-language names/descriptions for platform tools and builtin skills in Makelore catalogs and live/history tool cards.
|
||||
- Both repositories use isolated worktrees for this task. The previously completed agent deletion task remains separate and untouched.
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- 2026-10-13: User requested merging this completed capability-copy change into both local main branches. Resume for pre-commit checks and fresh read-only review; integration ownership handoff preserves all unrelated records. Push/deployment remain outside scope.
|
||||
|
||||
- Make names and descriptions understandable without changing Yuxi runtime identity, model instructions, tool schema, permissions, selections or custom author content.
|
||||
- User approved the recommended approach with “可以”, then explicitly requested commit/merge to both local main branches. No new admin editor, database fields, automatic translation, push or deployment.
|
||||
- Concurrent/Planning Gates passed on same-task resume; existing peer scopes were rechecked and no semantic conflict requires a handoff. User explicitly approved one fresh read-only Reviewer for the paired change.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Yuxi owns one small copy module for platform tools and five builtin skills. Catalog keeps key/name/description; skill source_type prevents rewriting uploaded/imported and personal override text.
|
||||
- Tool display_name/display_description come from the same module. Frozen per-run configuration identifies local tools and knowledge-derived tools, preserving same-named MCP text and the original execution name. Unknown or source-less history uses the original name. With configured MCP servers, implicit filesystem/Todo/subagent names also keep their raw names because existing snapshots cannot prove whether MCP replaced them; execution rules remain unchanged.
|
||||
- History batches frozen inputs for its own message runs. Makelore SSE uses the existing authorized stream with an optional output projector after native compaction; raw events and Last-Event-ID remain intact.
|
||||
- Client Main whitelist, typed tool calls, live reducer and shared personal/channel message component carry and display the new fields. Expanded tool cards retain raw identifiers, arguments, results and errors.
|
||||
- Skill labels, search and preload wording are Chinese; configuration still stores original keys. README and Yuxi owning documentation describe the behavior. Proposed decision moved to implemented after direct verification.
|
||||
- Reviewed changes since the recorded bases and inspected the actual Electron reopened tool card. No unrelated source or canonical project memory changed.
|
||||
|
||||
## Verification
|
||||
|
||||
- Backend: WSL Python 3.12, pytest test/unit/services/test_makelore_capability_copy.py test/unit/services/test_agent_run_service.py -q: 65 passed. Covers same-named MCP, missing source, derived knowledge tools, event batches/single chunks/lifecycle and immutable inputs. New read_file/write_todos/task cases prove raw-name fallback for MCP runs in history and live projection, positive labels without MCP, and retained explicit/knowledge/schedule labels.
|
||||
- Backend: isolated PostgreSQL on port 58419 and real HTTP, pytest test/integration/services/test_makelore_agents.py -k 'capability_copy or personal_history' -q: 3 passed. Catalog checks builtin/upload/personal overrides, original saved keys and persisted descriptions; SSE checks authorization, replay cursors and unchanged native output; history checks durable labels and visibility.
|
||||
- Client: pnpm 10.33.4 run typecheck passed. Related cloud-chat-rendering, cloud-agents-main, cloud-agents-page, cloud-agents-management, cloud-agents-workflows and cloud-channel-conversations unit tests: 108 passed in total. The initial old “上传 Skill” selector failed as expected after the rename; updated selector and that file's 3 tests passed.
|
||||
- Client: build:vite passed; changed-source ESLint passed; real Electron cloud-agents.spec.ts and cloud-chat-rendering.spec.ts: 2 passed, covering readable catalog, live tool card and reopening.
|
||||
- Documentation: engineering contracts passed; verifier unittest 62 passed in WSL. Windows verifier tests hit existing Unix symlink/path assumptions, so the test suite was run in Linux. VitePress docs build passed with link checking.
|
||||
- Backend changed-source Ruff passed. Working/cached diff whitespace checks and task-aware documentation drift checks are final gate commands.
|
||||
- Evidence: C:/Users/7brot/.codex/visualizations/2026/10/12/01a12e4f-9365-74c2-9e57-3f900a028618/capability-copy-local; unit/integration runtime logs also under /tmp/makelore-capability-copy-febf9c28 in WSL.
|
||||
- Final pre-commit backend non-slow unit suite after the review fix: 2276 passed, 1 skipped (WSL Python 3.12; 59.14 seconds). Engineering contracts passed with 120 decisions; engineering verifier unittest: 62 passed.
|
||||
- Fresh independent read-only Reviewer found a real implicit-tool/MCP collision that could mislabel the actual MCP implementation. The final projection-only fix preserves raw names when source is uncertain, covering old history without changing runtime behavior. Reviewer independently reran 65 backend tests and 62 client tests, and confirmed the final patch has no open blocking findings (PASS).
|
||||
- Limits: Redis event retrieval and cloud providers use deterministic fixtures; no paid model calls, production deployment, package installation or child comprehension study.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Independent review and verification are complete; this source result is ready for the authorized commit and integration. Exclusive Integration tasks own both main directories and preserve all five unrelated untracked project records. Integration outcome belongs to their records.
|
||||
- Task-owned PostgreSQL process stopped after verification; its data and external artifacts are retained.
|
||||
- Future platform capabilities need presentation entries; custom capability copy remains author-owned.
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- Target: current-state.md / integrated architecture or domain note for Makelore resources.
|
||||
- Proposal: Yuxi owns Makelore presentation copy; catalog keys/model instructions remain unchanged, and live/history tool display is derived from each run's frozen source.
|
||||
- Evidence: catalog/save HTTP, SSE/native comparison, history PostgreSQL reread, Main projection and Electron DOM tests.
|
||||
- Future impact: maintain one server-side copy source and preserve custom provenance when adding capabilities.
|
||||
- Semantic conflicts: none identified; deletion/purpose tasks stay independent.
|
||||
- Human confirmation: implementation already approved; canonical promotion requires the normal integration task.
|
||||
@@ -12,7 +12,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智
|
||||
|
||||
- `Makelore Code|AI 编程`:管理本地项目、项目智能体、对话、文件上下文、代码变更和运行时。
|
||||
会话工作台采用浅蓝项目栏、彩色通用手柄图标与卡片选中态;中间对话/作品使用圆角标签,作品预览的后退、前进、刷新与网址输入统一放在标签右侧;只在作品页显示,窄窗口自动换行。右侧智能体咨询使用奶油色标题区。沿用真实项目和会话入口、侧栏伸缩与账户菜单,样式只作用于 Code 会话工作台。可编辑的空对话在输入区上方的可用区域中央显示童趣欢迎语;「去作品广场逛逛」入口紧贴输入框上方,两者一起位于底部。老师介绍以固定宽度浮在右上角,不占用中间布局;欢迎语和底部输入区始终相对项目栏右侧的整个内容区域居中;小窗口的欢迎内容独立滚动;发送首条消息后收起欢迎区。侧栏保留「作品广场」入口,只有点击后才在系统浏览器打开公共广场,不请求、摘取或内嵌展示广场作品。打开广场保留当前项目、对话、草稿和附件,不自动发送消息;老师求助按钮保留临时应用的最新文案「帮我整体看🧠」。
|
||||
- `Makelore Agents|AI 智能体`:配置个人云智能体并试聊,发布后正式对话、接入个人微信或创建应用 API Key。支持流式回复、审批、排队请求、附件与产物、自动任务和活动历史;重新进入时恢复最近智能体与对话,未确认操作可按原输入重试。聊天可提出日程建议,由创建者核对能力、时间和费用后启用。知识库上传、替换及导入本人会话附件后自动解析并建立索引;文档卡片区分排队、解析、索引、可用和失败,持续刷新已加载文档,失败可原地重试;零文本内容不会显示为可用,新文档可用后才移除被替换的旧文档。支持删除知识内容;个人 MCP 凭据、Skills、子智能体可自助管理。支持智能体和会话归档、发布版本比较及恢复为草稿。除知识库 embedding 向量化外,模型调用(包括知识处理触发的对话模型调用)仍按原规则由创建者个人词元点数支付,可按智能体设置每次任务和每日上限,费用按时间、来源、应用分页统计;embedding 向量化由平台承担,不计入用户用量、词元点数或智能体费用上限;应用调用者保有自己的内容空间。Main 管理云会话和本机文件,Renderer 通过 Host API 操作。配套服务接入见 [Yuxi MakeLore 说明](https://xerrors.github.io/Yuxi/advanced/makelore-agents.html)。 编辑采用全高桌面分栏,支持拖动调整与展开、独立滚动、小窗口配置/试用切换。首次创建按选用途、说要求、试一试引导,提供科学问答、故事、英语和笔记整理起点;回答方式可多选,特别要求直接编辑,仍以草稿的 system_prompt 保存。新助手预选云端目录的第一个可用模型并明确显示,可自行更换;已有助手不自动换模型。编辑区以“我的要求”和“高级设置”两个 Tab 切换,默认显示基础要求;高级设置包含能力、知识与限制,返回时保留上次分类,切换保留未保存的输入及当前试用。已开始使用的助手在顶部显示“修改已保存,尚未应用”或“已应用最新修改”,并提供显眼的“应用最新修改”按钮;保存只更新草稿,应用沿用同一发布操作,失败重试保留原操作身份。草稿明确保存(Ctrl/⌘+S),保存与切换页面保留当前试用及未发送文字;“保存并重新试用”使用最新保存的要求,保留上次试用供对比,并可复用上次问题,不自动发送。示例问题仅填入输入框;有运行、排队、审批或未确认操作时先处理当前试用。工具确认先展示实际操作说明与参数,完整技术详情可展开。确认“开始使用”会创建仅自己可用的发布版本并进入独立对话,微信渠道与应用接入仍单独配置。费用上限在限制中独立保存。Enter 发送、Shift+Enter 换行,F6 切换面板焦点、Esc 恢复分栏。
|
||||
- `Makelore Agents|AI 智能体`:配置个人云智能体并试聊,发布后正式对话、接入个人微信或创建应用 API Key。支持流式回复、审批、排队请求、附件与产物、自动任务和活动历史;重新进入时恢复最近智能体与对话,未确认操作可按原输入重试。聊天可提出日程建议,由创建者核对能力、时间和费用后启用。知识库上传、替换及导入本人会话附件后自动解析并建立索引;文档卡片区分排队、解析、索引、可用和失败,持续刷新已加载文档,失败可原地重试;零文本内容不会显示为可用,新文档可用后才移除被替换的旧文档。支持删除知识内容;个人 MCP 凭据、技能、子智能体可自助管理。平台工具和内置技能使用 Yuxi 下发的通俗名称与说明;对话过程和历史工具卡片沿用相同文案,展开可查看原始工具标识。个人上传技能保留作者文案,选择仍保存原始资源标识。支持智能体和会话归档、发布版本比较及恢复为草稿。除知识库 embedding 向量化外,模型调用(包括知识处理触发的对话模型调用)仍按原规则由创建者个人词元点数支付,可按智能体设置每次任务和每日上限,费用按时间、来源、应用分页统计;embedding 向量化由平台承担,不计入用户用量、词元点数或智能体费用上限;应用调用者保有自己的内容空间。Main 管理云会话和本机文件,Renderer 通过 Host API 操作。配套服务接入见 [Yuxi MakeLore 说明](https://xerrors.github.io/Yuxi/advanced/makelore-agents.html)。 编辑采用全高桌面分栏,支持拖动调整与展开、独立滚动、小窗口配置/试用切换。首次创建按选用途、说要求、试一试引导,提供科学问答、故事、英语和笔记整理起点;回答方式可多选,特别要求直接编辑,仍以草稿的 system_prompt 保存。新助手预选云端目录的第一个可用模型并明确显示,可自行更换;已有助手不自动换模型。编辑区以“我的要求”和“高级设置”两个 Tab 切换,默认显示基础要求;高级设置包含能力、知识与限制,返回时保留上次分类,切换保留未保存的输入及当前试用。已开始使用的助手在顶部显示“修改已保存,尚未应用”或“已应用最新修改”,并提供显眼的“应用最新修改”按钮;保存只更新草稿,应用沿用同一发布操作,失败重试保留原操作身份。草稿明确保存(Ctrl/⌘+S),保存与切换页面保留当前试用及未发送文字;“保存并重新试用”使用最新保存的要求,保留上次试用供对比,并可复用上次问题,不自动发送。示例问题仅填入输入框;有运行、排队、审批或未确认操作时先处理当前试用。工具确认先展示实际操作说明与参数,完整技术详情可展开。确认“开始使用”会创建仅自己可用的发布版本并进入独立对话,微信渠道与应用接入仍单独配置。费用上限在限制中独立保存。Enter 发送、Shift+Enter 换行,F6 切换面板焦点、Esc 恢复分栏。
|
||||
|
||||
云知识库的 embedding 模型只由平台管理员在 Yuxi 后台维护:在「智能体管理 → 模型供应商」启用供应商,按供应商要求填写有效的服务地址和 API Key,添加并启用 embedding 模型并填写实际向量维度;embedding 向量化费用由平台承担,不计入用户用量、词元点数或智能体费用上限;one-api 不参与 embedding 配置,Works Square 只负责其他应计费模型调用的创建者词元点数账本。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ function projectMessage(value: unknown): unknown {
|
||||
return { ...copy(source, ['id', 'role', 'content', 'request_id', 'run_id', 'created_at', 'message_id', 'reasoning_content']),
|
||||
...(Array.isArray(source.tool_calls) ? { tool_calls: source.tool_calls.map(item => {
|
||||
const tool = object(item);
|
||||
return { ...copy(tool, ['id', 'name', 'args', 'status', 'error_message']),
|
||||
return { ...copy(tool, ['id', 'name', 'display_name', 'display_description', 'args', 'status', 'error_message']),
|
||||
...(tool.tool_call_result == null ? {} : { tool_call_result: copy(tool.tool_call_result, ['content']) }) };
|
||||
}) } : {}),
|
||||
};
|
||||
|
||||
@@ -104,6 +104,7 @@ export interface CloudMessage {
|
||||
}
|
||||
export interface CloudToolCall {
|
||||
id: string; name: string; args: unknown; status: string;
|
||||
display_name?: string; display_description?: string;
|
||||
tool_call_result?: { content: unknown } | null;
|
||||
error_message?: string | null;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { CloudAgentDraft, CloudVersion } from '../../../shared/cloud-agents
|
||||
import { usePendingCloudInput } from './CloudPending';
|
||||
|
||||
const configurationLabels: Record<string, string> = {
|
||||
model: '模型', tools: '工具', knowledges: '知识库', mcps: 'MCP 连接', skills: 'Skills', preload_skills: '预加载 Skills',
|
||||
model: '模型', tools: '工具', knowledges: '知识库', mcps: 'MCP 连接', skills: '技能', preload_skills: '提前读好的技能',
|
||||
subagents: '子智能体', tool_approval_mode: '工具确认方式', max_execution_steps: '最多执行步数',
|
||||
max_output_tokens: '每次输出词元上限', max_run_seconds: '最长执行秒数',
|
||||
};
|
||||
|
||||
@@ -14,8 +14,10 @@ export function CloudMessageBody({ content, reasoning_content, tool_calls = [],
|
||||
<div className="mt-3 space-y-3">
|
||||
{reasoning_content && <div aria-label="智能体思考过程" className="max-h-72 overflow-auto whitespace-pre-wrap break-words border-l-2 pl-3 leading-6">{reasoning_content}</div>}
|
||||
{tool_calls.map(tool => <details key={tool.id} className="rounded-md border border-border p-2">
|
||||
<summary className="cursor-pointer break-words">{tool.name} · {tool.status === 'success' ? '已完成' : tool.status === 'error' ? '失败' : active ? tool.status === 'running' ? '执行中' : '准备调用' : '未完成'}</summary>
|
||||
<summary className="cursor-pointer break-words">{tool.display_name || tool.name} · {tool.status === 'success' ? '已完成' : tool.status === 'error' ? '失败' : active ? tool.status === 'running' ? '执行中' : '准备调用' : '未完成'}</summary>
|
||||
<div className="mt-2 space-y-2">
|
||||
{tool.display_description && <p className="leading-5">{tool.display_description}</p>}
|
||||
{tool.display_name && tool.display_name !== tool.name && <p className="break-all">工具标识:{tool.name}</p>}
|
||||
<p>输入</p><pre className="max-h-48 overflow-auto whitespace-pre-wrap break-words">{display(tool.args)}</pre>
|
||||
{tool.tool_call_result != null && <><p>结果</p><pre className="max-h-60 overflow-auto whitespace-pre-wrap break-words">{display(tool.tool_call_result.content)}</pre></>}
|
||||
{tool.error_message && <p className="whitespace-pre-wrap break-words text-destructive">{tool.error_message}</p>}
|
||||
|
||||
@@ -25,7 +25,7 @@ export function CloudResources({ onChanged }: { onChanged: () => void }) {
|
||||
return <details className="rounded-lg border p-4" onToggle={e => { if (e.currentTarget.open && !data) void act(refresh); }}>
|
||||
<summary className="min-h-8 cursor-pointer text-sm font-medium">管理我的云端能力</summary>
|
||||
<div className="mt-4 space-y-6 text-sm">
|
||||
<p className="text-xs leading-5 text-muted-foreground">连接、Skills 和子智能体属于你的个人账号。创建后在上面的能力目录选用,保存并发布后生效。</p>
|
||||
<p className="text-xs leading-5 text-muted-foreground">连接、技能和子智能体属于你的个人账号。创建后在上面的能力目录选用,保存并发布后生效。</p>
|
||||
{error && <p role="alert" className="text-destructive">{error} <Button variant="ghost" onClick={() => void act(refresh)}>刷新能力</Button></p>}
|
||||
<section className="space-y-3"><div className="flex items-center justify-between"><h3 className="font-medium">MCP 连接</h3><Button type="button" size="sm" variant="outline" disabled={busy || Boolean(mcp)} onClick={() => setMcp('new')}>添加 MCP</Button></div>
|
||||
<p className="text-xs text-muted-foreground">连接由云端访问。凭据只用于你的连接,分享调用可在你已发布的授权范围内使用这些工具。</p>
|
||||
@@ -37,8 +37,8 @@ export function CloudResources({ onChanged }: { onChanged: () => void }) {
|
||||
<Button type="button" size="sm" variant="ghost" disabled={busy} onClick={() => setDeleting({ kind: 'mcp', slug: item.slug, name: item.name })}>删除连接</Button>
|
||||
</div></div>)}
|
||||
</section>
|
||||
<section className="space-y-3"><div className="flex items-center justify-between"><h3 className="font-medium">个人 Skills</h3><Button type="button" size="sm" variant="outline" disabled={busy || Boolean(skill)} onClick={() => void act(async () => setSkill(await cloudAgentsApi.uploadSkill()))}>上传 Skill</Button></div>
|
||||
<p className="text-xs text-muted-foreground">支持 SKILL.md 或单个 Skill 的 ZIP,最大 5 MB。同名 Skill 需先删除个人旧版再安装,并重新核对相关智能体配置。</p>
|
||||
<section className="space-y-3"><div className="flex items-center justify-between"><h3 className="font-medium">个人技能</h3><Button type="button" size="sm" variant="outline" disabled={busy || Boolean(skill)} onClick={() => void act(async () => setSkill(await cloudAgentsApi.uploadSkill()))}>上传技能</Button></div>
|
||||
<p className="text-xs text-muted-foreground">支持 SKILL.md 或一个技能的 ZIP 压缩包,最大 5 MB。同名技能需先删除个人旧版再安装,并重新核对相关智能体配置。</p>
|
||||
{skill && <div className="space-y-3 rounded-lg border p-3">{skill.items.map(item => <p key={item.slug}>{item.name || item.slug} · {item.description}{item.error && <span className="text-destructive">{item.error}</span>}</p>)}
|
||||
<div className="flex gap-2"><Button type="button" size="sm" disabled={busy} onClick={() => void act(async () => {
|
||||
const result = await cloudAgentsApi.call('confirmSkill', { draft_id: skill.draft_id });
|
||||
@@ -46,7 +46,7 @@ export function CloudResources({ onChanged }: { onChanged: () => void }) {
|
||||
setSkill(null);
|
||||
})}>确认安装到个人能力</Button><Button type="button" variant="ghost" disabled={busy} onClick={() => setSkill(null)}>取消安装</Button></div></div>}
|
||||
{data?.skills.map(item => <div key={item.slug} className="flex items-center justify-between gap-2 rounded-lg bg-muted/30 p-3"><div><p>{item.name}</p><p className="mt-1 text-xs text-muted-foreground">{item.description}</p></div>
|
||||
<Button type="button" size="sm" variant="ghost" disabled={busy} onClick={() => setDeleting({ kind: 'skill', slug: item.slug, name: item.name })}>删除 Skill</Button></div>)}
|
||||
<Button type="button" size="sm" variant="ghost" disabled={busy} onClick={() => setDeleting({ kind: 'skill', slug: item.slug, name: item.name })}>删除技能</Button></div>)}
|
||||
</section>
|
||||
<section className="space-y-3"><div className="flex items-center justify-between"><h3 className="font-medium">个人子智能体</h3><Button type="button" size="sm" variant="outline" disabled={busy || Boolean(child)} onClick={() => setChild('new')}>创建子智能体</Button></div>
|
||||
<p className="text-xs text-muted-foreground">子智能体处理专门任务,继承父智能体发布的模型、能力和费用限制。</p>
|
||||
|
||||
@@ -113,7 +113,7 @@ function ScheduleEditor({ slug, initial, proposal, onClose, onSaved }: { slug: s
|
||||
<h3 className="font-medium">启用前确认</h3>
|
||||
<p>当前发布版本 {context.version} · {context.configuration.model}。触发时使用当时的发布版本。</p>
|
||||
<p>能力:{['tools', 'knowledges', 'mcps', 'skills', 'subagents'].map(key => {
|
||||
const labels: Record<string, string> = { tools: '工具', knowledges: '知识库', mcps: 'MCP', skills: 'Skills', subagents: '子智能体' };
|
||||
const labels: Record<string, string> = { tools: '工具', knowledges: '知识库', mcps: 'MCP', skills: '技能', subagents: '子智能体' };
|
||||
const selected = context.configuration![key as 'tools']; return selected.length ? labels[key] + ':' + selected.join('、') : '';
|
||||
}).filter(Boolean).join(';') || '基础对话'}</p>
|
||||
<p>每次最多 {context.configuration.max_execution_steps} 步、{context.configuration.max_run_seconds} 秒,每次模型输出最多 {context.configuration.max_output_tokens} 词元。</p>
|
||||
|
||||
@@ -48,7 +48,7 @@ export function ConfigurationFields({ slug, value, disabled, onChange, section =
|
||||
</p>}
|
||||
</div>
|
||||
{(['tools', 'knowledges', 'mcps', 'skills', 'subagents'] as const).map(key => {
|
||||
const labels = { tools: '工具', knowledges: '知识库', mcps: 'MCP 服务', skills: 'Skills', subagents: '子智能体' };
|
||||
const labels = { tools: '工具', knowledges: '知识库', mcps: 'MCP 服务', skills: '技能', subagents: '子智能体' };
|
||||
const options = catalog?.resources[key] ?? [];
|
||||
const missing = value[key].filter(id => !options.some(option => option.key === id));
|
||||
return <fieldset key={key} disabled={disabled} hidden={section !== 'all' && section !== (key === 'knowledges' ? 'knowledge' : 'capabilities')} className="space-y-2">
|
||||
@@ -69,7 +69,7 @@ export function ConfigurationFields({ slug, value, disabled, onChange, section =
|
||||
</div>
|
||||
{key === 'skills' && value.skills.length > 0 && <label className="flex min-h-10 items-center gap-2 text-xs">
|
||||
<input type="checkbox" checked={value.preload_skills.length === value.skills.length}
|
||||
onChange={e => change('preload_skills', e.target.checked ? [...value.skills] : [])} />开始运行时预加载已选 Skills</label>}
|
||||
onChange={e => change('preload_skills', e.target.checked ? [...value.skills] : [])} />开始对话前,先读好已选技能的使用方法</label>}
|
||||
</fieldset>;
|
||||
})}
|
||||
<div hidden={section !== 'all' && section !== 'capabilities'} className="space-y-4">
|
||||
|
||||
@@ -47,6 +47,8 @@ export function reduceCloudStream(state: CloudStream, data: unknown, threadId: s
|
||||
const existing = current.tool_calls.find(tool => (event.tool_call_id && tool.id === event.tool_call_id) || tool.index === index);
|
||||
const tool = { ...existing, id: typeof event.tool_call_id === 'string' ? event.tool_call_id : existing?.id ?? id + ':' + index,
|
||||
name: typeof event.name === 'string' ? event.name : existing?.name ?? '工具',
|
||||
display_name: typeof event.display_name === 'string' ? event.display_name : existing?.display_name,
|
||||
display_description: typeof event.display_description === 'string' ? event.display_description : existing?.display_description,
|
||||
args: existing && existing.status !== 'pending' ? existing.args
|
||||
: event.type === 'tool_call' ? event.args ?? {} : String(existing?.args ?? '') + String(event.args_delta ?? ''),
|
||||
status: existing?.status ?? 'pending', index };
|
||||
@@ -60,6 +62,9 @@ export function reduceCloudStream(state: CloudStream, data: unknown, threadId: s
|
||||
if (toolEvent.method === 'tools' && typeof result.tool_call_id === 'string') {
|
||||
messages = messages.map(message => ({ ...message, tool_calls: message.tool_calls.map(tool => {
|
||||
if (tool.id !== result.tool_call_id) return tool;
|
||||
tool = { ...tool,
|
||||
display_name: typeof result.display_name === 'string' ? result.display_name : tool.display_name,
|
||||
display_description: typeof result.display_description === 'string' ? result.display_description : tool.display_description };
|
||||
if (result.event === 'tool-started') return { ...tool, args: result.input ?? tool.args, status: 'running' };
|
||||
if (result.event !== 'tool-finished') return tool;
|
||||
const output = object(result.output);
|
||||
|
||||
@@ -52,7 +52,10 @@ test('personal cloud Agent creation, draft save and leave protection in Electron
|
||||
if (path.endsWith('/recovery/recent')) { recent = body; return result({ saved: true }); }
|
||||
if (path.endsWith('/actions')) {
|
||||
const input = body.input ?? {};
|
||||
if (body.operation === 'catalog') return result({ models: [{ id: 'test-model', name: '创作模型' }], resources: {}, pricing: null });
|
||||
if (body.operation === 'catalog') return result({ models: [{ id: 'test-model', name: '创作模型' }], resources: {
|
||||
tools: [{ key: 'web_search', name: '上网查资料', description: '在网上查找相关信息。' }],
|
||||
skills: [{ key: 'deep-research', name: '查资料,做研究', description: '比较不同说法,再整理出结论和出处。' }],
|
||||
}, pricing: null });
|
||||
if (body.operation === 'knowledge') return result({ databases: knowledgeCreated ? [{ kb_id: 'kb', name: '我的创作资料' }] : [], models: ++knowledgeCatalogLoads === 1 ? [] : [{ id: 'fixture:test-embedding', name: '资料向量模型', dimension: 1024 }] });
|
||||
if (body.operation === 'createKnowledge') { knowledgeCreated = true; return result({ kb_id: 'kb', name: '我的创作资料' }); }
|
||||
if (body.operation === 'processKnowledge') { knowledgeStage = 0; return result({ task_id: 'process-document' }); }
|
||||
@@ -145,6 +148,11 @@ test('personal cloud Agent creation, draft save and leave protection in Electron
|
||||
await expect(page.getByRole('tabpanel', { name: '高级设置' })).toBeVisible();
|
||||
await expect(page.getByLabel('特别要求')).toBeHidden();
|
||||
await expect(page.getByRole('button', { name: '能力', exact: true })).toHaveAttribute('aria-current', 'page');
|
||||
await page.getByRole('button', { name: '选择工具', exact: true }).click();
|
||||
await expect(page.getByRole('checkbox', { name: /上网查资料.*在网上查找相关信息/ })).toBeVisible();
|
||||
await page.getByRole('button', { name: '选择技能', exact: true }).click();
|
||||
await page.getByLabel('搜索技能', { exact: true }).fill('出处');
|
||||
await expect(page.getByRole('checkbox', { name: /查资料,做研究.*出处/ })).toBeVisible();
|
||||
await advancedTab.click();
|
||||
await expect(advancedTab).toHaveAttribute('aria-selected', 'true');
|
||||
await page.screenshot({ path: testInfo.outputPath('cloud-agent-advanced-tab.png') });
|
||||
|
||||
@@ -35,7 +35,7 @@ test('personal chat displays thinking, tools and Markdown, then restores them af
|
||||
id: 1, message_id: 'answer', role: 'assistant', run_id: run.agent_run_id, request_id: run.request_id, created_at: '',
|
||||
content: '## 整理结果\n\n| 项目 | 状态 |\n| --- | --- |\n| 资料 | 已核对 |\n\n**可以使用**',
|
||||
reasoning_content: '先核对来源,再整理结果。',
|
||||
tool_calls: [{ id: 'lookup', name: '资料检索', args: { query: '实际检索资料' }, status: 'success', tool_call_result: { content: '找到两条资料' } }],
|
||||
tool_calls: [{ id: 'lookup', name: 'web_search', display_name: '上网查资料', display_description: '在网上查找相关信息。', args: { query: '实际检索资料' }, status: 'success', tool_call_result: { content: '找到两条资料' } }],
|
||||
}] : [] });
|
||||
if (body.operation === 'run') return result({ ...run, status: complete ? 'completed' : 'running' });
|
||||
if (body.operation === 'viewed') return result({});
|
||||
@@ -71,7 +71,7 @@ test('personal chat displays thinking, tools and Markdown, then restores them af
|
||||
const process = page.getByTestId('cloud-message-process');
|
||||
await expect(page.getByLabel('智能体思考过程')).toBeVisible();
|
||||
await emit([
|
||||
{ stream_event: { type: 'tool_call', message_id: 'answer', tool_call_id: 'lookup', name: '资料检索', args: { query: '测试资料' } } },
|
||||
{ stream_event: { type: 'tool_call', message_id: 'answer', tool_call_id: 'lookup', name: 'web_search', display_name: '上网查资料', display_description: '在网上查找相关信息。', args: { query: '测试资料' } } },
|
||||
{ event: { method: 'tools', data: { event: 'tool-started', tool_call_id: 'lookup', input: { query: '实际检索资料' } } } },
|
||||
{ event: { method: 'tools', data: { event: 'tool-finished', tool_call_id: 'lookup', output: { content: '找到两条资料' } } } },
|
||||
{ stream_event: { type: 'message_delta', message_id: 'answer', content: '## 整理结果\n\n| 项目 | 状态 |\n| --- | --- |\n| 资料 | 已核对 |\n\n**可以使用**' } },
|
||||
@@ -79,8 +79,10 @@ test('personal chat displays thinking, tools and Markdown, then restores them af
|
||||
await expect(page.getByRole('heading', { name: '整理结果' })).toBeVisible();
|
||||
await expect(page.getByRole('table')).toBeVisible();
|
||||
await process.locator(':scope > summary').click();
|
||||
await process.getByText('资料检索 · 已完成', { exact: true }).click();
|
||||
await process.getByText('上网查资料 · 已完成', { exact: true }).click();
|
||||
await expect(process.getByText('找到两条资料', { exact: true })).toBeVisible();
|
||||
await expect(process.getByText('在网上查找相关信息。', { exact: true })).toBeVisible();
|
||||
await expect(process.getByText('工具标识:web_search', { exact: true })).toBeVisible();
|
||||
await expect(process.getByText('"实际检索资料"', { exact: false })).toBeVisible();
|
||||
await page.screenshot({ path: testInfo.outputPath('personal-chat-process.png') });
|
||||
await app.evaluate(() => (globalThis as unknown as { finishRenderRun: () => void }).finishRenderRun());
|
||||
@@ -91,7 +93,9 @@ test('personal chat displays thinking, tools and Markdown, then restores them af
|
||||
await expect(page.getByRole('heading', { name: '整理结果' })).toBeVisible();
|
||||
await page.getByTestId('cloud-message-process').locator(':scope > summary').click();
|
||||
await expect(page.getByLabel('智能体思考过程')).toHaveText('先核对来源,再整理结果。');
|
||||
await expect(page.getByText('资料检索 · 已完成', { exact: true })).toBeVisible();
|
||||
await page.getByText('上网查资料 · 已完成', { exact: true }).click();
|
||||
await expect(page.getByText('在网上查找相关信息。', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('工具标识:web_search', { exact: true })).toBeVisible();
|
||||
await page.screenshot({ path: testInfo.outputPath('personal-chat-reopened.png') });
|
||||
} finally { await closeElectronApp(app); }
|
||||
});
|
||||
@@ -48,9 +48,11 @@ describe('Main cloud Agents boundary', () => {
|
||||
it('projects saved reasoning and tool results for both personal and channel history without raw metadata', () => {
|
||||
const message = { id: 1, role: 'assistant', content: 'answer', reasoning_content: 'thought', message_id: 'model-1',
|
||||
extra_metadata: { internal: true }, tool_calls: [{ id: 'call-1', name: 'search', args: { query: 'topic' },
|
||||
display_name: '上网查资料', display_description: '在网上查找相关信息。',
|
||||
status: 'success', internal: 'hidden', tool_call_result: { content: 'result', secret: 'hidden' } }] };
|
||||
const expected = { id: 1, role: 'assistant', content: 'answer', reasoning_content: 'thought', message_id: 'model-1',
|
||||
tool_calls: [{ id: 'call-1', name: 'search', args: { query: 'topic' }, status: 'success', tool_call_result: { content: 'result' } }] };
|
||||
tool_calls: [{ id: 'call-1', name: 'search', display_name: '上网查资料', display_description: '在网上查找相关信息。',
|
||||
args: { query: 'topic' }, status: 'success', tool_call_result: { content: 'result' } }] };
|
||||
for (const input of [
|
||||
{ operation: 'history', input: { thread_id: 'thread-1' } },
|
||||
{ operation: 'channelConversation', input: { session_id: 'session-1' } },
|
||||
|
||||
@@ -52,7 +52,7 @@ it('keeps one child creation intent after a lost response and waits for Skill in
|
||||
await waitFor(() => expect(screen.queryByLabelText('子智能体名称')).not.toBeInTheDocument());
|
||||
const calls = api.call.mock.calls.filter(call => call[0] === 'createChild');
|
||||
expect(calls).toHaveLength(2); expect(calls[0][1]).toEqual(calls[1][1]);
|
||||
fireEvent.click(screen.getByRole('button', { name: '上传 Skill' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '上传技能' }));
|
||||
const confirm = await screen.findByRole('button', { name: '确认安装到个人能力' });
|
||||
expect(api.call.mock.calls.some(call => call[0] === 'confirmSkill')).toBe(false);
|
||||
fireEvent.click(confirm);
|
||||
|
||||
@@ -547,7 +547,10 @@ it('searches capabilities while retaining existing selections outside the result
|
||||
api.list.mockResolvedValue({ agents: [configured], next_cursor: null });
|
||||
const base = api.call.getMockImplementation()!;
|
||||
api.call.mockImplementation(async (operation, input) => operation === 'catalog' ? {
|
||||
models: [{ id: 'test-model', name: '写作模型' }], resources: { tools: [{ key: 'search', name: '资料搜索', description: '搜索素材' }, { key: 'clock', name: '当前时间' }] }, pricing: null,
|
||||
models: [{ id: 'test-model', name: '写作模型' }], resources: {
|
||||
tools: [{ key: 'web_search', name: '上网查资料', description: '在网上查找相关信息。' }, { key: 'clock', name: '当前时间' }],
|
||||
skills: [{ key: 'deep-research', name: '查资料,做研究', description: '比较不同说法,再整理出结论和出处。' }],
|
||||
}, pricing: null,
|
||||
} : base(operation, input));
|
||||
api.save.mockImplementation(async (_slug, input) => ({ ...configured, ...input, draft_revision: 2 }));
|
||||
open();
|
||||
@@ -561,9 +564,15 @@ it('searches capabilities while retaining existing selections outside the result
|
||||
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
|
||||
expect(screen.getByLabelText('搜索工具')).toHaveValue('资料');
|
||||
expect(screen.queryByRole('checkbox', { name: '当前时间' })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /资料搜索/ }));
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /上网查资料/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '完成选择' }));
|
||||
expect(screen.getByRole('checkbox', { name: /missing-tool/ })).toBeChecked();
|
||||
fireEvent.click(screen.getByRole('button', { name: '选择技能', exact: true }));
|
||||
fireEvent.change(screen.getByLabelText('搜索技能'), { target: { value: '出处' } });
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /查资料,做研究/ }));
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: '开始对话前,先读好已选技能的使用方法' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
|
||||
await waitFor(() => expect(api.save).toHaveBeenCalledWith(draft.slug, expect.objectContaining({ configuration: expect.objectContaining({ tools: ['missing-tool', 'search'] }) })));
|
||||
await waitFor(() => expect(api.save).toHaveBeenCalledWith(draft.slug, expect.objectContaining({
|
||||
configuration: expect.objectContaining({ tools: ['missing-tool', 'web_search'], skills: ['deep-research'], preload_skills: ['deep-research'] }),
|
||||
})));
|
||||
});
|
||||
@@ -74,14 +74,17 @@ it('associates tool arguments and results by call identity, keeps failures visib
|
||||
render(<CloudChat slug="agent" />);
|
||||
await waitFor(() => expect(api.events).toHaveBeenCalled());
|
||||
await emit([
|
||||
{ stream_event: { type: 'tool_call_delta', message_id: 'm', tool_call_id: 'search', name: '查资料', args_delta: '{"query":', index: 0 } },
|
||||
{ stream_event: { type: 'tool_call_delta', message_id: 'm', tool_call_id: 'search', name: 'web_search',
|
||||
display_name: '上网查资料', display_description: '在网上查找相关信息。', args_delta: '{"query":', index: 0 } },
|
||||
{ stream_event: { type: 'tool_call_delta', message_id: 'm', args_delta: '"天气"}', index: 0 } },
|
||||
{ stream_event: { type: 'tool_call', message_id: 'm', tool_call_id: 'write', name: '写文件', args: { path: '/a' }, index: 1 } },
|
||||
{ event: { method: 'tools', data: { event: 'tool-finished', tool_call_id: 'write', output: { content: '磁盘不足', status: 'error' } } } },
|
||||
{ event: { method: 'tools', data: { event: 'tool-finished', tool_call_id: 'search', output: { content: '晴天', status: 'success' } } } },
|
||||
], '101-0');
|
||||
const process = screen.getByTestId('cloud-message-process');
|
||||
expect(process).toHaveTextContent('查资料 · 已完成');
|
||||
expect(process).toHaveTextContent('上网查资料 · 已完成');
|
||||
expect(process).toHaveTextContent('在网上查找相关信息。');
|
||||
expect(process).toHaveTextContent('工具标识:web_search');
|
||||
expect(process).toHaveTextContent('写文件 · 失败');
|
||||
expect(within(process).getByText('{"query":"天气"}')).toBeInTheDocument();
|
||||
expect(within(process).getByText('晴天')).toBeInTheDocument();
|
||||
|
||||
Reference in new issue
Block a user