Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/app/core/agent_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -1416,6 +1416,7 @@ def _mark_session_running(self, chat_session: ChatSession) -> None:
chat_session.updated_at = utc_now()
self.db.add(chat_session)

@staticmethod
def _fallback_session_title_from_message(message: str) -> str:
return ConversationProjection.fallback_session_title(message)

Expand Down
52 changes: 46 additions & 6 deletions backend/app/core/harness_capability_invoker.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
HarnessToolCall,
HarnessToolContext,
build_file_tool_registry,
is_noise_artifact_path,
open_harness_artifact,
publish_changed_harness_artifacts,
register_command_tools,
Expand Down Expand Up @@ -721,13 +722,17 @@ def _invoke_general_skill(
for item in (structured.get("artifact_errors") or [])[:20]
if isinstance(item, dict)
]
declared = response.artifacts or [
item
for item in (structured.get("artifacts") or [])[:20]
if isinstance(item, dict)
]
if not declared and succeeded:
# 兜底:模型未在结果 JSON 声明产物时,自动扫描本次运行的 artifact_dir 补登,
# 产出文件不因"忘了声明"而丢失(声明式仍是首选路径)
declared = self._auto_declare_artifacts(structured)
artifacts, publish_errors = self._general_skill_artifacts(
response.artifacts
or [
item
for item in (structured.get("artifacts") or [])[:20]
if isinstance(item, dict)
],
declared,
skill_slug=skill.slug,
)
artifact_errors.extend(publish_errors)
Expand Down Expand Up @@ -774,6 +779,41 @@ def _invoke_general_skill(
},
}

def _auto_declare_artifacts(self, structured: dict[str, Any]) -> list[dict[str, Any]]:
"""未声明产物的兜底:扫描本次运行的 artifact_dir,把净产出文件自动登记为产物。

只接受 runner 写入 structured 的工作区相对 artifact_dir(我们强制注入的,
模型输出里的自报值已被覆盖);拒绝越出 TaskFrame 工作区的路径;缓存/中间
文件(点开头、__pycache__、*.tmp/*.part/*.log)不算产出;每个文件仍经
open_harness_artifact 校验。
"""
artifact_dir = str(structured.get("artifact_dir") or "").strip()
if not artifact_dir:
return []
try:
workspace_root = self.workspace_root.resolve()
root = (workspace_root / artifact_dir).resolve()
if workspace_root not in root.parents or not root.is_dir():
return []
declared: list[dict[str, Any]] = []
for path in sorted(root.rglob("*")):
if not path.is_file() or path.stat().st_size == 0:
continue
relative = path.relative_to(root).as_posix()
if is_noise_artifact_path(relative):
continue
declared.append({"path": f"{artifact_dir}/{relative}", "display_name": path.name})
if len(declared) >= 20:
break
except OSError:
return []
if declared:
self._emit_trace(
"general_skill_artifacts_auto_declared",
{"count": len(declared), "artifact_dir": artifact_dir},
)
return declared

