mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
fix: fix frontend issue and let draft check relax
This commit is contained in:
parent
2fe4790722
commit
a5ff29eea5
@ -818,7 +818,7 @@ class SkillManagementService:
|
||||
with session_factory.create_session() as session:
|
||||
skill = self._require_skill(session, tenant_id=tenant_id, skill_id=skill_id)
|
||||
self._check_expected_updated_at(skill, payload.expected_updated_at)
|
||||
files = self._build_draft_rows_from_tree(skill=skill, payload=payload)
|
||||
files = self._build_draft_rows_from_tree(skill=skill, payload=payload, strict_frontmatter=False)
|
||||
session.execute(delete(SkillDraftFile).where(SkillDraftFile.skill_id == skill.id))
|
||||
session.flush()
|
||||
for file in files:
|
||||
@ -864,6 +864,7 @@ class SkillManagementService:
|
||||
files = self._build_draft_rows_from_tree(
|
||||
skill=skill,
|
||||
payload=SkillDraftTreePayload(files=updated_items),
|
||||
strict_frontmatter=False,
|
||||
)
|
||||
existing_files_by_path = {
|
||||
file.path: file
|
||||
@ -1424,14 +1425,21 @@ class SkillManagementService:
|
||||
agent = session.scalar(select(Agent).where(Agent.id == agent_id, Agent.tenant_id == tenant_id))
|
||||
if agent is None:
|
||||
raise SkillManagementServiceError("agent_not_found", "agent not found", status_code=404)
|
||||
found = set(session.scalars(select(Skill.id).where(Skill.tenant_id == tenant_id, Skill.id.in_(skill_ids))))
|
||||
missing = [skill_id for skill_id in skill_ids if skill_id not in found]
|
||||
skills = list(session.scalars(select(Skill).where(Skill.tenant_id == tenant_id, Skill.id.in_(skill_ids))))
|
||||
skills_by_id = {skill.id: skill for skill in skills}
|
||||
missing = [skill_id for skill_id in skill_ids if skill_id not in skills_by_id]
|
||||
if missing:
|
||||
raise SkillManagementServiceError(
|
||||
"skill_not_found",
|
||||
"one or more skills were not found",
|
||||
status_code=404,
|
||||
)
|
||||
self._check_agent_skill_name_conflicts(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
selected_skill_names=[skills_by_id[skill_id].name for skill_id in skill_ids],
|
||||
)
|
||||
session.query(AgentSkillBinding).filter(
|
||||
AgentSkillBinding.tenant_id == tenant_id,
|
||||
AgentSkillBinding.agent_id == agent_id,
|
||||
@ -1450,6 +1458,66 @@ class SkillManagementService:
|
||||
session.commit()
|
||||
return {"agent_id": agent_id, "skill_ids": skill_ids}
|
||||
|
||||
@staticmethod
|
||||
def _check_agent_skill_name_conflicts(
|
||||
session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
agent_id: str,
|
||||
selected_skill_names: list[str],
|
||||
) -> None:
|
||||
if not selected_skill_names:
|
||||
return
|
||||
|
||||
current_bound_names = set(
|
||||
session.scalars(
|
||||
select(Skill.name)
|
||||
.join(AgentSkillBinding, AgentSkillBinding.skill_id == Skill.id)
|
||||
.where(
|
||||
AgentSkillBinding.tenant_id == tenant_id,
|
||||
AgentSkillBinding.agent_id == agent_id,
|
||||
Skill.tenant_id == tenant_id,
|
||||
)
|
||||
)
|
||||
)
|
||||
configured_names: set[str] = set()
|
||||
snapshot = session.scalar(
|
||||
select(AgentConfigSnapshot).where(
|
||||
AgentConfigSnapshot.tenant_id == tenant_id,
|
||||
AgentConfigSnapshot.agent_id == agent_id,
|
||||
AgentConfigSnapshot.id
|
||||
== select(Agent.active_config_snapshot_id)
|
||||
.where(Agent.tenant_id == tenant_id, Agent.id == agent_id)
|
||||
.scalar_subquery(),
|
||||
)
|
||||
)
|
||||
if snapshot is not None:
|
||||
configured_names.update(
|
||||
skill.name
|
||||
for skill in AgentSoulConfig.model_validate(snapshot.config_snapshot_dict).config_skills
|
||||
if not skill.is_missing
|
||||
)
|
||||
drafts = session.scalars(
|
||||
select(AgentConfigDraft).where(
|
||||
AgentConfigDraft.tenant_id == tenant_id,
|
||||
AgentConfigDraft.agent_id == agent_id,
|
||||
)
|
||||
)
|
||||
for draft in drafts:
|
||||
configured_names.update(
|
||||
skill.name
|
||||
for skill in AgentSoulConfig.model_validate(draft.config_snapshot_dict).config_skills
|
||||
if not skill.is_missing
|
||||
)
|
||||
|
||||
conflicts = sorted(set(selected_skill_names) & (configured_names - current_bound_names))
|
||||
if conflicts:
|
||||
raise SkillManagementServiceError(
|
||||
"agent_skill_name_conflict",
|
||||
"agent already has a config skill with the same name",
|
||||
details={"names": conflicts},
|
||||
)
|
||||
|
||||
def list_agent_bindings(
|
||||
self,
|
||||
*,
|
||||
@ -2687,6 +2755,7 @@ class SkillManagementService:
|
||||
skill: Skill,
|
||||
payload: SkillDraftTreePayload,
|
||||
sync_frontmatter_name: bool = True,
|
||||
strict_frontmatter: bool = True,
|
||||
) -> list[SkillDraftFile]:
|
||||
entries_by_path: dict[str, SkillDraftTreeItemPayload] = {}
|
||||
for item in payload.files:
|
||||
@ -2698,15 +2767,18 @@ class SkillManagementService:
|
||||
if skill_md is None or skill_md.kind != SkillFileKind.FILE or skill_md.storage != SkillFileStorage.TEXT:
|
||||
raise SkillManagementServiceError("missing_skill_md", "skill must contain text SKILL.md")
|
||||
skill_md_content = skill_md.content or ""
|
||||
frontmatter = self._parse_frontmatter(skill_md_content)
|
||||
frontmatter_name = self._require_frontmatter_name(frontmatter, content=skill_md_content)
|
||||
if sync_frontmatter_name:
|
||||
self._sync_skill_metadata_from_skill_md(
|
||||
skill=skill,
|
||||
content=skill_md_content,
|
||||
parsed_frontmatter=frontmatter,
|
||||
validated_name=frontmatter_name,
|
||||
)
|
||||
if strict_frontmatter:
|
||||
frontmatter = self._parse_frontmatter(skill_md_content)
|
||||
frontmatter_name = self._require_frontmatter_name(frontmatter, content=skill_md_content)
|
||||
if sync_frontmatter_name:
|
||||
self._sync_skill_metadata_from_skill_md(
|
||||
skill=skill,
|
||||
content=skill_md_content,
|
||||
parsed_frontmatter=frontmatter,
|
||||
validated_name=frontmatter_name,
|
||||
)
|
||||
elif sync_frontmatter_name:
|
||||
self._sync_skill_metadata_from_draft_skill_md(skill=skill, content=skill_md_content)
|
||||
|
||||
file_paths = {path for path, item in entries_by_path.items() if item.kind == SkillFileKind.FILE}
|
||||
for path in file_paths:
|
||||
@ -2743,7 +2815,7 @@ class SkillManagementService:
|
||||
file_size = item.size
|
||||
file_hash = item.hash
|
||||
if item.kind == SkillFileKind.FILE and item.storage == SkillFileStorage.TEXT:
|
||||
if item.path == _SKILL_MD:
|
||||
if item.path == _SKILL_MD and strict_frontmatter:
|
||||
content_text = self._sync_skill_md_text(skill, content_text or "")
|
||||
content_bytes = (content_text or "").encode("utf-8")
|
||||
if len(content_bytes) > _MAX_FILE_BYTES:
|
||||
@ -2772,6 +2844,33 @@ class SkillManagementService:
|
||||
raise SkillManagementServiceError("skill_too_large", "skill exceeds 5MB limit")
|
||||
return rows
|
||||
|
||||
def _sync_skill_metadata_from_draft_skill_md(self, *, skill: Skill, content: str) -> None:
|
||||
"""Best-effort metadata sync for editor autosave.
|
||||
|
||||
Draft saves must accept temporarily incomplete frontmatter while the user
|
||||
is editing. Strict validation still runs on import and publish.
|
||||
"""
|
||||
try:
|
||||
frontmatter = self._parse_frontmatter(content)
|
||||
except SkillManagementServiceError:
|
||||
return
|
||||
name = frontmatter.get("name")
|
||||
if isinstance(name, str) and name.strip():
|
||||
try:
|
||||
validated_name = validate_skill_name(name)
|
||||
except ValueError:
|
||||
validated_name = None
|
||||
if validated_name is not None:
|
||||
if validated_name != skill.name:
|
||||
skill.name_manually_edited = True
|
||||
skill.name = validated_name
|
||||
description = frontmatter.get("description")
|
||||
if isinstance(description, str) and description.strip():
|
||||
skill.description = description.strip()[:1024]
|
||||
display_name = self._display_name_override_from_frontmatter(frontmatter)
|
||||
if display_name is not None:
|
||||
skill.display_name = display_name
|
||||
|
||||
def _sync_skill_md_text(self, skill: Skill, content: str) -> str:
|
||||
body = _FRONTMATTER_RE.sub("", content, count=1)
|
||||
metadata = self._parse_frontmatter(content)
|
||||
|
||||
@ -459,6 +459,76 @@ def test_list_agent_bindings_returns_draft_skill_card_data() -> None:
|
||||
assert bindings["data"][0]["latest_published_at"] is None
|
||||
|
||||
|
||||
def test_replace_agent_bindings_rejects_skill_name_conflict_with_agent_config_skill() -> None:
|
||||
service = SkillManagementService(tool_file_manager=_FakeToolFileManager())
|
||||
created = service.create_skill(tenant_id=TENANT, user_id=USER, payload=SkillCreatePayload(name="finance-sop"))
|
||||
with session_factory.create_session() as session:
|
||||
snapshot = AgentConfigSnapshot(
|
||||
tenant_id=TENANT,
|
||||
agent_id=AGENT,
|
||||
version=1,
|
||||
config_snapshot=AgentSoulConfig(
|
||||
config_skills=[
|
||||
AgentConfigSkillRefConfig(
|
||||
name="finance-sop",
|
||||
description="Existing uploaded config skill.",
|
||||
file_id="tool-file-1",
|
||||
)
|
||||
]
|
||||
),
|
||||
created_by=USER,
|
||||
)
|
||||
session.add(snapshot)
|
||||
session.flush()
|
||||
agent = session.get(Agent, AGENT)
|
||||
assert agent is not None
|
||||
agent.active_config_snapshot_id = snapshot.id
|
||||
session.commit()
|
||||
|
||||
with pytest.raises(SkillManagementServiceError) as exc_info:
|
||||
service.replace_agent_bindings(tenant_id=TENANT, user_id=USER, agent_id=AGENT, skill_ids=[created["id"]])
|
||||
|
||||
assert exc_info.value.code == "agent_skill_name_conflict"
|
||||
assert exc_info.value.details == {"names": ["finance-sop"]}
|
||||
|
||||
|
||||
def test_replace_agent_bindings_allows_existing_bound_workspace_skill_name_in_agent_config() -> None:
|
||||
service = SkillManagementService(tool_file_manager=_FakeToolFileManager())
|
||||
created = service.create_skill(tenant_id=TENANT, user_id=USER, payload=SkillCreatePayload(name="finance-sop"))
|
||||
service.replace_agent_bindings(tenant_id=TENANT, user_id=USER, agent_id=AGENT, skill_ids=[created["id"]])
|
||||
with session_factory.create_session() as session:
|
||||
snapshot = AgentConfigSnapshot(
|
||||
tenant_id=TENANT,
|
||||
agent_id=AGENT,
|
||||
version=1,
|
||||
config_snapshot=AgentSoulConfig(
|
||||
config_skills=[
|
||||
AgentConfigSkillRefConfig(
|
||||
name="finance-sop",
|
||||
description="Synced workspace skill.",
|
||||
file_id="tool-file-1",
|
||||
)
|
||||
]
|
||||
),
|
||||
created_by=USER,
|
||||
)
|
||||
session.add(snapshot)
|
||||
session.flush()
|
||||
agent = session.get(Agent, AGENT)
|
||||
assert agent is not None
|
||||
agent.active_config_snapshot_id = snapshot.id
|
||||
session.commit()
|
||||
|
||||
result = service.replace_agent_bindings(
|
||||
tenant_id=TENANT,
|
||||
user_id=USER,
|
||||
agent_id=AGENT,
|
||||
skill_ids=[created["id"]],
|
||||
)
|
||||
|
||||
assert result["skill_ids"] == [created["id"]]
|
||||
|
||||
|
||||
def test_list_skill_references_resolves_agent_apps_and_inline_workflow_nodes() -> None:
|
||||
service = SkillManagementService(tool_file_manager=_FakeToolFileManager())
|
||||
created = service.create_skill(
|
||||
@ -629,6 +699,24 @@ def test_publish_updates_referenced_agent_config_skill_archives() -> None:
|
||||
payload=SkillCreatePayload(name="finance-sop"),
|
||||
)
|
||||
inline_agent_id = "77777777-7777-7777-7777-777777777777"
|
||||
with session_factory.create_session() as session:
|
||||
session.add(
|
||||
Agent(
|
||||
id=inline_agent_id,
|
||||
tenant_id=TENANT,
|
||||
name="Agent 内嵌节点 C",
|
||||
scope=AgentScope.WORKFLOW_ONLY,
|
||||
source=AgentSource.WORKFLOW,
|
||||
app_id="66666666-6666-6666-6666-666666666666",
|
||||
workflow_id="88888888-8888-8888-8888-888888888888",
|
||||
workflow_node_id="node-c",
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
|
||||
service.replace_agent_bindings(tenant_id=TENANT, user_id=USER, agent_id=AGENT, skill_ids=[created["id"]])
|
||||
service.replace_agent_bindings(tenant_id=TENANT, user_id=USER, agent_id=inline_agent_id, skill_ids=[created["id"]])
|
||||
|
||||
with session_factory.create_session() as session:
|
||||
agent_snapshot = AgentConfigSnapshot(
|
||||
tenant_id=TENANT,
|
||||
@ -675,18 +763,6 @@ def test_publish_updates_referenced_agent_config_skill_archives() -> None:
|
||||
updated_by=USER,
|
||||
)
|
||||
)
|
||||
session.add(
|
||||
Agent(
|
||||
id=inline_agent_id,
|
||||
tenant_id=TENANT,
|
||||
name="Agent 内嵌节点 C",
|
||||
scope=AgentScope.WORKFLOW_ONLY,
|
||||
source=AgentSource.WORKFLOW,
|
||||
app_id="66666666-6666-6666-6666-666666666666",
|
||||
workflow_id="88888888-8888-8888-8888-888888888888",
|
||||
workflow_node_id="node-c",
|
||||
)
|
||||
)
|
||||
inline_snapshot = AgentConfigSnapshot(
|
||||
tenant_id=TENANT,
|
||||
agent_id=inline_agent_id,
|
||||
@ -724,8 +800,6 @@ def test_publish_updates_referenced_agent_config_skill_archives() -> None:
|
||||
)
|
||||
session.commit()
|
||||
|
||||
service.replace_agent_bindings(tenant_id=TENANT, user_id=USER, agent_id=AGENT, skill_ids=[created["id"]])
|
||||
service.replace_agent_bindings(tenant_id=TENANT, user_id=USER, agent_id=inline_agent_id, skill_ids=[created["id"]])
|
||||
service.publish_skill(tenant_id=TENANT, user_id=USER, skill_id=created["id"], payload=SkillPublishPayload())
|
||||
|
||||
with session_factory.create_session() as session:
|
||||
@ -1032,77 +1106,90 @@ def test_publish_syncs_frontmatter_display_name_from_existing_draft() -> None:
|
||||
assert detail["description"] == "Handle refund approvals."
|
||||
|
||||
|
||||
def test_replace_draft_tree_rejects_missing_frontmatter_name() -> None:
|
||||
def test_replace_draft_tree_allows_missing_frontmatter_name_until_publish() -> None:
|
||||
service = SkillManagementService(tool_file_manager=_FakeToolFileManager())
|
||||
created = service.create_skill(tenant_id=TENANT, user_id=USER, payload=SkillCreatePayload(name="finance-sop"))
|
||||
|
||||
with pytest.raises(SkillManagementServiceError) as exc_info:
|
||||
service.replace_draft_tree(
|
||||
tenant_id=TENANT,
|
||||
user_id=USER,
|
||||
skill_id=created["id"],
|
||||
payload=SkillDraftTreePayload(files=[{"path": "SKILL.md", "content": "# Missing frontmatter"}]),
|
||||
)
|
||||
draft = service.replace_draft_tree(
|
||||
tenant_id=TENANT,
|
||||
user_id=USER,
|
||||
skill_id=created["id"],
|
||||
payload=SkillDraftTreePayload(files=[{"path": "SKILL.md", "content": "# Missing frontmatter"}]),
|
||||
)
|
||||
|
||||
assert draft["name"] == "finance-sop"
|
||||
assert next(item for item in draft["files"] if item["path"] == "SKILL.md")["content"] == "# Missing frontmatter"
|
||||
|
||||
with pytest.raises(SkillManagementServiceError) as exc_info:
|
||||
service.publish_skill(tenant_id=TENANT, user_id=USER, skill_id=created["id"], payload=SkillPublishPayload())
|
||||
assert exc_info.value.code == "missing_skill_name"
|
||||
assert exc_info.value.details == {"path": "SKILL.md", "field": "name", "line": 2}
|
||||
|
||||
|
||||
def test_replace_draft_tree_rejects_missing_frontmatter_description() -> None:
|
||||
def test_replace_draft_tree_allows_missing_frontmatter_description_until_publish() -> None:
|
||||
service = SkillManagementService(tool_file_manager=_FakeToolFileManager())
|
||||
created = service.create_skill(tenant_id=TENANT, user_id=USER, payload=SkillCreatePayload(name="finance-sop"))
|
||||
|
||||
with pytest.raises(SkillManagementServiceError) as exc_info:
|
||||
service.replace_draft_tree(
|
||||
tenant_id=TENANT,
|
||||
user_id=USER,
|
||||
skill_id=created["id"],
|
||||
payload=SkillDraftTreePayload(
|
||||
files=[{"path": "SKILL.md", "content": "---\nname: finance-sop\n---\n# Missing description"}]
|
||||
),
|
||||
)
|
||||
draft = service.replace_draft_tree(
|
||||
tenant_id=TENANT,
|
||||
user_id=USER,
|
||||
skill_id=created["id"],
|
||||
payload=SkillDraftTreePayload(
|
||||
files=[{"path": "SKILL.md", "content": "---\nname: finance-sop\n---\n# Missing description"}]
|
||||
),
|
||||
)
|
||||
|
||||
assert draft["description"] == "Describe what this Skill does and when an Agent should use it."
|
||||
assert "description:" not in next(item for item in draft["files"] if item["path"] == "SKILL.md")["content"]
|
||||
|
||||
with pytest.raises(SkillManagementServiceError) as exc_info:
|
||||
service.publish_skill(tenant_id=TENANT, user_id=USER, skill_id=created["id"], payload=SkillPublishPayload())
|
||||
assert exc_info.value.code == "missing_skill_description"
|
||||
assert exc_info.value.details == {"path": "SKILL.md", "field": "description", "line": 2}
|
||||
|
||||
|
||||
def test_replace_draft_tree_rejects_blank_frontmatter_description() -> None:
|
||||
def test_replace_draft_tree_allows_blank_frontmatter_description_until_publish() -> None:
|
||||
service = SkillManagementService(tool_file_manager=_FakeToolFileManager())
|
||||
created = service.create_skill(tenant_id=TENANT, user_id=USER, payload=SkillCreatePayload(name="finance-sop"))
|
||||
|
||||
with pytest.raises(SkillManagementServiceError) as exc_info:
|
||||
service.replace_draft_tree(
|
||||
tenant_id=TENANT,
|
||||
user_id=USER,
|
||||
skill_id=created["id"],
|
||||
payload=SkillDraftTreePayload(
|
||||
files=[{"path": "SKILL.md", "content": "---\nname: finance-sop\ndescription: ''\n---\n# Blank"}]
|
||||
),
|
||||
)
|
||||
draft = service.replace_draft_tree(
|
||||
tenant_id=TENANT,
|
||||
user_id=USER,
|
||||
skill_id=created["id"],
|
||||
payload=SkillDraftTreePayload(
|
||||
files=[{"path": "SKILL.md", "content": "---\nname: finance-sop\ndescription: ''\n---\n# Blank"}]
|
||||
),
|
||||
)
|
||||
|
||||
assert draft["description"] == "Describe what this Skill does and when an Agent should use it."
|
||||
assert "description: ''" in next(item for item in draft["files"] if item["path"] == "SKILL.md")["content"]
|
||||
|
||||
with pytest.raises(SkillManagementServiceError) as exc_info:
|
||||
service.publish_skill(tenant_id=TENANT, user_id=USER, skill_id=created["id"], payload=SkillPublishPayload())
|
||||
assert exc_info.value.code == "missing_skill_description"
|
||||
assert exc_info.value.details == {"path": "SKILL.md", "field": "description", "line": 3}
|
||||
|
||||
|
||||
def test_replace_draft_tree_reports_actual_frontmatter_name_line() -> None:
|
||||
def test_publish_reports_actual_frontmatter_name_line() -> None:
|
||||
service = SkillManagementService(tool_file_manager=_FakeToolFileManager())
|
||||
created = service.create_skill(tenant_id=TENANT, user_id=USER, payload=SkillCreatePayload(name="finance-sop"))
|
||||
|
||||
with pytest.raises(SkillManagementServiceError) as exc_info:
|
||||
service.replace_draft_tree(
|
||||
tenant_id=TENANT,
|
||||
user_id=USER,
|
||||
skill_id=created["id"],
|
||||
payload=SkillDraftTreePayload(
|
||||
files=[
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"content": "---\ndescription: x\nmetadata:\nname: bad_name\n---\n# Body",
|
||||
}
|
||||
]
|
||||
),
|
||||
)
|
||||
service.replace_draft_tree(
|
||||
tenant_id=TENANT,
|
||||
user_id=USER,
|
||||
skill_id=created["id"],
|
||||
payload=SkillDraftTreePayload(
|
||||
files=[
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"content": "---\ndescription: x\nmetadata:\nname: bad_name\n---\n# Body",
|
||||
}
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
with pytest.raises(SkillManagementServiceError) as exc_info:
|
||||
service.publish_skill(tenant_id=TENANT, user_id=USER, skill_id=created["id"], payload=SkillPublishPayload())
|
||||
assert exc_info.value.code == "invalid_skill_name"
|
||||
assert exc_info.value.details == {"path": "SKILL.md", "field": "name", "line": 4}
|
||||
|
||||
@ -1311,6 +1398,7 @@ def test_delete_skill_requires_confirmation_when_referenced() -> None:
|
||||
def test_delete_skill_removes_synced_agent_config_skill_refs() -> None:
|
||||
service = SkillManagementService(tool_file_manager=_FakeToolFileManager())
|
||||
created = service.create_skill(tenant_id=TENANT, user_id=USER, payload=SkillCreatePayload(name="finance-sop"))
|
||||
service.replace_agent_bindings(tenant_id=TENANT, user_id=USER, agent_id=AGENT, skill_ids=[created["id"]])
|
||||
with session_factory.create_session() as session:
|
||||
agent_snapshot = AgentConfigSnapshot(
|
||||
tenant_id=TENANT,
|
||||
@ -1366,8 +1454,6 @@ def test_delete_skill_removes_synced_agent_config_skill_refs() -> None:
|
||||
)
|
||||
session.commit()
|
||||
|
||||
service.replace_agent_bindings(tenant_id=TENANT, user_id=USER, agent_id=AGENT, skill_ids=[created["id"]])
|
||||
|
||||
deleted = service.delete_skill(tenant_id=TENANT, skill_id=created["id"], confirmation_name="finance-sop")
|
||||
|
||||
with session_factory.create_session() as session:
|
||||
|
||||
@ -27,6 +27,23 @@ export const viewport: Viewport = {
|
||||
viewportFit: 'cover',
|
||||
}
|
||||
|
||||
const resizeObserverErrorFilterScript = `
|
||||
(() => {
|
||||
const ignoredMessages = new Set([
|
||||
'ResizeObserver loop completed with undelivered notifications.',
|
||||
'ResizeObserver loop limit exceeded',
|
||||
]);
|
||||
const ignore = (event) => {
|
||||
const message = event?.message || event?.reason?.message;
|
||||
if (!ignoredMessages.has(message)) return;
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
};
|
||||
window.addEventListener('error', ignore, true);
|
||||
window.addEventListener('unhandledrejection', ignore, true);
|
||||
})();
|
||||
`
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const datasetMap = getDatasetMap()
|
||||
const queryClient = getQueryClientServer()
|
||||
@ -43,6 +60,11 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<html lang={locale ?? 'en'} className="h-full" suppressHydrationWarning>
|
||||
<head>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<script
|
||||
nonce={nonce}
|
||||
// oxlint-disable-next-line eslint-react/dom-no-dangerously-set-innerhtml -- Static early listener must run before the dev error overlay registers.
|
||||
dangerouslySetInnerHTML={{ __html: resizeObserverErrorFilterScript }}
|
||||
/>
|
||||
<meta name="theme-color" content="#1C64F2" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import type { AgentAppPartial, AgentIconType } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -224,7 +225,12 @@ function AgentRosterItem({ agent }: { agent: AgentAppPartial }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pointer-events-none absolute top-2 right-2 z-20 flex items-center overflow-hidden rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 opacity-0 shadow-lg backdrop-blur-xs transition-opacity group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100 has-data-popup-open:pointer-events-auto has-data-popup-open:opacity-100">
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute right-2 z-20 flex items-center overflow-hidden rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 opacity-0 shadow-lg backdrop-blur-xs transition-opacity group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100 has-data-popup-open:pointer-events-auto has-data-popup-open:opacity-100',
|
||||
isDraft ? 'top-7' : 'top-2',
|
||||
)}
|
||||
>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
aria-label={t(($) => $['roster.moreActions'], { name: agent.name })}
|
||||
|
||||
@ -555,6 +555,34 @@ function setMarkdownDisplayName(content: string, value: string) {
|
||||
return addMarkdownMetadata(nextContent, 'display-name', value)
|
||||
}
|
||||
|
||||
function setMarkdownFrontmatterField(content: string, key: 'description' | 'name', value: string) {
|
||||
const fieldLine = `${key}: ${stringifyYamlValue(value)}`
|
||||
|
||||
if (!content.startsWith('---')) {
|
||||
return `---\n${fieldLine}\n---\n\n${content}`
|
||||
}
|
||||
|
||||
const lines = content.split(/\r?\n/)
|
||||
const closingIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---')
|
||||
if (closingIndex === -1) {
|
||||
return `---\n${fieldLine}\n---\n\n${content}`
|
||||
}
|
||||
|
||||
for (let index = 1; index < closingIndex; index += 1) {
|
||||
const line = lines[index] ?? ''
|
||||
if (line.startsWith(' ') || line.startsWith('\t')) continue
|
||||
|
||||
const separatorIndex = line.indexOf(':')
|
||||
if (separatorIndex <= 0) continue
|
||||
|
||||
if (line.slice(0, separatorIndex).trim() === key) {
|
||||
return [...lines.slice(0, index), fieldLine, ...lines.slice(index + 1)].join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
return [...lines.slice(0, closingIndex), fieldLine, ...lines.slice(closingIndex)].join('\n')
|
||||
}
|
||||
|
||||
function removeMarkdownMetadata(content: string, key: string) {
|
||||
const trimmedKey = key.trim()
|
||||
if (!isEditableMetadataKey(trimmedKey) || isProtectedMarkdownMetadataKey(trimmedKey))
|
||||
@ -4338,20 +4366,53 @@ function FileEditor({
|
||||
markdownContent.metadata.length > 0 ||
|
||||
!readonly) && (
|
||||
<div className="mb-8 space-y-5">
|
||||
{markdownContent.name && (
|
||||
{(markdownContent.name || !readonly) && (
|
||||
<div className="max-w-full space-y-1">
|
||||
<div className="system-sm-regular text-text-tertiary">name</div>
|
||||
<div className="max-w-[320px] truncate system-sm-regular text-text-secondary">
|
||||
{markdownContent.name}
|
||||
</div>
|
||||
{readonly ? (
|
||||
<div className="max-w-[320px] truncate system-sm-regular text-text-secondary">
|
||||
{markdownContent.name || '-'}
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
value={markdownContent.name}
|
||||
className="h-8 w-[280px] max-w-full rounded-lg border border-transparent bg-transparent px-0 system-sm-regular text-text-secondary outline-hidden placeholder:text-text-quaternary hover:border-divider-regular hover:bg-background-default hover:px-2.5 focus-visible:border-divider-regular focus-visible:bg-background-default focus-visible:px-2.5 focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||
onChange={(event) => {
|
||||
updateDraftContent(
|
||||
setMarkdownFrontmatterField(
|
||||
draftContentRef.current,
|
||||
'name',
|
||||
event.target.value,
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{markdownContent.description && (
|
||||
{(markdownContent.description || !readonly) && (
|
||||
<div className="max-w-full space-y-1">
|
||||
<div className="system-sm-regular text-text-tertiary">description</div>
|
||||
<div className="max-w-[520px] system-sm-regular break-words whitespace-pre-wrap text-text-secondary">
|
||||
{markdownContent.description}
|
||||
</div>
|
||||
{readonly ? (
|
||||
<div className="max-w-[520px] system-sm-regular break-words whitespace-pre-wrap text-text-secondary">
|
||||
{markdownContent.description || '-'}
|
||||
</div>
|
||||
) : (
|
||||
<textarea
|
||||
value={markdownContent.description}
|
||||
rows={2}
|
||||
className="min-h-8 w-[520px] max-w-full resize-none rounded-lg border border-transparent bg-transparent px-0 py-1 system-sm-regular text-text-secondary outline-hidden placeholder:text-text-quaternary hover:border-divider-regular hover:bg-background-default hover:px-2.5 focus-visible:border-divider-regular focus-visible:bg-background-default focus-visible:px-2.5 focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||
onChange={(event) => {
|
||||
updateDraftContent(
|
||||
setMarkdownFrontmatterField(
|
||||
draftContentRef.current,
|
||||
'description',
|
||||
event.target.value,
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(markdownContent.displayName || !readonly) && (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user