Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .agents/skills/offeru/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ user-invocable: true
argument-hint: "[skill-id | goal | JD/URL]"
---

<!-- generated: offeru-skill-registry@2026-07-30.2 sha256=bc92f50701240cdc9c8f42c63a8dd440cccce7aa8af5c7b543f8045dc11debaf -->
<!-- generated: offeru-skill-registry@2026-07-30.2 sha256=aaed3fc9f2d46ef3564d49f1a20fdd408c03779eea2554b710b7274118406f23 -->

# OfferU External-Agent Router

Expand Down
2 changes: 1 addition & 1 deletion .claude/agents/offeru-operator.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ skills:
- offeru
---

<!-- generated: offeru-skill-registry@2026-07-30.2 sha256=bc92f50701240cdc9c8f42c63a8dd440cccce7aa8af5c7b543f8045dc11debaf -->
<!-- generated: offeru-skill-registry@2026-07-30.2 sha256=aaed3fc9f2d46ef3564d49f1a20fdd408c03779eea2554b710b7274118406f23 -->

You are the OfferU operator subagent. Work from `backend/` and treat the live CLI manifest as the only capability source.

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/offeru/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ user-invocable: true
argument-hint: "[skill-id | goal | JD/URL]"
---

<!-- generated: offeru-skill-registry@2026-07-30.2 sha256=bc92f50701240cdc9c8f42c63a8dd440cccce7aa8af5c7b543f8045dc11debaf -->
<!-- generated: offeru-skill-registry@2026-07-30.2 sha256=aaed3fc9f2d46ef3564d49f1a20fdd408c03779eea2554b710b7274118406f23 -->

# OfferU External-Agent Router