def _general_skill_artifacts(
self,
declared: list[dict[str, Any]],
Expand Down
4 changes: 4 additions & 0 deletions backend/app/general_skills/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,10 @@ def _execute_plan(
artifact_root=artifact_dir,
workspace_root=workspace_root,
)
if workspace_root is not None:
# 供 invoker 在产物未声明时自动扫描补登(工作区相对路径);
# 强制覆盖:模型在输出 JSON 里自报的 artifact_dir 不可信,不得劫持扫描目录
structured["artifact_dir"] = artifact_dir.relative_to(workspace_root).as_posix()
if return_code != 0:
structured.setdefault("success", False)
structured.setdefault("error", f"runner exited with code {return_code}")
Expand Down
2 changes: 2 additions & 0 deletions backend/app/harness/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
HarnessArtifactAccessError,
HarnessWorkspaceSnapshot,
OpenedHarnessArtifact,
is_noise_artifact_path,
normalize_harness_artifact_path,
open_harness_artifact,
publish_changed_harness_artifacts,
Expand Down Expand Up @@ -51,6 +52,7 @@
"build_command_tool_registry",
"build_file_tool_registry",
"exec_command",
"is_noise_artifact_path",
"normalize_harness_artifact_path",
"open_harness_artifact",
"publish_artifact",
Expand Down
12 changes: 12 additions & 0 deletions backend/app/harness/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ def normalize_harness_artifact_path(raw_path: str) -> str:
return PurePosixPath(*parts).as_posix()


_NOISE_ARTIFACT_SUFFIXES = (".tmp", ".part", ".partial", ".cache", ".log", ".lock")


def is_noise_artifact_path(relative_path: str) -> bool:
"""缓存/中间产物判定(自动补登时排除):点开头文件、__pycache__、临时/日志后缀。"""
for part in relative_path.replace("\\", "/").split("/"):
if part.startswith(".") or part == "__pycache__":
return True
lower = relative_path.lower()
return lower.endswith(_NOISE_ARTIFACT_SUFFIXES)


def open_harness_artifact(
workspace_root: Path,
raw_path: str,
Expand Down
4 changes: 2 additions & 2 deletions backend/app/llm/prompts/general_skill_repair_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ Markdown 可能非常混乱,不一定有 frontmatter、标题、固定字段
- 如果选择 runtime=`python`,code 必须是完整 Python 代码,并从标准输入读取 JSON,字段包括 query、skill_slug、skill_name、skill_workspace、output_dir、skill_files。
- skill_workspace 是运行时恢复出的技能文件夹绝对路径;如果技能依赖同目录的脚本、模板、数据或说明文件,应从 skill_workspace 中读取,不要假设文件在当前仓库。
- 程序必须向标准输出打印一个 JSON 对象。
- 如果任务产生需要交付给用户下载的最终文件,必须写入 `OUTPUT_DIR`,并在标准输出 JSON 的 `artifacts` 数组中逐个显式声明相对 `OUTPUT_DIR` 的路径,可附带 `display_name` 和 `description`。
- `artifacts` 只列最终交付物;不得列入输入附件、技能包文件、缓存、日志、临时文件、runner 源码或构建中间产物。修复时必须保留这一显式交付协议,禁止改成扫描目录。
- 如果任务产生需要交付给用户下载的最终文件,必须写入 `ARTIFACT_DIR`(Python 使用 stdin 的 `artifact_dir`),并在标准输出 JSON 的 `artifacts` 数组中逐个显式声明相对该目录的路径,可附带 `display_name` 和 `description`。
- `artifacts` 只列最终交付物;不得列入输入附件、技能包文件、缓存、日志、临时文件、runner 源码或构建中间产物。修复时必须保留这一显式交付协议,禁止改成扫描目录。未声明时系统会把 artifact 目录里的文件自动补登为下载产物(丢失 display_name/description),显式声明仍是首选。
- 只能使用 SKILL.md 或 package.files 明确提供的脚本、数据、命令、URL 和 API。不要自行发明第三方接口、备用 URL 或在线服务;如果文档没有足够执行来源,返回稳定失败 JSON,并设置 retryable=false。
- 如果外部网络不可用、API 返回异常、页面结构无法解析或结果不符合预期,程序也必须返回稳定 JSON,不要崩溃。
- 失败 JSON 不要只写 `Fetch failed` 这种粗粒度错误;必须尽量包含 attempted_urls、status_code、exception_type、exception_message、response_preview、parse_strategy、retryable。
Expand Down
2 changes: 1 addition & 1 deletion backend/app/llm/prompts/general_skill_runner_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Markdown 可能非常混乱,不一定有 frontmatter、标题、固定字段
- skill_workspace 是运行时恢复出的技能文件夹绝对路径;如果技能依赖同目录的脚本、模板、数据或说明文件,应从 skill_workspace 中读取,不要假设文件在当前仓库。
- 程序必须向标准输出打印一个 JSON 对象。
- 如果任务产生需要交付给用户下载的最终文件,必须写入 `ARTIFACT_DIR`(Python 使用 stdin 的 `artifact_dir`),并在标准输出 JSON 的 `artifacts` 数组中逐个显式声明相对该目录的路径。可选字段为 `display_name` 和 `description`,例如 `{"success": true, "artifacts": [{"path": "report.xlsx", "display_name": "报销明细.xlsx"}]}`。不要输出 `/workspace/...` 或宿主机绝对路径。
- `artifacts` 只列最终交付物;不得列入输入附件、技能包文件、缓存、日志、临时文件、runner 源码或构建中间产物。未在 `artifacts` 中声明的文件不会出现在对话下载区
- `artifacts` 只列最终交付物;不得列入输入附件、技能包文件、缓存、日志、临时文件、runner 源码或构建中间产物。未声明时系统会把 artifact 目录里的文件自动补登为下载产物(丢失 display_name/description),显式声明仍是首选
- 只能使用 SKILL.md 或 package.files 明确提供的脚本、数据、命令、URL 和 API。不要自行发明第三方接口、备用 URL 或在线服务;如果文档没有足够执行来源,返回稳定失败 JSON,并设置 retryable=false。
- 如果外部网络不可用、API 返回异常、页面结构无法解析或结果不符合预期,程序也必须返回稳定 JSON,不要崩溃。
- 失败 JSON 不要只写 `Fetch failed` 这种粗粒度错误;必须尽量包含 attempted_urls、status_code、exception_type、exception_message、response_preview、parse_strategy、retryable。
Expand Down
Loading