feat(skills/settings): 技能复制 + 设置持久化等会话前改动

- 技能复制:新增 POST 复制接口(_rewrite_skill_frontmatter_name 重写 frontmatter name + 校验 + 去重),前端 skills api/hooks 与技能设置页配套
- 设置持久化:lastAgentId、线程 RAG 模式锁(settings/index、local)
- i18n(en/zh/types)、agent-card/input-box/message-list 等页面文案与交互微调
- scheduler/element.py 小修

(本批为会话前已存在的工作区改动,按要求一并提交;构建产物 backend.zip 未提交)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
AdministratorandClaude Opus 4.8 authored and xuhd committed 2026-06-03 11:38:47 +08:00
1 parent adcc5a358b
commit 133054e2dd
18 files changed
+526 -54

No files matched your search

+5 -1
View File
@@ -26,7 +26,7 @@ import { useAgent } from "@/core/agents";
import { useI18n } from "@/core/i18n/hooks";
import { useModels } from "@/core/models/hooks";
import { useNotification } from "@/core/notification/hooks";
import { useThreadSettings } from "@/core/settings";
import { saveLastAgentId, useThreadSettings } from "@/core/settings";
import { useThreadStream } from "@/core/threads/hooks";
import { textOfMessage } from "@/core/threads/utils";
import { env } from "@/env";
@@ -114,6 +114,9 @@ export default function AgentChatPage() {
const handleSubmit = useCallback(
(message: PromptInputMessage) => {
setHasStartedConversation(true);
// Remember this agent so the new-chat landing screen pre-selects it next
// time (covers both direct agent chats and the ChatPage handoff replay).
saveLastAgentId(agent_id);
window.dispatchEvent(
new CustomEvent("workspace:sandbox-resume-autoscroll"),
);
@@ -216,6 +219,7 @@ export default function AgentChatPage() {
paddingBottom={messageListPaddingBottom}
tokenUsageEnabled={tokenUsageEnabled}
onClarificationSubmit={handleClarificationSubmit}
onEditResubmit={(text) => handleSubmit({ text, files: [] })}
/>
</div>
+33 -3
View File
@@ -23,10 +23,15 @@ import { ThreadTitle } from "@/components/workspace/thread-title";
import { TodoList } from "@/components/workspace/todo-list";
import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator";
import { Welcome } from "@/components/workspace/welcome";
import { useAgentSelector } from "@/core/agents";
import { useI18n } from "@/core/i18n/hooks";
import { useModels } from "@/core/models/hooks";
import { useNotification } from "@/core/notification/hooks";
import { useThreadSettings } from "@/core/settings";
import {
getLastAgentId,
saveLastAgentId,
useThreadSettings,
} from "@/core/settings";
import { useThreadStream } from "@/core/threads/hooks";
import { useRouteBase } from "@/core/route-base";
import { textOfMessage } from "@/core/threads/utils";
@@ -47,6 +52,8 @@ export default function ChatPage() {
const { data: promptPrefixSettings } = usePromptPrefixSettings();
const [promptPrefixOn, setPromptPrefixOn] = useState(false);
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
const { agents: selectorAgents, isLoading: selectorLoading } =
useAgentSelector();
const { selectedArtifact } = useArtifacts();
useSpecificChatMode();
@@ -58,10 +65,22 @@ export default function ChatPage() {
if (isNewThread) {
setHasStartedConversation(false);
setPromptPrefixOn(false);
setSelectedAgentId(null);
// Pre-select the agent the user last started a conversation with so a
// fresh chat defaults to it. Validated against the selector list below.
setSelectedAgentId(getLastAgentId());
}
}, [isNewThread, threadId]);
// Drop a restored last-used agent that is no longer in the selector list
// (deleted / unpublished / no longer curated), so we never hand off to a
// dead agent. Wait for the list to load before deciding.
useEffect(() => {
if (selectorLoading || !selectedAgentId) return;
if (!selectorAgents.some((a) => a.id === selectedAgentId)) {
setSelectedAgentId(null);
}
}, [selectedAgentId, selectorAgents, selectorLoading]);
const { showNotification } = useNotification();
const [thread, sendMessage, isUploading] = useThreadStream({
@@ -184,6 +203,14 @@ export default function ChatPage() {
},
[sendMessage, threadId],
);
// Persist the picked agent the moment it changes so the next new chat
// defaults to it — even if the user never sends a message this session.
// Passing null (the ✕ clear) wipes the stored preference.
const handleAgentChange = useCallback((agentId: string | null) => {
setSelectedAgentId(agentId);
saveLastAgentId(agentId);
}, []);
const handleStop = useCallback(async () => {
await thread.stop();
}, [thread]);
@@ -248,6 +275,9 @@ export default function ChatPage() {
paddingBottom={messageListPaddingBottom}
tokenUsageEnabled={tokenUsageEnabled}
onClarificationSubmit={handleClarificationSubmit}
onEditResubmit={(text) =>
handleSubmit({ text, files: [] })
}
/>
)}
</div>
@@ -305,7 +335,7 @@ export default function ChatPage() {
isLandingState
? {
selectedAgentId,
onAgentChange: setSelectedAgentId,
onAgentChange: handleAgentChange,
}
: undefined
}
+3 -3
View File
@@ -545,13 +545,13 @@ export default function NewAgentPage() {
没有匹配的技能。
</p>
) : (
<div className="flex flex-wrap gap-2">
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
{filteredSkills.map((skill) => {
const checked = selectedSkills.includes(skill.name);
return (
<div
key={skill.name}
className="inline-flex max-w-full items-center"
className="flex w-full min-w-0 items-center"
>
<Button
type="button"
@@ -560,7 +560,7 @@ export default function NewAgentPage() {
onClick={() => toggleSkill(skill)}
title={skill.description}
className={cn(
"h-8 max-w-[200px] rounded-r-none",
"h-8 min-w-0 flex-1 rounded-r-none",
!checked && "text-muted-foreground",
)}
>