Expand Down
2 changes: 1 addition & 1 deletion .codex/agents/offeru-operator.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# generated: offeru-skill-registry@2026-07-30.2 sha256=bc92f50701240cdc9c8f42c63a8dd440cccce7aa8af5c7b543f8045dc11debaf
# generated: offeru-skill-registry@2026-07-30.2 sha256=aaed3fc9f2d46ef3564d49f1a20fdd408c03779eea2554b710b7274118406f23
name = "offeru-operator"
description = "Operate OfferU through its live Skill Registry and atomic CLI control contract."
developer_instructions = """
Expand Down
2 changes: 1 addition & 1 deletion .copilot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ user-invocable: true
argument-hint: "[skill-id | goal | JD/URL]"
---

<!-- generated: offeru-skill-registry@2026-07-30.2 sha256=bc92f50701240cdc9c8f42c63a8dd440cccce7aa8af5c7b543f8045dc11debaf -->
<!-- generated: offeru-skill-registry@2026-07-30.2 sha256=aaed3fc9f2d46ef3564d49f1a20fdd408c03779eea2554b710b7274118406f23 -->

# OfferU External-Agent Router

Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ Playwright MCP 或Browser 来访问/截图/识别/探索网站的视觉和代码
- **扩展验收同样不得选系统浏览器**:`extension/scripts/` 下的 fixture、smoke 和 E2E 脚本必须使用 Playwright 自带的 managed Chromium、临时隔离 profile 与 `headless: true`;不得扫描或传入 Chrome/Edge 可执行文件路径。
- **仓库内所有自动浏览器脚本都遵守同一边界**:包括根目录临时/历史脚本;统一使用 Playwright managed Chromium 与 `headless: true`,不得保留系统 Chrome/Edge 的 `executablePath`、channel 或可见窗口入口。用户主动触发的授权登录窗口是唯一例外,且不属于自动验收。
- **测试临时空间固定使用 H 盘**:Agent、浏览器、后端、前端和扩展测试产生的工作目录、缓存、隔离 profile、日志与截图统一放到 `H:\tmp\offeru` 或其子目录;不得把测试临时目录写入 C 盘。已安装的 Agent 可执行文件及其原生认证目录只读使用,不搬迁、不清理用户凭据。
- **旧二进制不得作为入口**:仓库根目录若存在版本低于当前 Release 的 `OfferU.exe`(当前发现为历史 `0.1.0`),不得自动启动、覆盖或作为验收依据;当前源码开发只使用 `DEVELOPMENT.md` 的 `7410/8766` 进程,正式使用只接受经过 Release Gate 的安装包。
- **Agent-native 产品验收不得用 Playwright 代替 Agent 操作**:当验收目标是“本地 Coding Agent 使用 OfferU”时,Coding Agent 必须通过 OfferU Skill → CLI/Bridge → Operation Registry 操作业务能力;Playwright 只能作为独立的前端回归测试,不得充当 Agent。用户可以自行打开可见 OfferU 前端作为观察、编辑和 HITL 确认界面,这不属于自动浏览器验收,也不受 `headless=true` 限制。
- **禁止 scripted executor 冒充 Agent**:预先根据 prompt 关键字写死 Operation 序列的 deterministic executor 只能标为 workflow/smoke,不得标记为 OMP/SWE-2 Agent E2E;Agent E2E 必须保留可验证的模型身份和 model-issued tool calls。

## Agent skills

Expand Down
60 changes: 50 additions & 10 deletions backend/app/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
from app.ops import get_operation_schema, list_operations
from app.bridge_cli import main as bridge_main
from app.runtime_paths import runtime_data_dir, runtime_uploads_dir
from app.services.agent_skill_registry import catalog, registry_snapshot, resolve_skill
from app.services.agent_skill_registry import (
agent_operation_names,
catalog,
registry_snapshot,
resolve_skill,
)
from app.services.agent_host_registry import host_capability_matrix
from app.services.operation_projection import (
confirm_operation_proposal,
Expand Down Expand Up @@ -107,6 +112,12 @@ def main(argv: Optional[list[str]] = None) -> int:
{
"ok": True,
"operation_count": len(list_operations()),
"operation_registry_count": len(list_operations()),
"agent_tool_count": len(agent_operation_names()),
"featured_tool_count": len(agent_operation_names(featured_only=True)),
"internal_operation_count": (
len(list_operations()) - len(agent_operation_names())
),
"returned_count": len(operations),
"selector": selector,
"operations": operations,
Expand Down Expand Up @@ -154,6 +165,9 @@ def main(argv: Optional[list[str]] = None) -> int:
args.pretty,
exit_code=0 if result.get("ok") else 1,
)
if args.command == "ui":
from app.ui_cli import main as ui_main
return ui_main(["ui"] + args.ui_args)
return _print({"ok": False, "errors": ["缺少命令"], "commands": _commands()}, exit_code=2)
except KeyboardInterrupt:
return _print({"ok": False, "errors": ["interrupted"]}, getattr(args, "pretty", False), exit_code=130)
Expand Down Expand Up @@ -189,11 +203,19 @@ def _build_parser() -> JsonArgumentParser:
manifest_selector.add_argument("--group", default="", help="Show compact Operation summaries for one group.")
manifest_selector.add_argument("--all", dest="all_operations", action="store_true", help="Show the full developer audit manifest.")

ops = sub.add_parser("ops", help="Discover atomic internal operations.", add_help=False)
ops = sub.add_parser(
"ops",
help="Discover Agent-facing OfferU tools; --all is the raw Operation Registry audit.",
add_help=False,
)
ops.add_argument("--pretty", action="store_true", help="Pretty-print JSON.")
ops_selector = ops.add_mutually_exclusive_group()
ops_selector.add_argument("--skill", default="", help="List compact Operations for one Skill.")
ops_selector.add_argument("--group", default="", help="List compact Operations for one group.")
ops_selector.add_argument(
"--group",
default="",
help="List Agent-exposed Operations for one group; internal/legacy entries stay hidden.",
)
ops_selector.add_argument("--all", dest="all_operations", action="store_true", help="List every full Operation schema for auditing.")

schema = sub.add_parser("schema", help="Show one operation schema.", add_help=False)
Expand Down Expand Up @@ -246,11 +268,15 @@ def _build_parser() -> JsonArgumentParser:
)
conformance.add_argument("--refresh", action="store_true", help="Bypass local version probe cache.")
conformance.add_argument("--pretty", action="store_true", help="Pretty-print JSON.")

ui = sub.add_parser("ui", help="UI automation commands (open/click/fill/eval/screenshot/content/wait).", add_help=False)
ui.add_argument("ui_args", nargs=argparse.REMAINDER, help="Arguments passed to ui command.")

return parser


def _commands() -> list[str]:
return ["doctor", "manifest", "ops", "schema", "run", "confirm", "conformance", "bridge"]
return ["doctor", "manifest", "ops", "schema", "run", "confirm", "conformance", "bridge", "ui"]


def _doctor() -> dict[str, Any]:
Expand Down Expand Up @@ -716,14 +742,13 @@ def _select_operations(
known_groups = {str(operation.get("group") or "ungrouped") for operation in operations}
if clean_group not in known_groups:
raise ValueError(f"未知能力组: {clean_group}")
# Group discovery is an Agent surface, not a raw Registry dump. UI-only,
# migration, diagnostic and compatibility Operations stay reachable by
# exact schema/run name and by the explicit --all developer audit path.
selected_names = agent_operation_names()
selector = f"group:{clean_group}"
elif default_featured and not all_operations:
selected_names = {
str(operation_name)
for skill_item in catalog()
if skill_item.get("featured")
for operation_name in skill_item.get("allowed_tools", [])
}
selected_names = agent_operation_names(featured_only=True)
selector = "featured_skills"
if selected_names is not None:
operations = [operation for operation in operations if operation.get("name") in selected_names]
Expand Down Expand Up @@ -776,6 +801,13 @@ def _manifest(*, skill: str = "", group: str = "", all_operations: bool = False)
"sha256": full_registry["sha256"],
"skills": [_summarize_skill(item) for item in full_registry["skills"]],
}
agent_names = agent_operation_names()
featured_names = agent_operation_names(featured_only=True)
agent_schemas = [
operation
for operation in operation_schemas
if str(operation.get("name") or "") in agent_names
]
return {
"ok": True,
"service": "OfferU CLI",
Expand Down Expand Up @@ -809,10 +841,18 @@ def _manifest(*, skill: str = "", group: str = "", all_operations: bool = False)
"raw_api_capability": False,
"side_effect_labels": sorted({effect for op in operation_schemas for effect in op.get("side_effects", [])}),
},
# operation_count is retained for backwards compatibility. New callers
# should distinguish the governed Registry from the model-facing Tool
# surface instead of treating every Operation as an Agent tool.
"operation_count": len(operation_schemas),
"operation_registry_count": len(operation_schemas),
"agent_tool_count": len(agent_names),
"featured_tool_count": len(featured_names),
"internal_operation_count": len(operation_schemas) - len(agent_names),
"returned_count": len(operations),
"selector": selector,
"groups": _groups(operation_schemas),
"agent_groups": _groups(agent_schemas),
"operations": operations,
"skill_registry": skills,
# Host × Skill support matrix (full/limited/unsupported) so an external
Expand Down
43 changes: 11 additions & 32 deletions backend/app/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,13 @@
)
from app.services.privacy_hygiene import (
get_privacy_hygiene_status,
get_synthetic_email_test_data_status,
purge_synthetic_email_test_data,
scrub_legacy_email_notification_bodies,
)
from app.services.job_ingest import JobIngestItem, import_job_batch
from app.services.job_sources.protocol import JobSearchQuery
from app.services.job_sources.router import job_source_router
from app.services.application_actions import preview_application_action
from app.services.application_actions import list_application_action_connectors, preview_application_action
from app.services.scraper_operations import finalize_scraper_batch, start_scraper_batch
from app.services.harness_operations import (
delete_harness_conversation,
Expand Down Expand Up @@ -92,7 +91,6 @@
get_hosted_executor_session,
get_career_task,
get_career_task_result,
get_agent_provider_health,
get_agent_connections,
get_local_agent_capability_matrix,
get_local_agent_capability_report,
Expand Down Expand Up @@ -217,7 +215,6 @@
duplicate_resume_template,
extract_interview_questions,
generate_html_resume,
generate_legacy_cover_letter,
generate_legacy_interview_answer,
import_jobs_to_application_table,
import_latest_extension_batch_to_application_table,
Expand Down Expand Up @@ -889,11 +886,6 @@ class ResumeTemplateDuplicateInput(_StrictOperationInput):
new_name: str = Field(min_length=1, max_length=300)


class LegacyCoverLetterInput(_StrictOperationInput):
job_id: int = Field(gt=0)
resume_id: int = Field(gt=0)


class GenerateHtmlResumeInput(_StrictOperationInput):
profile_id: int = Field(gt=0)
template_id: int = Field(gt=0)
Expand Down Expand Up @@ -2272,6 +2264,16 @@ async def _search_jobs_via_sources(
input_model=PreviewApplicationActionInput,
version="2026-09-21",
),
"list_application_action_connectors": Operation(
name="list_application_action_connectors",
fn=list_application_action_connectors,
description="列出已注册站外投递动作 Connector 的能力矩阵(greet/send_message/send_resume/exchange_contact)与可用性;第一阶段无写执行器,仅声明边界,任何真实写都须走受保护 Operation + Proposal/HITL。",
parameters={},
group="applications",
side_effects=("read",),
input_model=_StrictOperationInput,
version="2026-09-21",
),
"create_application_attempt": Operation(
name="create_application_attempt",
fn=create_application_attempt,
Expand Down Expand Up @@ -2565,14 +2567,6 @@ async def _search_jobs_via_sources(
input_model=DelegateCareerTaskInput,
version="2026-08-27",
),
"get_agent_provider_health": Operation(
name="get_agent_provider_health",
fn=get_agent_provider_health,
description="读取一个 Agent Runtime provider 的脱敏可用性、认证与阻塞状态。",
group="agent_runtime",
input_model=ProviderHealthInput,
version="2026-08-27",
),
"list_agent_provider_health": Operation(
name="list_agent_provider_health",
fn=list_agent_provider_health,
Expand Down Expand Up @@ -3114,13 +3108,6 @@ async def _search_jobs_via_sources(
group="governance",
version="2026-08-31",
),
"get_synthetic_email_test_data_status": Operation(
name="get_synthetic_email_test_data_status",
fn=get_synthetic_email_test_data_status,
description="读取严格限定的合成邮箱测试数据计数,不返回账号地址或邮件内容。",
group="governance",
version="2026-08-31",
),
"scrub_legacy_email_notification_bodies": Operation(
name="scrub_legacy_email_notification_bodies",
fn=scrub_legacy_email_notification_bodies,
Expand Down Expand Up @@ -3510,14 +3497,6 @@ async def _search_jobs_via_sources(
side_effects=("llm", "write"),
input_model=GenerateLegacyInterviewAnswerInput,
),
"generate_legacy_cover_letter": Operation(
name="generate_legacy_cover_letter",
fn=generate_legacy_cover_letter,
description="兼容旧投递接口生成求职信草稿。",
group="applications",
side_effects=("llm",),
input_model=LegacyCoverLetterInput,
),
"create_resume_template": Operation(
name="create_resume_template",
fn=create_resume_template,
Expand Down
2 changes: 1 addition & 1 deletion backend/app/routes/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ async def create_application(data: ApplicationCreate):

@router.post("/generate")
async def generate(data: GenerateRequest):
return await _execute_operation("generate_legacy_cover_letter", data.model_dump())
return await _execute_operation("generate_cover_letter", data.model_dump())


@router.get("/stats")
Expand Down
6 changes: 0 additions & 6 deletions backend/app/services/agent_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -1097,12 +1097,6 @@ async def delegate_career_task(
)


async def get_agent_provider_health(provider_id: str) -> dict:
from app.services.agent_provider_health import get_provider_health

return await get_provider_health(provider_id)


async def list_agent_provider_health() -> dict:
from app.services.agent_provider_health import list_provider_health

Expand Down
Loading
Loading