refactor: remove ws-only scene routing remnants
Keep the ws branch focused on websocket and Zhihu behavior by dropping staged scene-routing artifacts and restoring single-path skills dir semantics. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,455 +0,0 @@
|
|||||||
# Scene Skill Runtime Routing Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Add a first scene-routing slice that recognizes staged business scenes from natural language and dispatches them through browser-backed execution, with `fault-details-report` using direct browser execution and `95598-repair-city-dispatch` using agent-mediated browser execution.
|
|
||||||
|
|
||||||
**Architecture:** Introduce a small registry module that loads the first staged `scene.json` contracts plus runtime dispatch policy from the external `skill_staging` root. Route matched scenes through one of two paths: `direct_browser` scenes execute through compat orchestration without the model choosing tools, while `agent_browser` scenes stay in the existing agent flow but get scene-specific browser-first prompt injection. Both modes must still execute through the existing `BrowserScriptSkillTool` / browser backend path so the final business action uses browser-internal methods.
|
|
||||||
|
|
||||||
**Tech Stack:** Rust, serde/JSON metadata loading, existing compat orchestration/runtime/workflow layers, browser-backed skill tools, focused `cargo test` coverage.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Map
|
|
||||||
|
|
||||||
**Create:**
|
|
||||||
- `src/runtime/scene_registry.rs` — load staged scene metadata, attach runtime dispatch policy, expose matching helpers for the first slice.
|
|
||||||
- `tests/scene_registry_test.rs` — focused tests for registry loading, matching, and policy behavior.
|
|
||||||
|
|
||||||
**Modify:**
|
|
||||||
- `src/runtime/mod.rs` — export the new scene registry module/types used by runtime and compat layers.
|
|
||||||
- `src/compat/config_adapter.rs` — verify the moved external `skill_staging` root resolves to the staged `skills` child, and only change path resolution if a targeted regression proves it is insufficient.
|
|
||||||
- `src/runtime/engine.rs` — inject scene-specific browser-first contracts for `agent_browser` scenes and keep existing Zhihu prompt behavior intact.
|
|
||||||
- `src/compat/workflow_executor.rs` — extend route detection and direct execution support for `fault-details-report` using the browser-backed skill path.
|
|
||||||
- `src/compat/orchestration.rs` — let primary orchestration prefer direct execution for `direct_browser` scenes while leaving `agent_browser` scenes in the agent path.
|
|
||||||
- `src/compat/browser_script_skill_tool.rs` — expose the thinnest reusable browser-backed execution helper needed so direct scene execution can reuse the same `browser_script` semantics instead of drifting into a duplicate local path.
|
|
||||||
- `src/compat/runtime.rs` — ensure runtime sees the staged skills root and continues exposing browser-backed scene tools.
|
|
||||||
- `tests/compat_config_test.rs` — add path-resolution coverage for the staged external root.
|
|
||||||
- `tests/runtime_profile_test.rs` — add scene-specific instruction contract assertions.
|
|
||||||
- `tests/browser_script_skill_tool_test.rs` — add coverage for any new reusable direct-execution helper introduced in the browser-script layer.
|
|
||||||
- `tests/compat_runtime_test.rs` — add orchestration/direct-route coverage for the new scene behavior.
|
|
||||||
|
|
||||||
**Reference:**
|
|
||||||
- `docs/superpowers/specs/2026-04-06-scene-skill-runtime-routing-design.md`
|
|
||||||
- `D:\data\ideaSpace\rust\sgClaw\claw\claw\skills\skill_staging\scenes\fault-details-report\scene.json`
|
|
||||||
- `D:\data\ideaSpace\rust\sgClaw\claw\claw\skills\skill_staging\scenes\95598-repair-city-dispatch\scene.json`
|
|
||||||
- `D:\data\ideaSpace\rust\sgClaw\claw\claw\skills\skill_staging\skills\fault-details-report\SKILL.toml`
|
|
||||||
- `D:\data\ideaSpace\rust\sgClaw\claw\claw\skills\skill_staging\skills\95598-repair-city-dispatch\SKILL.toml`
|
|
||||||
|
|
||||||
### Task 1: Add Scene Registry And Matching
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `src/runtime/scene_registry.rs`
|
|
||||||
- Modify: `src/runtime/mod.rs`
|
|
||||||
- Test: `tests/scene_registry_test.rs`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write the failing registry tests**
|
|
||||||
|
|
||||||
Add tests that prove the first-slice registry can:
|
|
||||||
- load `fault-details-report` with `dispatch_mode = direct_browser`
|
|
||||||
- load `95598-repair-city-dispatch` with `dispatch_mode = agent_browser`
|
|
||||||
- match natural-language phrases like `导出故障明细` and `95598抢修市指监测`
|
|
||||||
- ignore missing/broken scene files without panicking
|
|
||||||
|
|
||||||
Example assertions to include:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
assert_eq!(entry.scene_id, "fault-details-report");
|
|
||||||
assert_eq!(entry.dispatch_mode, DispatchMode::DirectBrowser);
|
|
||||||
assert_eq!(entry.tool_name(), "fault-details-report.collect_fault_details");
|
|
||||||
assert_eq!(entry.expected_domain, "__scene_fault_details__");
|
|
||||||
```
|
|
||||||
|
|
||||||
```rust
|
|
||||||
assert_eq!(matched.scene_id, "95598-repair-city-dispatch");
|
|
||||||
assert_eq!(matched.dispatch_mode, DispatchMode::AgentBrowser);
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Run the new registry tests to verify they fail**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
```bash
|
|
||||||
cargo test scene_registry --test scene_registry_test -- --nocapture
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: FAIL because `src/runtime/scene_registry.rs` and the exported registry APIs do not exist yet.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Implement the minimal scene registry module**
|
|
||||||
|
|
||||||
Create `src/runtime/scene_registry.rs` with:
|
|
||||||
- a small deserialized scene metadata struct for `scene.json`
|
|
||||||
- a `DispatchMode` enum
|
|
||||||
- a single runtime registry-entry struct combining scene metadata plus runtime policy
|
|
||||||
- first-slice hardcoded runtime policy for the two initial scenes
|
|
||||||
- helper methods like:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub fn load_first_slice_scene_registry() -> Vec<SceneRegistryEntry>
|
|
||||||
pub fn match_scene_instruction(instruction: &str) -> Option<SceneRegistryEntry>
|
|
||||||
```
|
|
||||||
|
|
||||||
Use deterministic keyword/alias matching only. Do not add embeddings, fuzzy search, or generic scoring infrastructure beyond what the spec requires.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Export the registry from `src/runtime/mod.rs`**
|
|
||||||
|
|
||||||
Expose the new types/helpers needed by runtime and compat layers, for example:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
mod scene_registry;
|
|
||||||
|
|
||||||
pub use scene_registry::{
|
|
||||||
load_first_slice_scene_registry,
|
|
||||||
match_scene_instruction,
|
|
||||||
DispatchMode,
|
|
||||||
SceneRegistryEntry,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 5: Run the registry tests to verify they pass**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
```bash
|
|
||||||
cargo test scene_registry --test scene_registry_test -- --nocapture
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: PASS
|
|
||||||
|
|
||||||
- [ ] **Step 6: Commit Task 1**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add src/runtime/scene_registry.rs src/runtime/mod.rs tests/scene_registry_test.rs
|
|
||||||
git commit -m "feat: add staged scene registry matching"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task 2: Verify Staged Skills Root Resolution
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify if needed: `src/compat/config_adapter.rs:94`
|
|
||||||
- Modify if needed: `src/compat/runtime.rs:152`
|
|
||||||
- Test: `tests/compat_config_test.rs`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write the targeted staged-root path test**
|
|
||||||
|
|
||||||
Add a focused test that proves an external configured `skill_staging` root resolves to its `skills` child and preserves current nested-skills behavior.
|
|
||||||
|
|
||||||
Add a test shape like:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
let staged_root = root.join("external/skill_staging");
|
|
||||||
fs::create_dir_all(staged_root.join("skills")).unwrap();
|
|
||||||
fs::create_dir_all(staged_root.join("scenes")).unwrap();
|
|
||||||
|
|
||||||
let settings = DeepSeekSettings {
|
|
||||||
api_key: "key".to_string(),
|
|
||||||
base_url: "https://api.deepseek.com".to_string(),
|
|
||||||
model: "deepseek-chat".to_string(),
|
|
||||||
skills_dir: Some(staged_root.clone()),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(resolve_skills_dir(&root, &settings), staged_root.join("skills"));
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Run the focused config test and record the actual result**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
```bash
|
|
||||||
cargo test --test compat_config_test resolve_skills_dir_ -- --nocapture
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: either
|
|
||||||
- PASS immediately, proving current path resolution already supports the staged-root contract, or
|
|
||||||
- FAIL with a concrete staged-root regression that justifies a minimal config fix.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Only if the staged-root test fails, implement the narrowest config fix**
|
|
||||||
|
|
||||||
If the test fails, update `src/compat/config_adapter.rs` so configured external staged roots resolve to the staged skill package directory used by runtime skill loading. Keep the change narrow:
|
|
||||||
- preserve current behavior for normal `skills` roots
|
|
||||||
- add the smallest extra branch needed for the failing staged-root case
|
|
||||||
- do not create a broad path-discovery system
|
|
||||||
|
|
||||||
- [ ] **Step 4: Verify runtime alignment with the resolved staged skills root**
|
|
||||||
|
|
||||||
Confirm `src/compat/runtime.rs` still uses the resolved `skills_dir` as-is. If no runtime code change is needed after the test outcome, leave the file untouched and rely on test coverage.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Run the focused config tests to verify they pass**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
```bash
|
|
||||||
cargo test --test compat_config_test resolve_skills_dir_ -- --nocapture
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: PASS
|
|
||||||
|
|
||||||
- [ ] **Step 6: Commit Task 2**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add src/compat/config_adapter.rs src/compat/runtime.rs tests/compat_config_test.rs
|
|
||||||
git commit -m "test: verify staged scene skill root resolution"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task 3: Inject Agent-Browser Scene Contract For 95598
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `src/runtime/engine.rs:135`
|
|
||||||
- Test: `tests/runtime_profile_test.rs`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write the failing instruction-contract tests**
|
|
||||||
|
|
||||||
Add focused tests proving that when the instruction matches `95598-repair-city-dispatch`, `RuntimeEngine::build_instruction(...)` includes a scene-specific browser contract requiring the tool `95598-repair-city-dispatch.collect_repair_orders` first.
|
|
||||||
|
|
||||||
Example assertion pattern:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
let instruction = engine.build_instruction(
|
|
||||||
"请做95598抢修市指监测",
|
|
||||||
Some("https://example.invalid/dispatch"),
|
|
||||||
Some("95598抢修-市指"),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(instruction.contains("95598-repair-city-dispatch.collect_repair_orders"));
|
|
||||||
assert!(instruction.contains("browser workflow, not a text-only task"));
|
|
||||||
assert!(instruction.contains("generic browser probing only after"));
|
|
||||||
```
|
|
||||||
|
|
||||||
Also add a negative control showing unrelated tasks do not receive this scene contract.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Run the focused runtime-profile tests to verify they fail**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
```bash
|
|
||||||
cargo test --test runtime_profile_test 95598 -- --nocapture
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: FAIL because no scene-specific contract is injected yet.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Implement minimal scene-aware prompt injection**
|
|
||||||
|
|
||||||
Update `src/runtime/engine.rs` to:
|
|
||||||
- query the new scene matcher
|
|
||||||
- when the matched scene is `agent_browser`, append a scene execution contract section
|
|
||||||
- preserve existing Zhihu prompt sections unchanged
|
|
||||||
|
|
||||||
Keep the contract explicit and narrow, for example:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Scene execution contract:
|
|
||||||
- Matched scene: 95598-repair-city-dispatch
|
|
||||||
- Required tool: 95598-repair-city-dispatch.collect_repair_orders
|
|
||||||
- This is a browser workflow, not a text-only task.
|
|
||||||
- Business data must come from the matched browser-backed scene tool.
|
|
||||||
- Only use generic browser probing after the matched scene tool fails.
|
|
||||||
```
|
|
||||||
|
|
||||||
Do not add hard allowed-tool narrowing in this task; slice one only promises instruction-level enforcement.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Run the focused runtime-profile tests to verify they pass**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
```bash
|
|
||||||
cargo test --test runtime_profile_test 95598 -- --nocapture
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: PASS
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit Task 3**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add src/runtime/engine.rs tests/runtime_profile_test.rs
|
|
||||||
git commit -m "feat: inject 95598 scene browser contract"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task 4: Add Direct Browser Route For Fault Details
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `src/compat/workflow_executor.rs:58`
|
|
||||||
- Modify: `src/compat/orchestration.rs:9`
|
|
||||||
- Modify: `src/compat/browser_script_skill_tool.rs:101`
|
|
||||||
- Test: `tests/browser_script_skill_tool_test.rs`
|
|
||||||
- Test: `tests/compat_runtime_test.rs`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write the failing route-detection tests**
|
|
||||||
|
|
||||||
Add focused tests that prove:
|
|
||||||
- natural language like `导出故障明细` is detected as a direct scene route
|
|
||||||
- primary orchestration is selected for that scene
|
|
||||||
- missing scene metadata leaves unrelated routing unchanged
|
|
||||||
|
|
||||||
Target the existing routing seams with test shapes like:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
assert!(sgclaw::compat::orchestration::should_use_primary_orchestration(
|
|
||||||
"导出故障明细",
|
|
||||||
Some("https://example.invalid/fault"),
|
|
||||||
Some("故障明细"),
|
|
||||||
));
|
|
||||||
```
|
|
||||||
|
|
||||||
and a focused route assertion using the new route enum variant.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Run the focused route tests to verify they fail**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
```bash
|
|
||||||
cargo test --test compat_runtime_test fault_details -- --nocapture
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: FAIL because no direct scene route exists yet.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Write the failing browser-script helper tests**
|
|
||||||
|
|
||||||
Add focused tests in `tests/browser_script_skill_tool_test.rs` for the thinnest reusable helper needed by direct scene execution. The tests should prove that the helper:
|
|
||||||
- reads the packaged script from the skill root
|
|
||||||
- wraps args exactly like `BrowserScriptSkillTool`
|
|
||||||
- invokes browser `Eval`
|
|
||||||
- returns normalized serialized output
|
|
||||||
- fails clearly when required fields like `expected_domain` are missing
|
|
||||||
|
|
||||||
- [ ] **Step 4: Run the focused browser-script helper tests to verify they fail**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
```bash
|
|
||||||
cargo test --test browser_script_skill_tool_test -- --nocapture
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: FAIL because the reusable helper does not exist yet.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Implement the reusable browser-backed execution helper**
|
|
||||||
|
|
||||||
Update `src/compat/browser_script_skill_tool.rs` with the smallest reusable helper that direct scene execution can call while preserving the same `browser_script` semantics as normal skill execution. Keep it narrow:
|
|
||||||
- reuse the same script loading and wrapping rules
|
|
||||||
- require explicit `expected_domain`
|
|
||||||
- return normalized serialized output
|
|
||||||
- do not introduce a second browser-script execution model
|
|
||||||
|
|
||||||
- [ ] **Step 6: Implement the direct fault-details route on top of that helper**
|
|
||||||
|
|
||||||
Update `src/compat/workflow_executor.rs` to:
|
|
||||||
- introduce a new direct route variant for `fault-details-report`
|
|
||||||
- extend `detect_route(...)` to return it when the scene matcher says `direct_browser`
|
|
||||||
- build required args from scene runtime policy
|
|
||||||
- call the reusable browser-script execution helper
|
|
||||||
- return normalized serialized tool output
|
|
||||||
|
|
||||||
If required scene args cannot be derived safely, return a clear failure instead of guessing.
|
|
||||||
|
|
||||||
- [ ] **Step 7: Wire primary orchestration to prefer the new direct scene route**
|
|
||||||
|
|
||||||
Update `src/compat/orchestration.rs` so `should_use_primary_orchestration(...)` and the direct execution branch treat the new `fault-details-report` route like the existing direct Zhihu routes.
|
|
||||||
|
|
||||||
- [ ] **Step 8: Run the focused direct-route and helper tests to verify they pass**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
```bash
|
|
||||||
cargo test --test browser_script_skill_tool_test -- --nocapture && cargo test --test compat_runtime_test fault_details -- --nocapture
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: PASS
|
|
||||||
|
|
||||||
- [ ] **Step 9: Commit Task 4**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add src/compat/browser_script_skill_tool.rs src/compat/workflow_executor.rs src/compat/orchestration.rs tests/browser_script_skill_tool_test.rs tests/compat_runtime_test.rs
|
|
||||||
git commit -m "feat: add direct fault-details scene routing"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task 5: Verify Tool Exposure, Browser-Surface Fallback, And Mixed Routing Together
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify if needed: `src/compat/runtime.rs:142`
|
|
||||||
- Test: `tests/compat_runtime_test.rs`
|
|
||||||
- Test: `tests/runtime_profile_test.rs`
|
|
||||||
- Test: `tests/scene_registry_test.rs`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write the failing integration-shape tests**
|
|
||||||
|
|
||||||
Add focused assertions that prove the mixed-mode design works together:
|
|
||||||
- staged browser-backed tool names are exposed
|
|
||||||
- `fault-details-report` uses direct routing
|
|
||||||
- `95598-repair-city-dispatch` stays in the agent path but gets scene-specific browser-first instruction injection
|
|
||||||
- browser-surface-disabled turns do not gain scene browser contracts
|
|
||||||
- browser-surface-disabled turns do not trigger `direct_browser` scene execution
|
|
||||||
- missing scene metadata preserves unchanged runtime behavior for unrelated tasks
|
|
||||||
- unrelated Zhihu behavior still works the same way
|
|
||||||
|
|
||||||
Use existing test seams instead of broad integration scaffolding.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Run the focused mixed-routing tests to verify they fail**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
```bash
|
|
||||||
cargo test --test scene_registry_test -- --nocapture && cargo test --test compat_runtime_test scene_ -- --nocapture && cargo test --test runtime_profile_test scene_ -- --nocapture
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: FAIL until the mixed-routing assertions are implemented.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Make the minimum runtime adjustments needed**
|
|
||||||
|
|
||||||
Only if required by the tests, adjust `src/compat/runtime.rs` so the loaded staged skills from the resolved external root are visible in the same way as existing browser-backed skills. Keep the shape of `build_browser_script_skill_tools(...)` and runtime tool assembly intact.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Run the focused mixed-routing tests to verify they pass**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
```bash
|
|
||||||
cargo test --test scene_registry_test -- --nocapture && cargo test --test compat_runtime_test scene_ -- --nocapture && cargo test --test runtime_profile_test scene_ -- --nocapture
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: PASS
|
|
||||||
|
|
||||||
- [ ] **Step 5: Run the broader targeted verification sweep**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
```bash
|
|
||||||
cargo test --test browser_script_skill_tool_test -- --nocapture && cargo test --test scene_registry_test -- --nocapture && cargo test --test compat_config_test resolve_skills_dir_ -- --nocapture && cargo test --test runtime_profile_test -- --nocapture && cargo test --test compat_runtime_test fault_details -- --nocapture
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: PASS
|
|
||||||
|
|
||||||
- [ ] **Step 6: Commit Task 5**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add src/compat/runtime.rs tests/scene_registry_test.rs tests/compat_runtime_test.rs tests/runtime_profile_test.rs
|
|
||||||
git commit -m "feat: wire staged scene mixed routing"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task 6: Final Verification And Handoff
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Verify: `src/runtime/scene_registry.rs`
|
|
||||||
- Verify: `src/compat/config_adapter.rs`
|
|
||||||
- Verify: `src/runtime/engine.rs`
|
|
||||||
- Verify: `src/compat/workflow_executor.rs`
|
|
||||||
- Verify: `src/compat/orchestration.rs`
|
|
||||||
- Verify: `tests/scene_registry_test.rs`
|
|
||||||
- Verify: `tests/compat_config_test.rs`
|
|
||||||
- Verify: `tests/runtime_profile_test.rs`
|
|
||||||
- Verify: `tests/compat_runtime_test.rs`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Run the full focused verification set**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
```bash
|
|
||||||
cargo test --test scene_registry_test -- --nocapture && cargo test --test compat_config_test -- --nocapture && cargo test --test runtime_profile_test -- --nocapture && cargo test --test compat_runtime_test -- --nocapture
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: PASS
|
|
||||||
|
|
||||||
- [ ] **Step 2: If any test fails, fix only the minimal root cause and re-run the same command**
|
|
||||||
|
|
||||||
Do not broaden scope. Keep fixes limited to scene registry, path resolution, prompt injection, or direct routing.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Review the resulting diff against the spec**
|
|
||||||
|
|
||||||
Manually verify:
|
|
||||||
- `fault-details-report` is direct-browser
|
|
||||||
- `95598-repair-city-dispatch` is agent-browser
|
|
||||||
- both still use browser-backed execution semantics
|
|
||||||
- no broad Zhihu refactor slipped in
|
|
||||||
- the new scene-routing abstraction stays registry-driven
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit the final verification pass**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add src/runtime/scene_registry.rs src/runtime/mod.rs src/compat/config_adapter.rs src/runtime/engine.rs src/compat/workflow_executor.rs src/compat/orchestration.rs src/compat/runtime.rs tests/scene_registry_test.rs tests/compat_config_test.rs tests/runtime_profile_test.rs tests/compat_runtime_test.rs
|
|
||||||
git commit -m "test: verify scene skill runtime routing"
|
|
||||||
```
|
|
||||||
@@ -1,291 +0,0 @@
|
|||||||
# Scene Skill Runtime Routing Design
|
|
||||||
|
|
||||||
**Goal:** Add a minimal, extensible scene-routing layer so staged business scenes can be triggered from natural language while still executing through the existing browser-backed skill path.
|
|
||||||
|
|
||||||
**Architecture:** Introduce a registry-driven scene contract loader that reads staged `scene.json` metadata, matches user instructions to a scene, and chooses one of two dispatch modes: direct browser execution or agent-mediated browser execution. Both modes must reuse the same browser-backed skill tool path so scene skills continue to execute through browser-internal methods rather than text-only responses or local fake execution.
|
|
||||||
|
|
||||||
**Tech Stack:** Rust, serde/JSON scene metadata loading, existing `BrowserScriptSkillTool`, existing compat runtime / runtime engine / workflow executor layers, focused Rust unit tests.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Problem Statement
|
|
||||||
|
|
||||||
The codebase already supports two useful but separate ideas:
|
|
||||||
|
|
||||||
1. **Zhihu special-case runtime routing**
|
|
||||||
- `src/compat/workflow_executor.rs` detects a narrow set of Zhihu tasks and can execute them directly without relying on the model to choose tools.
|
|
||||||
- This is stable, but not extensible for a growing set of business scenes.
|
|
||||||
|
|
||||||
2. **Browser-backed skills**
|
|
||||||
- `src/compat/runtime.rs` loads skills and exposes `browser_script` tools through `BrowserScriptSkillTool`.
|
|
||||||
- `src/compat/browser_script_skill_tool.rs` executes those tools by calling the browser backend with `Action::Eval`, so actual execution already happens through browser-internal methods.
|
|
||||||
- This is extensible, but tool choice currently depends too heavily on generic agent behavior.
|
|
||||||
|
|
||||||
The staged business scenes under `D:\data\ideaSpace\rust\sgClaw\claw\claw\skills\skill_staging` already provide most of the metadata needed to bridge these two ideas. We need a first integration slice that uses scene metadata to improve routing without turning every scene into a hardcoded Zhihu-style exception.
|
|
||||||
|
|
||||||
## Design Goals
|
|
||||||
|
|
||||||
- Support natural-language triggering for staged scenes.
|
|
||||||
- Preserve the current browser-backed execution contract: both scene modes must end in browser-internal execution via the existing browser tool path.
|
|
||||||
- Support both dispatch styles discussed with the user:
|
|
||||||
- one scene that can execute without the model
|
|
||||||
- one scene that still uses the model for orchestration
|
|
||||||
- Keep the first slice small, covering only:
|
|
||||||
- `fault-details-report`
|
|
||||||
- `95598-repair-city-dispatch`
|
|
||||||
- Keep the design extensible so more scene skills can be added in the same directory later without more ad hoc routing branches.
|
|
||||||
- Avoid broad refactors or a new generic workflow platform in this slice.
|
|
||||||
|
|
||||||
## Non-Goals
|
|
||||||
|
|
||||||
- Do not build a scene editor, scene UI, or registry authoring workflow.
|
|
||||||
- Do not implement a full artifact post-processing platform for all report/monitor types.
|
|
||||||
- Do not convert every staged scene into a direct Rust executor.
|
|
||||||
- Do not replace the existing Zhihu-specific runtime path in this slice.
|
|
||||||
|
|
||||||
## Source of Truth and Paths
|
|
||||||
|
|
||||||
### Staged scene source
|
|
||||||
The new staged scene source for this work is:
|
|
||||||
|
|
||||||
- `D:\data\ideaSpace\rust\sgClaw\claw\claw\skills\skill_staging`
|
|
||||||
|
|
||||||
The runtime integration must read scene metadata from this location for the initial slice.
|
|
||||||
|
|
||||||
### Existing runtime integration points
|
|
||||||
- `src/compat/config_adapter.rs` — current skills-dir resolution logic
|
|
||||||
- `src/compat/runtime.rs` — current skill loading and browser-script tool exposure
|
|
||||||
- `src/runtime/engine.rs` — runtime instruction building and allowed-tool shaping
|
|
||||||
- `src/compat/workflow_executor.rs` — existing direct execution routing pattern
|
|
||||||
- `src/compat/browser_script_skill_tool.rs` — browser-backed execution path for `browser_script` tools
|
|
||||||
|
|
||||||
## Scene Contract Model
|
|
||||||
|
|
||||||
Introduce a small internal scene contract model derived from `scene.json` and paired runtime policy. The loader should extract only the fields needed for the first slice:
|
|
||||||
|
|
||||||
- `id`
|
|
||||||
- `name`
|
|
||||||
- `summary`
|
|
||||||
- `tags`
|
|
||||||
- `inputs`
|
|
||||||
- `outputs`
|
|
||||||
- `skill.package`
|
|
||||||
- `skill.tool`
|
|
||||||
- `skill.artifact_type`
|
|
||||||
|
|
||||||
Add a runtime-only dispatch policy associated with each enabled scene inside the same internal registry entry used at runtime:
|
|
||||||
|
|
||||||
- `dispatch_mode`
|
|
||||||
- `direct_browser`
|
|
||||||
- `agent_browser`
|
|
||||||
- `expected_domain`
|
|
||||||
- bare hostname required by the underlying browser-backed skill tool
|
|
||||||
- optional `aliases`
|
|
||||||
- additional deterministic keywords/phrases when `id/name/summary/tags` are not enough for first-slice matching
|
|
||||||
- optional `default_args`
|
|
||||||
- runtime-supplied tool arguments when a scene needs fixed/default values for first execution
|
|
||||||
|
|
||||||
This runtime policy may be hardcoded in Rust for the first slice, but it must be represented through one consistent scene-routing abstraction so future scenes can join the same path without rewriting the whole design. The abstraction should be a single registry entry type that combines scene metadata with runtime dispatch policy, rather than a metadata loader plus a separate ad hoc match table.
|
|
||||||
|
|
||||||
## Dispatch Modes
|
|
||||||
|
|
||||||
### 1. `direct_browser`
|
|
||||||
This mode is for scenes whose collection flow is deterministic enough to bypass the model once the scene is recognized.
|
|
||||||
|
|
||||||
**Initial scene:** `fault-details-report`
|
|
||||||
|
|
||||||
**Behavior:**
|
|
||||||
- Detect scene from natural language.
|
|
||||||
- Resolve the corresponding browser-backed skill tool.
|
|
||||||
- Execute it directly through the existing browser-backed skill path.
|
|
||||||
- Return the collected artifact result without delegating tool choice to the model.
|
|
||||||
|
|
||||||
**Important constraint:**
|
|
||||||
This is not a local fake implementation. Even in direct mode, the actual collection must still go through the existing browser-backed execution path, meaning it ultimately uses browser-internal methods through the browser backend.
|
|
||||||
|
|
||||||
### 2. `agent_browser`
|
|
||||||
This mode is for scenes that still benefit from agent orchestration, explanation, or downstream reasoning, but whose business data must still come from browser-backed execution.
|
|
||||||
|
|
||||||
**Initial scene:** `95598-repair-city-dispatch`
|
|
||||||
|
|
||||||
**Behavior:**
|
|
||||||
- Detect scene from natural language.
|
|
||||||
- Inject a strong scene execution contract into the runtime instruction.
|
|
||||||
- Treat calling the matching browser-backed skill tool first as a policy requirement for the scene.
|
|
||||||
- In slice one, enforce that policy through scene-specific instruction injection rather than a hard runtime gate.
|
|
||||||
- Allow generic browser probing only as a fallback after the scene tool fails.
|
|
||||||
- Keep final explanation/summarization in the agent path, but never let the model invent business data.
|
|
||||||
|
|
||||||
## Matching Strategy
|
|
||||||
|
|
||||||
Implement a minimal matcher that scores user instructions against enabled scenes using:
|
|
||||||
|
|
||||||
- scene `id`
|
|
||||||
- scene `name`
|
|
||||||
- scene `summary`
|
|
||||||
- scene `tags`
|
|
||||||
- optional runtime aliases for the first slice
|
|
||||||
|
|
||||||
The matcher should be intentionally simple and deterministic in this slice. Avoid semantic embedding or fuzzy retrieval infrastructure.
|
|
||||||
|
|
||||||
Expected first-slice matches:
|
|
||||||
|
|
||||||
- `fault-details-report`
|
|
||||||
- phrases like `故障明细`, `故障明细报表`, `导出故障明细`
|
|
||||||
- `95598-repair-city-dispatch`
|
|
||||||
- phrases like `95598抢修市指`, `市指抢修监测`, `95598抢修队列`
|
|
||||||
|
|
||||||
If no scene matches, runtime behavior must remain unchanged.
|
|
||||||
|
|
||||||
## Runtime Loading Design
|
|
||||||
|
|
||||||
### Scene registry loading
|
|
||||||
Add a small loader that reads enabled scenes from the staged scene directory. For the first slice, it is acceptable to read the concrete scene files directly instead of implementing a full generic registry parser, as long as the resulting module boundary is registry-oriented rather than one-off.
|
|
||||||
|
|
||||||
The loader should:
|
|
||||||
- resolve the staged scene root
|
|
||||||
- read the two initial `scene.json` files
|
|
||||||
- deserialize them into a small internal scene metadata struct
|
|
||||||
- pair them with dispatch policy in the same in-memory registry entry
|
|
||||||
- ignore malformed or missing scenes safely
|
|
||||||
- never fail runtime startup solely because one or both initial scene files are absent
|
|
||||||
|
|
||||||
### Skill loading alignment
|
|
||||||
The corresponding skill packages must still be loaded into runtime skill exposure so the browser-backed tools are available to the runtime.
|
|
||||||
|
|
||||||
For this slice, the staged scene source and staged skill packages should be treated as coming from the same external root:
|
|
||||||
- staged scenes under `.../skill_staging/scenes`
|
|
||||||
- staged skill packages under `.../skill_staging/skills`
|
|
||||||
|
|
||||||
The implementation must make that staged skill package root visible to runtime skill loading. If current `skills_dir` resolution cannot express that directly, the design should extend configuration/path resolution to support a staged external skills root explicitly rather than relying on implicit mirroring.
|
|
||||||
|
|
||||||
## Execution Design
|
|
||||||
|
|
||||||
### Direct browser path (`fault-details-report`)
|
|
||||||
Add a direct execution route that is scene-driven rather than Zhihu-specific.
|
|
||||||
|
|
||||||
High-level flow:
|
|
||||||
1. Runtime receives user instruction.
|
|
||||||
2. Scene matcher recognizes `fault-details-report`.
|
|
||||||
3. Runtime resolves the browser-backed tool name `fault-details-report.collect_fault_details`.
|
|
||||||
4. Runtime builds the required tool arguments, including:
|
|
||||||
- `expected_domain` from the matched scene's runtime policy
|
|
||||||
- any first-slice scene inputs that can be deterministically derived from the current request/context
|
|
||||||
- any fixed/default args declared in runtime policy
|
|
||||||
5. Runtime executes that skill through the existing browser-backed mechanism.
|
|
||||||
6. Runtime returns normalized tool output as the direct route result.
|
|
||||||
|
|
||||||
Input/argument rules for the first slice:
|
|
||||||
- Direct execution is only allowed when all required tool arguments are available.
|
|
||||||
- `expected_domain` must always come from runtime scene policy, not from model inference.
|
|
||||||
- If a required scene/tool input cannot be derived from the user request or current browser context, the direct route must fail clearly instead of fabricating values.
|
|
||||||
- The first slice may keep direct-mode argument mapping intentionally narrow; unsupported requests should fall back safely rather than guessing.
|
|
||||||
|
|
||||||
Return-shape rule for the first slice:
|
|
||||||
- The direct route should return normalized serialized tool output (for example, the tool payload string or normalized JSON text), not a model-authored prose summary. This keeps direct mode deterministic and makes the browser-backed result explicit.
|
|
||||||
|
|
||||||
Implementation note:
|
|
||||||
The cleanest first slice is to add a small scene direct-execution helper in the compat runtime/workflow area that invokes the already-loaded browser-backed skill tool abstraction rather than duplicating browser request logic.
|
|
||||||
|
|
||||||
### Agent browser path (`95598-repair-city-dispatch`)
|
|
||||||
This path stays inside the agent flow.
|
|
||||||
|
|
||||||
High-level flow:
|
|
||||||
1. Runtime receives user instruction.
|
|
||||||
2. Scene matcher recognizes `95598-repair-city-dispatch`.
|
|
||||||
3. `RuntimeEngine::build_instruction` injects a scene execution contract containing:
|
|
||||||
- the matched scene name
|
|
||||||
- the required tool name `95598-repair-city-dispatch.collect_repair_orders`
|
|
||||||
- explicit requirement that this is a browser workflow, not a text-only task
|
|
||||||
- explicit requirement that business data must come from the browser-backed scene tool
|
|
||||||
- fallback rules for generic browser probing only after tool failure
|
|
||||||
4. Agent runs and chooses the required tool.
|
|
||||||
5. Tool executes through the existing browser-backed skill path.
|
|
||||||
6. Agent may summarize the result, but cannot fabricate data.
|
|
||||||
|
|
||||||
Enforcement note for the first slice:
|
|
||||||
- The `agent_browser` guarantee is primarily an instruction-contract guarantee in slice one.
|
|
||||||
- If allowed-tool shaping can narrow the exposed tool set for a matched scene without destabilizing existing behavior, that is a valid enhancement, but it is not required for the first slice.
|
|
||||||
- The minimum guaranteed behavior for slice one is strong scene-specific prompt injection plus preservation of the rule that the model must not invent collected business data.
|
|
||||||
|
|
||||||
## Browser Execution Contract
|
|
||||||
|
|
||||||
This requirement is non-negotiable for both dispatch modes:
|
|
||||||
|
|
||||||
- scene skills must execute like the Zhihu flow in the sense that the final business action is performed through browser-internal methods
|
|
||||||
- scene skills must not devolve into text-only pseudo execution
|
|
||||||
- direct mode and agent mode both reuse the existing browser-backed skill execution path
|
|
||||||
|
|
||||||
Concretely, the final path for scene skill execution should remain compatible with:
|
|
||||||
- `BrowserScriptSkillTool`
|
|
||||||
- browser backend invocation
|
|
||||||
- browser-side `Eval` / browser action execution semantics
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
- **Scene metadata missing or invalid:** skip that scene and continue with normal runtime behavior.
|
|
||||||
- **Scene matched but skill/tool unavailable:** do not crash; log enough context for diagnosis and fall back safely.
|
|
||||||
- **Browser surface unavailable:** disable scene browser routing for that turn and fall back to current non-scene behavior.
|
|
||||||
- **Tool execution fails in `agent_browser` mode:** allow existing fallback prompt behavior to continue, but preserve the rule that the model cannot invent collected data.
|
|
||||||
- **Tool execution fails in `direct_browser` mode:** return a concise execution failure instead of pretending collection succeeded.
|
|
||||||
|
|
||||||
## Extensibility Rules
|
|
||||||
|
|
||||||
This slice should be built so future scene additions only need:
|
|
||||||
- a new scene metadata file under the staged scene path
|
|
||||||
- a matching skill package/tool
|
|
||||||
- a dispatch-mode declaration/policy
|
|
||||||
- optional aliases if the natural-language names are not sufficiently explicit
|
|
||||||
|
|
||||||
Avoid these anti-patterns:
|
|
||||||
- per-scene `if user said X then do Y` branches scattered across runtime files
|
|
||||||
- duplicating browser execution code for each scene
|
|
||||||
- binding future scenes to Zhihu-specific assumptions
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
### Scene registry tests
|
|
||||||
- load valid metadata for `fault-details-report`
|
|
||||||
- load valid metadata for `95598-repair-city-dispatch`
|
|
||||||
- ignore broken/missing scene files safely
|
|
||||||
|
|
||||||
### Matching tests
|
|
||||||
- instruction variants match `fault-details-report`
|
|
||||||
- instruction variants match `95598-repair-city-dispatch`
|
|
||||||
- unrelated instructions do not match
|
|
||||||
|
|
||||||
### Instruction-building tests
|
|
||||||
- `agent_browser` scene injects the required browser-first scene contract
|
|
||||||
- unmatched instructions do not gain scene-specific constraints
|
|
||||||
- Zhihu-specific instruction behavior remains unchanged
|
|
||||||
|
|
||||||
### Tool exposure tests
|
|
||||||
- staged skills from the moved path are loaded into runtime
|
|
||||||
- browser-backed tool names include:
|
|
||||||
- `fault-details-report.collect_fault_details`
|
|
||||||
- `95598-repair-city-dispatch.collect_repair_orders`
|
|
||||||
|
|
||||||
### Direct execution tests
|
|
||||||
- `fault-details-report` direct route invokes the browser-backed tool path rather than bypassing the browser layer
|
|
||||||
- direct route returns failure cleanly when tool execution fails
|
|
||||||
|
|
||||||
## Recommended First Implementation Slice
|
|
||||||
|
|
||||||
1. Add a tiny scene metadata loader and dispatch-mode policy module.
|
|
||||||
2. Extend runtime path resolution so the moved staged skills/scenes are visible.
|
|
||||||
3. Add deterministic scene matching for the two initial scenes.
|
|
||||||
4. Implement `agent_browser` instruction injection for `95598-repair-city-dispatch`.
|
|
||||||
5. Implement `direct_browser` execution for `fault-details-report` using the browser-backed skill path.
|
|
||||||
6. Add focused tests for matching, loading, tool exposure, and direct-vs-agent behavior.
|
|
||||||
|
|
||||||
## Open Design Constraint Captured From Discussion
|
|
||||||
|
|
||||||
The user explicitly requires the following combined behavior:
|
|
||||||
|
|
||||||
- support both kinds of scene execution in the same architecture
|
|
||||||
- one initial scene should be able to execute without the model
|
|
||||||
- one initial scene should execute through the model
|
|
||||||
- both must still use browser-internal execution methods like the Zhihu path
|
|
||||||
- the design must stay extensible because more staged skills may be added under the same path later
|
|
||||||
|
|
||||||
This design is built around those exact constraints.
|
|
||||||
@@ -176,7 +176,7 @@ pub fn run_submit_task<T: Transport + 'static>(
|
|||||||
|
|
||||||
let completion = match context.load_sgclaw_settings() {
|
let completion = match context.load_sgclaw_settings() {
|
||||||
Ok(Some(settings)) => {
|
Ok(Some(settings)) => {
|
||||||
let resolved_skills_dirs =
|
let resolved_skills_dir =
|
||||||
resolve_skills_dir_from_sgclaw_settings(&context.workspace_root, &settings);
|
resolve_skills_dir_from_sgclaw_settings(&context.workspace_root, &settings);
|
||||||
let _ = sink.send(&AgentMessage::LogEntry {
|
let _ = sink.send(&AgentMessage::LogEntry {
|
||||||
level: "info".to_string(),
|
level: "info".to_string(),
|
||||||
@@ -189,7 +189,7 @@ pub fn run_submit_task<T: Transport + 'static>(
|
|||||||
});
|
});
|
||||||
let _ = sink.send(&AgentMessage::LogEntry {
|
let _ = sink.send(&AgentMessage::LogEntry {
|
||||||
level: "info".to_string(),
|
level: "info".to_string(),
|
||||||
message: format!("skills dirs resolved to [{}]", resolved_skills_dirs.iter().map(|d| d.display().to_string()).collect::<Vec<_>>().join(", ")),
|
message: format!("skills dir resolved to {}", resolved_skills_dir.display()),
|
||||||
});
|
});
|
||||||
let _ = sink.send(&AgentMessage::LogEntry {
|
let _ = sink.send(&AgentMessage::LogEntry {
|
||||||
level: "info".to_string(),
|
level: "info".to_string(),
|
||||||
@@ -310,7 +310,7 @@ pub fn run_submit_task_with_browser_backend<T: Transport + 'static>(
|
|||||||
|
|
||||||
let completion = match context.load_sgclaw_settings() {
|
let completion = match context.load_sgclaw_settings() {
|
||||||
Ok(Some(settings)) => {
|
Ok(Some(settings)) => {
|
||||||
let resolved_skills_dirs =
|
let resolved_skills_dir =
|
||||||
resolve_skills_dir_from_sgclaw_settings(&context.workspace_root, &settings);
|
resolve_skills_dir_from_sgclaw_settings(&context.workspace_root, &settings);
|
||||||
let _ = sink.send(&AgentMessage::LogEntry {
|
let _ = sink.send(&AgentMessage::LogEntry {
|
||||||
level: "info".to_string(),
|
level: "info".to_string(),
|
||||||
@@ -323,7 +323,7 @@ pub fn run_submit_task_with_browser_backend<T: Transport + 'static>(
|
|||||||
});
|
});
|
||||||
let _ = sink.send(&AgentMessage::LogEntry {
|
let _ = sink.send(&AgentMessage::LogEntry {
|
||||||
level: "info".to_string(),
|
level: "info".to_string(),
|
||||||
message: format!("skills dirs resolved to [{}]", resolved_skills_dirs.iter().map(|d| d.display().to_string()).collect::<Vec<_>>().join(", ")),
|
message: format!("skills dir resolved to {}", resolved_skills_dir.display()),
|
||||||
});
|
});
|
||||||
let _ = sink.send(&AgentMessage::LogEntry {
|
let _ = sink.send(&AgentMessage::LogEntry {
|
||||||
level: "info".to_string(),
|
level: "info".to_string(),
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ use crate::runtime::RuntimeProfile;
|
|||||||
|
|
||||||
const SGCLAW_ZEROCLAW_WORKSPACE_DIR: &str = ".sgclaw-zeroclaw-workspace";
|
const SGCLAW_ZEROCLAW_WORKSPACE_DIR: &str = ".sgclaw-zeroclaw-workspace";
|
||||||
const SKILLS_DIR_NAME: &str = "skills";
|
const SKILLS_DIR_NAME: &str = "skills";
|
||||||
const STAGED_SKILLS_DIR_NAME: &str = "skill_staging";
|
|
||||||
|
|
||||||
pub fn build_zeroclaw_config(
|
pub fn build_zeroclaw_config(
|
||||||
workspace_root: &Path,
|
workspace_root: &Path,
|
||||||
@@ -88,41 +87,23 @@ pub fn zeroclaw_default_skills_dir(workspace_root: &Path) -> PathBuf {
|
|||||||
zeroclaw_workspace_dir(workspace_root).join(SKILLS_DIR_NAME)
|
zeroclaw_workspace_dir(workspace_root).join(SKILLS_DIR_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_skills_dir(workspace_root: &Path, settings: &DeepSeekSettings) -> Vec<PathBuf> {
|
pub fn resolve_skills_dir(workspace_root: &Path, settings: &DeepSeekSettings) -> PathBuf {
|
||||||
resolve_skills_dir_paths(workspace_root, &settings.skills_dir)
|
settings
|
||||||
|
.skills_dir
|
||||||
|
.as_deref()
|
||||||
|
.map(normalize_configured_skills_dir)
|
||||||
|
.unwrap_or_else(|| zeroclaw_default_skills_dir(workspace_root))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_skills_dir_from_sgclaw_settings(
|
pub fn resolve_skills_dir_from_sgclaw_settings(
|
||||||
workspace_root: &Path,
|
workspace_root: &Path,
|
||||||
settings: &SgClawSettings,
|
settings: &SgClawSettings,
|
||||||
) -> Vec<PathBuf> {
|
) -> PathBuf {
|
||||||
resolve_skills_dir_paths(workspace_root, &settings.skills_dir)
|
settings
|
||||||
}
|
.skills_dir
|
||||||
|
.as_deref()
|
||||||
pub fn resolve_scene_skills_dir_from_sgclaw_settings(
|
.map(normalize_configured_skills_dir)
|
||||||
workspace_root: &Path,
|
.unwrap_or_else(|| zeroclaw_default_skills_dir(workspace_root))
|
||||||
settings: &SgClawSettings,
|
|
||||||
) -> Vec<PathBuf> {
|
|
||||||
resolve_skills_dir_from_sgclaw_settings(workspace_root, settings)
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|dir| {
|
|
||||||
let scene_dir = resolve_scene_skills_dir_path(dir.clone());
|
|
||||||
if scene_dir != dir {
|
|
||||||
vec![dir, scene_dir]
|
|
||||||
} else {
|
|
||||||
vec![dir]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resolve_scene_skills_dir_path(skills_dir: PathBuf) -> PathBuf {
|
|
||||||
let staged_skills_dir = skills_dir.join(STAGED_SKILLS_DIR_NAME).join(SKILLS_DIR_NAME);
|
|
||||||
if staged_skills_dir.is_dir() {
|
|
||||||
staged_skills_dir
|
|
||||||
} else {
|
|
||||||
skills_dir
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_configured_skills_dir(configured_dir: &Path) -> PathBuf {
|
fn normalize_configured_skills_dir(configured_dir: &Path) -> PathBuf {
|
||||||
@@ -138,13 +119,3 @@ fn normalize_configured_skills_dir(configured_dir: &Path) -> PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_skills_dir_paths(workspace_root: &Path, configured_dirs: &[PathBuf]) -> Vec<PathBuf> {
|
|
||||||
if configured_dirs.is_empty() {
|
|
||||||
vec![zeroclaw_default_skills_dir(workspace_root)]
|
|
||||||
} else {
|
|
||||||
configured_dirs
|
|
||||||
.iter()
|
|
||||||
.map(|d| normalize_configured_skills_dir(d))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -146,12 +146,12 @@ pub async fn execute_task_with_provider(
|
|||||||
instruction: &str,
|
instruction: &str,
|
||||||
task_context: &CompatTaskContext,
|
task_context: &CompatTaskContext,
|
||||||
config: ZeroClawConfig,
|
config: ZeroClawConfig,
|
||||||
skills_dir: Vec<PathBuf>,
|
skills_dir: PathBuf,
|
||||||
settings: SgClawSettings,
|
settings: SgClawSettings,
|
||||||
) -> Result<String, PipeError> {
|
) -> Result<String, PipeError> {
|
||||||
let engine = RuntimeEngine::new(settings.runtime_profile);
|
let engine = RuntimeEngine::new(settings.runtime_profile);
|
||||||
let browser_surface_present = engine.browser_surface_enabled();
|
let browser_surface_present = engine.browser_surface_enabled();
|
||||||
let loaded_skills = engine.loaded_skills(&config, &skills_dir);
|
let loaded_skills = engine.loaded_skills(&config, std::slice::from_ref(&skills_dir));
|
||||||
let loaded_skill_versions = loaded_skills
|
let loaded_skill_versions = loaded_skills
|
||||||
.iter()
|
.iter()
|
||||||
.map(|skill| (skill.name.clone(), skill.version.clone()))
|
.map(|skill| (skill.name.clone(), skill.version.clone()))
|
||||||
@@ -198,7 +198,7 @@ pub async fn execute_task_with_provider(
|
|||||||
let mut agent = engine.build_agent(
|
let mut agent = engine.build_agent(
|
||||||
provider,
|
provider,
|
||||||
&config,
|
&config,
|
||||||
&skills_dir,
|
std::slice::from_ref(&skills_dir),
|
||||||
tools,
|
tools,
|
||||||
browser_surface_present,
|
browser_surface_present,
|
||||||
instruction,
|
instruction,
|
||||||
|
|||||||
@@ -5,15 +5,11 @@ use std::thread;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::Url;
|
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use zeroclaw::skills::load_skills_from_directory;
|
|
||||||
use zeroclaw::tools::Tool;
|
use zeroclaw::tools::Tool;
|
||||||
|
|
||||||
use crate::browser::{BrowserBackend, PipeBrowserBackend};
|
use crate::browser::{BrowserBackend, PipeBrowserBackend};
|
||||||
use crate::compat::artifact_open::{open_exported_xlsx, open_local_dashboard, PostExportOpen};
|
use crate::compat::artifact_open::{open_exported_xlsx, open_local_dashboard, PostExportOpen};
|
||||||
use crate::compat::browser_script_skill_tool::execute_browser_script_tool;
|
|
||||||
use crate::compat::config_adapter::resolve_scene_skills_dir_from_sgclaw_settings;
|
|
||||||
use crate::compat::openxml_office_tool::OpenXmlOfficeTool;
|
use crate::compat::openxml_office_tool::OpenXmlOfficeTool;
|
||||||
use crate::compat::runtime::CompatTaskContext;
|
use crate::compat::runtime::CompatTaskContext;
|
||||||
use crate::compat::screen_html_export_tool::ScreenHtmlExportTool;
|
use crate::compat::screen_html_export_tool::ScreenHtmlExportTool;
|
||||||
@@ -27,7 +23,6 @@ const ZHIHU_EDITOR_DOMAIN: &str = "zhuanlan.zhihu.com";
|
|||||||
const ZHIHU_HOT_URL: &str = "https://www.zhihu.com/hot";
|
const ZHIHU_HOT_URL: &str = "https://www.zhihu.com/hot";
|
||||||
const ZHIHU_CREATOR_URL: &str = "https://www.zhihu.com/creator";
|
const ZHIHU_CREATOR_URL: &str = "https://www.zhihu.com/creator";
|
||||||
const ZHIHU_EDITOR_URL: &str = "https://zhuanlan.zhihu.com/write";
|
const ZHIHU_EDITOR_URL: &str = "https://zhuanlan.zhihu.com/write";
|
||||||
const FAULT_DETAILS_SCENE_ID: &str = "fault-details-report";
|
|
||||||
const HOTLIST_READY_POLL_ATTEMPTS: usize = 10;
|
const HOTLIST_READY_POLL_ATTEMPTS: usize = 10;
|
||||||
const HOTLIST_READY_POLL_INTERVAL: Duration = Duration::from_millis(500);
|
const HOTLIST_READY_POLL_INTERVAL: Duration = Duration::from_millis(500);
|
||||||
const EDITOR_READY_POLL_ATTEMPTS: usize = 12;
|
const EDITOR_READY_POLL_ATTEMPTS: usize = 12;
|
||||||
@@ -39,7 +34,6 @@ const HOTLIST_TEXT_READY_PATTERN: &str =
|
|||||||
r"\d+(?:\.\d+)?\s*(?:万|亿|k|K|m|M)\s*热度";
|
r"\d+(?:\.\d+)?\s*(?:万|亿|k|K|m|M)\s*热度";
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum WorkflowRoute {
|
pub enum WorkflowRoute {
|
||||||
FaultDetailsReport,
|
|
||||||
ZhihuHotlistExportXlsx,
|
ZhihuHotlistExportXlsx,
|
||||||
ZhihuHotlistScreen,
|
ZhihuHotlistScreen,
|
||||||
ZhihuArticleEntry,
|
ZhihuArticleEntry,
|
||||||
@@ -66,13 +60,6 @@ pub fn detect_route(
|
|||||||
page_url: Option<&str>,
|
page_url: Option<&str>,
|
||||||
page_title: Option<&str>,
|
page_title: Option<&str>,
|
||||||
) -> Option<WorkflowRoute> {
|
) -> Option<WorkflowRoute> {
|
||||||
if let Some(scene) = crate::runtime::match_scene_instruction(instruction) {
|
|
||||||
if scene.id == FAULT_DETAILS_SCENE_ID
|
|
||||||
&& matches!(scene.dispatch_mode, crate::runtime::DispatchMode::DirectBrowser)
|
|
||||||
{
|
|
||||||
return Some(WorkflowRoute::FaultDetailsReport);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if crate::runtime::is_zhihu_hotlist_task(instruction, page_url, page_title) {
|
if crate::runtime::is_zhihu_hotlist_task(instruction, page_url, page_title) {
|
||||||
let normalized = instruction.to_ascii_lowercase();
|
let normalized = instruction.to_ascii_lowercase();
|
||||||
if normalized.contains("dashboard")
|
if normalized.contains("dashboard")
|
||||||
@@ -106,8 +93,7 @@ pub fn detect_route(
|
|||||||
pub fn prefers_direct_execution(route: &WorkflowRoute) -> bool {
|
pub fn prefers_direct_execution(route: &WorkflowRoute) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
route,
|
route,
|
||||||
WorkflowRoute::FaultDetailsReport
|
WorkflowRoute::ZhihuHotlistExportXlsx
|
||||||
| WorkflowRoute::ZhihuHotlistExportXlsx
|
|
||||||
| WorkflowRoute::ZhihuHotlistScreen
|
| WorkflowRoute::ZhihuHotlistScreen
|
||||||
| WorkflowRoute::ZhihuArticleEntry
|
| WorkflowRoute::ZhihuArticleEntry
|
||||||
| WorkflowRoute::ZhihuArticleDraft
|
| WorkflowRoute::ZhihuArticleDraft
|
||||||
@@ -133,8 +119,7 @@ pub fn should_fallback_after_summary(summary: &str, route: &WorkflowRoute) -> bo
|
|||||||
looks_like_denial
|
looks_like_denial
|
||||||
|| matches!(
|
|| matches!(
|
||||||
route,
|
route,
|
||||||
WorkflowRoute::FaultDetailsReport
|
WorkflowRoute::ZhihuHotlistExportXlsx
|
||||||
| WorkflowRoute::ZhihuHotlistExportXlsx
|
|
||||||
| WorkflowRoute::ZhihuHotlistScreen
|
| WorkflowRoute::ZhihuHotlistScreen
|
||||||
| WorkflowRoute::ZhihuArticleEntry
|
| WorkflowRoute::ZhihuArticleEntry
|
||||||
| WorkflowRoute::ZhihuArticleDraft
|
| WorkflowRoute::ZhihuArticleDraft
|
||||||
@@ -153,13 +138,6 @@ pub fn execute_route_with_browser_backend(
|
|||||||
settings: &SgClawSettings,
|
settings: &SgClawSettings,
|
||||||
) -> Result<String, PipeError> {
|
) -> Result<String, PipeError> {
|
||||||
match route {
|
match route {
|
||||||
WorkflowRoute::FaultDetailsReport => execute_fault_details_route(
|
|
||||||
browser_backend.clone(),
|
|
||||||
instruction,
|
|
||||||
workspace_root,
|
|
||||||
settings,
|
|
||||||
task_context.page_url.as_deref(),
|
|
||||||
),
|
|
||||||
WorkflowRoute::ZhihuHotlistExportXlsx | WorkflowRoute::ZhihuHotlistScreen => {
|
WorkflowRoute::ZhihuHotlistExportXlsx | WorkflowRoute::ZhihuHotlistScreen => {
|
||||||
let top_n = extract_top_n(instruction);
|
let top_n = extract_top_n(instruction);
|
||||||
let items = collect_hotlist_items(transport, browser_backend.as_ref(), top_n, task_context)?;
|
let items = collect_hotlist_items(transport, browser_backend.as_ref(), top_n, task_context)?;
|
||||||
@@ -232,157 +210,6 @@ pub fn execute_route<T: Transport + 'static>(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_fault_details_route(
|
|
||||||
browser_backend: Arc<dyn BrowserBackend>,
|
|
||||||
instruction: &str,
|
|
||||||
workspace_root: &Path,
|
|
||||||
settings: &SgClawSettings,
|
|
||||||
page_url: Option<&str>,
|
|
||||||
) -> Result<String, PipeError> {
|
|
||||||
let scene = crate::runtime::match_scene_instruction(instruction).ok_or_else(|| {
|
|
||||||
PipeError::Protocol("故障明细直连路由失败:未找到场景元数据。".to_string())
|
|
||||||
})?;
|
|
||||||
if scene.id != FAULT_DETAILS_SCENE_ID {
|
|
||||||
return Err(PipeError::Protocol(format!(
|
|
||||||
"故障明细直连路由失败:场景不匹配,got {}",
|
|
||||||
scene.id
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let period = derive_fault_details_period(instruction).ok_or_else(|| {
|
|
||||||
PipeError::Protocol(
|
|
||||||
"故障明细直连路由失败:无法从当前指令安全推导必填参数 period,请明确提供例如“导出 2026-04 故障明细”。"
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let skills_dirs = resolve_scene_skills_dir_from_sgclaw_settings(workspace_root, settings);
|
|
||||||
let skill = skills_dirs
|
|
||||||
.iter()
|
|
||||||
.flat_map(|dir| load_skills_from_directory(dir, true))
|
|
||||||
.find(|skill| skill.name == scene.skill_package)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
PipeError::Protocol(format!(
|
|
||||||
"故障明细直连路由失败:未找到技能包 {} in [{}]",
|
|
||||||
scene.skill_package,
|
|
||||||
skills_dirs.iter().map(|d| d.display().to_string()).collect::<Vec<_>>().join(", ")
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
let skill_root = skill
|
|
||||||
.location
|
|
||||||
.as_deref()
|
|
||||||
.and_then(Path::parent)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
PipeError::Protocol(format!(
|
|
||||||
"故障明细直连路由失败:技能包 {} 缺少有效位置元数据",
|
|
||||||
scene.skill_package
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
let tool = skill
|
|
||||||
.tools
|
|
||||||
.iter()
|
|
||||||
.find(|tool| tool.name == scene.skill_tool)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
PipeError::Protocol(format!(
|
|
||||||
"故障明细直连路由失败:技能包 {} 缺少工具 {}",
|
|
||||||
scene.skill_package, scene.skill_tool
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
if tool.kind != "browser_script" {
|
|
||||||
return Err(PipeError::Protocol(format!(
|
|
||||||
"故障明细直连路由失败:工具 {} 必须是 browser_script,当前为 {}",
|
|
||||||
scene.skill_tool, tool.kind
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let expected_domain = fault_details_expected_domain(page_url, &scene.expected_domain)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
PipeError::Protocol(
|
|
||||||
"故障明细直连路由失败:无法从当前页面上下文解析可用域名。".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let runtime = tokio::runtime::Runtime::new()
|
|
||||||
.map_err(|err| PipeError::Protocol(format!("failed to create tokio runtime: {err}")))?;
|
|
||||||
let result = runtime
|
|
||||||
.block_on(execute_browser_script_tool(
|
|
||||||
tool,
|
|
||||||
skill_root,
|
|
||||||
browser_backend,
|
|
||||||
json!({
|
|
||||||
"expected_domain": expected_domain,
|
|
||||||
"period": period,
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
.map_err(|err| PipeError::Protocol(err.to_string()))?;
|
|
||||||
if !result.success {
|
|
||||||
return Err(PipeError::Protocol(
|
|
||||||
result
|
|
||||||
.error
|
|
||||||
.unwrap_or_else(|| "fault-details-report browser script failed".to_string()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result.output)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fault_details_expected_domain(page_url: Option<&str>, fallback: &str) -> Option<String> {
|
|
||||||
page_url
|
|
||||||
.and_then(host_from_url)
|
|
||||||
.or_else(|| host_from_url(fallback))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn host_from_url(raw: &str) -> Option<String> {
|
|
||||||
let trimmed = raw.trim();
|
|
||||||
if trimmed.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(url) = Url::parse(trimmed) {
|
|
||||||
return url.host_str().map(|host| host.to_ascii_lowercase());
|
|
||||||
}
|
|
||||||
|
|
||||||
let host = trimmed
|
|
||||||
.trim_start_matches("https://")
|
|
||||||
.trim_start_matches("http://")
|
|
||||||
.split(['/', '?', '#'])
|
|
||||||
.next()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.split(':')
|
|
||||||
.next()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.trim()
|
|
||||||
.to_ascii_lowercase();
|
|
||||||
|
|
||||||
(!host.is_empty()).then_some(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn derive_fault_details_period(instruction: &str) -> Option<String> {
|
|
||||||
let month_re = Regex::new(r"(20\d{2})[-/年](0?[1-9]|1[0-2])").expect("valid fault details month regex");
|
|
||||||
let derived = month_re.captures_iter(instruction).find_map(|capture| {
|
|
||||||
let matched = capture.get(0)?;
|
|
||||||
let before_is_digit = instruction[..matched.start()]
|
|
||||||
.chars()
|
|
||||||
.next_back()
|
|
||||||
.is_some_and(|ch| ch.is_ascii_digit());
|
|
||||||
let after_is_digit = instruction[matched.end()..]
|
|
||||||
.chars()
|
|
||||||
.next()
|
|
||||||
.is_some_and(|ch| ch.is_ascii_digit());
|
|
||||||
if before_is_digit || after_is_digit {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let year = capture.get(1).map(|m| m.as_str()).unwrap_or_default();
|
|
||||||
let month = capture
|
|
||||||
.get(2)
|
|
||||||
.and_then(|m| m.as_str().parse::<u32>().ok())
|
|
||||||
.unwrap_or(1);
|
|
||||||
Some(format!("{year}-{month:02}"))
|
|
||||||
});
|
|
||||||
derived
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_hotlist_items(
|
fn collect_hotlist_items(
|
||||||
transport: &dyn crate::agent::AgentEventSink,
|
transport: &dyn crate::agent::AgentEventSink,
|
||||||
browser_tool: &dyn BrowserBackend,
|
browser_tool: &dyn BrowserBackend,
|
||||||
@@ -1275,7 +1102,7 @@ mod tests {
|
|||||||
"test-key".to_string(),
|
"test-key".to_string(),
|
||||||
"http://127.0.0.1:9".to_string(),
|
"http://127.0.0.1:9".to_string(),
|
||||||
"deepseek-chat".to_string(),
|
"deepseek-chat".to_string(),
|
||||||
Vec::new(),
|
None,
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::de;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::runtime::RuntimeProfile;
|
use crate::runtime::RuntimeProfile;
|
||||||
@@ -106,7 +105,7 @@ pub struct DeepSeekSettings {
|
|||||||
pub api_key: String,
|
pub api_key: String,
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
pub model: String,
|
pub model: String,
|
||||||
pub skills_dir: Vec<PathBuf>,
|
pub skills_dir: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeepSeekSettings {
|
impl DeepSeekSettings {
|
||||||
@@ -125,7 +124,7 @@ pub struct SgClawSettings {
|
|||||||
pub provider_api_key: String,
|
pub provider_api_key: String,
|
||||||
pub provider_base_url: String,
|
pub provider_base_url: String,
|
||||||
pub provider_model: String,
|
pub provider_model: String,
|
||||||
pub skills_dir: Vec<PathBuf>,
|
pub skills_dir: Option<PathBuf>,
|
||||||
pub skills_prompt_mode: SkillsPromptMode,
|
pub skills_prompt_mode: SkillsPromptMode,
|
||||||
pub runtime_profile: RuntimeProfile,
|
pub runtime_profile: RuntimeProfile,
|
||||||
pub planner_mode: PlannerMode,
|
pub planner_mode: PlannerMode,
|
||||||
@@ -156,7 +155,7 @@ impl SgClawSettings {
|
|||||||
api_key: String,
|
api_key: String,
|
||||||
base_url: String,
|
base_url: String,
|
||||||
model: String,
|
model: String,
|
||||||
skills_dir: Vec<PathBuf>,
|
skills_dir: Option<PathBuf>,
|
||||||
) -> Result<Self, ConfigError> {
|
) -> Result<Self, ConfigError> {
|
||||||
Self::new(
|
Self::new(
|
||||||
api_key,
|
api_key,
|
||||||
@@ -199,7 +198,7 @@ impl SgClawSettings {
|
|||||||
api_key,
|
api_key,
|
||||||
base_url,
|
base_url,
|
||||||
model,
|
model,
|
||||||
Vec::new(),
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@@ -284,7 +283,7 @@ impl SgClawSettings {
|
|||||||
config.api_key,
|
config.api_key,
|
||||||
config.base_url,
|
config.base_url,
|
||||||
config.model,
|
config.model,
|
||||||
resolve_configured_skills_dirs(config.skills_dir, config_dir),
|
resolve_configured_skills_dir(config.skills_dir, config_dir),
|
||||||
skills_prompt_mode,
|
skills_prompt_mode,
|
||||||
runtime_profile,
|
runtime_profile,
|
||||||
planner_mode,
|
planner_mode,
|
||||||
@@ -302,7 +301,7 @@ impl SgClawSettings {
|
|||||||
api_key: String,
|
api_key: String,
|
||||||
base_url: String,
|
base_url: String,
|
||||||
model: String,
|
model: String,
|
||||||
skills_dir: Vec<PathBuf>,
|
skills_dir: Option<PathBuf>,
|
||||||
skills_prompt_mode: Option<SkillsPromptMode>,
|
skills_prompt_mode: Option<SkillsPromptMode>,
|
||||||
runtime_profile: Option<RuntimeProfile>,
|
runtime_profile: Option<RuntimeProfile>,
|
||||||
planner_mode: Option<PlannerMode>,
|
planner_mode: Option<PlannerMode>,
|
||||||
@@ -433,18 +432,11 @@ fn parse_office_backend(raw: &str) -> Result<OfficeBackend, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_configured_skills_dirs(raw: Vec<String>, config_dir: &Path) -> Vec<PathBuf> {
|
fn resolve_configured_skills_dir(raw: Option<String>, config_dir: &Path) -> Option<PathBuf> {
|
||||||
raw.into_iter()
|
raw.map(|value| value.trim().to_string())
|
||||||
.filter(|s| !s.trim().is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.map(|s| {
|
.map(PathBuf::from)
|
||||||
let path = PathBuf::from(s.trim());
|
.map(|path| if path.is_absolute() { path } else { config_dir.join(path) })
|
||||||
if path.is_absolute() {
|
|
||||||
path
|
|
||||||
} else {
|
|
||||||
config_dir.join(path)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_required_value(field: &'static str, raw: String) -> Result<String, ConfigError> {
|
fn normalize_required_value(field: &'static str, raw: String) -> Result<String, ConfigError> {
|
||||||
@@ -486,49 +478,6 @@ fn normalize_enum_token(raw: &str) -> String {
|
|||||||
.to_ascii_lowercase()
|
.to_ascii_lowercase()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_skills_dirs<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
|
||||||
where
|
|
||||||
D: de::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct StringOrVec;
|
|
||||||
|
|
||||||
impl<'de> de::Visitor<'de> for StringOrVec {
|
|
||||||
type Value = Vec<String>;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
formatter.write_str("a string or array of strings")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E: de::Error>(self, value: &str) -> Result<Vec<String>, E> {
|
|
||||||
if value.trim().is_empty() {
|
|
||||||
Ok(Vec::new())
|
|
||||||
} else {
|
|
||||||
Ok(vec![value.to_string()])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<String>, A::Error> {
|
|
||||||
let mut dirs = Vec::new();
|
|
||||||
while let Some(value) = seq.next_element::<String>()? {
|
|
||||||
if !value.trim().is_empty() {
|
|
||||||
dirs.push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(dirs)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_none<E: de::Error>(self) -> Result<Vec<String>, E> {
|
|
||||||
Ok(Vec::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_unit<E: de::Error>(self) -> Result<Vec<String>, E> {
|
|
||||||
Ok(Vec::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_any(StringOrVec)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct RawSgClawSettings {
|
struct RawSgClawSettings {
|
||||||
#[serde(rename = "apiKey", default)]
|
#[serde(rename = "apiKey", default)]
|
||||||
@@ -537,8 +486,8 @@ struct RawSgClawSettings {
|
|||||||
base_url: String,
|
base_url: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
model: String,
|
model: String,
|
||||||
#[serde(rename = "skillsDir", alias = "skills_dir", default, deserialize_with = "deserialize_skills_dirs")]
|
#[serde(rename = "skillsDir", alias = "skills_dir", default)]
|
||||||
skills_dir: Vec<String>,
|
skills_dir: Option<String>,
|
||||||
#[serde(rename = "skillsPromptMode", alias = "skills_prompt_mode", default)]
|
#[serde(rename = "skillsPromptMode", alias = "skills_prompt_mode", default)]
|
||||||
skills_prompt_mode: Option<String>,
|
skills_prompt_mode: Option<String>,
|
||||||
#[serde(rename = "runtimeProfile", alias = "runtime_profile", default)]
|
#[serde(rename = "runtimeProfile", alias = "runtime_profile", default)]
|
||||||
|
|||||||
@@ -12,9 +12,8 @@ use zeroclaw::tools::{self, ReadSkillTool};
|
|||||||
use zeroclaw::SecurityPolicy;
|
use zeroclaw::SecurityPolicy;
|
||||||
|
|
||||||
use crate::compat::memory_adapter::build_memory;
|
use crate::compat::memory_adapter::build_memory;
|
||||||
use crate::compat::config_adapter::resolve_scene_skills_dir_path;
|
|
||||||
use crate::pipe::PipeError;
|
use crate::pipe::PipeError;
|
||||||
use crate::runtime::{match_scene_instruction, DispatchMode, RuntimeProfile, ToolPolicy};
|
use crate::runtime::{RuntimeProfile, ToolPolicy};
|
||||||
|
|
||||||
const BROWSER_ACTION_TOOL_NAME: &str = "browser_action";
|
const BROWSER_ACTION_TOOL_NAME: &str = "browser_action";
|
||||||
const SUPERRPA_BROWSER_TOOL_NAME: &str = "superrpa_browser";
|
const SUPERRPA_BROWSER_TOOL_NAME: &str = "superrpa_browser";
|
||||||
@@ -26,7 +25,6 @@ const ZHIHU_HOTLIST_EXECUTION_PROMPT: &str = "Zhihu hotlist execution contract:\
|
|||||||
const OFFICE_EXPORT_COMPLETION_PROMPT: &str = "Export completion contract:\n- This task requires a real Excel export.\n- After the Zhihu rows are available, you must call openxml_office before finishing.\n- Never fabricate, simulate, or invent substitute hotlist data when a live collection/export task fails.\n- If live collection fails, report the failure concisely instead of producing fake rows.\n- Do not stop after describing how you will parse or export the data.\n- Do not repeat the same sentence or section in your final answer.\n- Your final answer must include the generated local .xlsx path.";
|
const OFFICE_EXPORT_COMPLETION_PROMPT: &str = "Export completion contract:\n- This task requires a real Excel export.\n- After the Zhihu rows are available, you must call openxml_office before finishing.\n- Never fabricate, simulate, or invent substitute hotlist data when a live collection/export task fails.\n- If live collection fails, report the failure concisely instead of producing fake rows.\n- Do not stop after describing how you will parse or export the data.\n- Do not repeat the same sentence or section in your final answer.\n- Your final answer must include the generated local .xlsx path.";
|
||||||
const SCREEN_EXPORT_COMPLETION_PROMPT: &str = "Presentation completion contract:\n- This task requires a real dashboard artifact.\n- After the Zhihu rows are available, you must call screen_html_export before finishing.\n- Do not stop after describing how you will render or present the data.\n- Do not repeat the same sentence or section in your final answer.\n- Your final answer must include the local .html path and the presentation object.";
|
const SCREEN_EXPORT_COMPLETION_PROMPT: &str = "Presentation completion contract:\n- This task requires a real dashboard artifact.\n- After the Zhihu rows are available, you must call screen_html_export before finishing.\n- Do not stop after describing how you will render or present the data.\n- Do not repeat the same sentence or section in your final answer.\n- Your final answer must include the local .html path and the presentation object.";
|
||||||
const ZHIHU_WRITE_PUBLISH_PROMPT: &str = "Zhihu article publish contract:\n- This task may publish a Zhihu article.\n- You must not click publish without explicit human confirmation in the current session.\n- If the user asked to publish but no explicit confirmation phrase is present yet, ask for confirmation concisely and stop after the confirmation request.\n- Do not keep exploring tools after you have determined that publish confirmation is missing.\n- If the user only asked to write or draft, stay in draft mode and do not treat it as publish mode.\n- Do not repeat the same sentence or section in your final answer.";
|
const ZHIHU_WRITE_PUBLISH_PROMPT: &str = "Zhihu article publish contract:\n- This task may publish a Zhihu article.\n- You must not click publish without explicit human confirmation in the current session.\n- If the user asked to publish but no explicit confirmation phrase is present yet, ask for confirmation concisely and stop after the confirmation request.\n- Do not keep exploring tools after you have determined that publish confirmation is missing.\n- If the user only asked to write or draft, stay in draft mode and do not treat it as publish mode.\n- Do not repeat the same sentence or section in your final answer.";
|
||||||
const REPAIR_CITY_DISPATCH_EXECUTION_PROMPT: &str = "95598 repair city dispatch execution contract:\n- Treat this as a browser workflow, not a text-only task.\n- You must call `95598-repair-city-dispatch.collect_repair_orders` first when the tool is available.\n- Use generic browser probing only after the scene-specific collection tool fails or is unavailable.\n- Collect the live repair order queue before summarizing or reporting status.";
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct RuntimeEngine {
|
pub struct RuntimeEngine {
|
||||||
@@ -153,9 +151,6 @@ impl RuntimeEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut sections = vec![BROWSER_TOOL_CONTRACT_PROMPT.to_string()];
|
let mut sections = vec![BROWSER_TOOL_CONTRACT_PROMPT.to_string()];
|
||||||
if let Some(scene_contract) = build_scene_execution_contract(trimmed_instruction) {
|
|
||||||
sections.push(scene_contract);
|
|
||||||
}
|
|
||||||
if is_zhihu_hotlist_task(trimmed_instruction, page_url, page_title) {
|
if is_zhihu_hotlist_task(trimmed_instruction, page_url, page_title) {
|
||||||
sections.push(ZHIHU_HOTLIST_EXECUTION_PROMPT.to_string());
|
sections.push(ZHIHU_HOTLIST_EXECUTION_PROMPT.to_string());
|
||||||
}
|
}
|
||||||
@@ -276,17 +271,6 @@ fn task_needs_local_file_read(instruction: &str) -> bool {
|
|||||||
normalized.contains("/home/") || normalized.contains("./") || normalized.contains("../")
|
normalized.contains("/home/") || normalized.contains("./") || normalized.contains("../")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_scene_execution_contract(instruction: &str) -> Option<String> {
|
|
||||||
let scene = match_scene_instruction(instruction)?;
|
|
||||||
if scene.id == "95598-repair-city-dispatch"
|
|
||||||
&& matches!(scene.dispatch_mode, DispatchMode::AgentBrowser)
|
|
||||||
{
|
|
||||||
Some(REPAIR_CITY_DISPATCH_EXECUTION_PROMPT.to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_zhihu_hotlist_task(
|
pub fn is_zhihu_hotlist_task(
|
||||||
instruction: &str,
|
instruction: &str,
|
||||||
page_url: Option<&str>,
|
page_url: Option<&str>,
|
||||||
@@ -402,14 +386,6 @@ fn load_runtime_skills(config: &ZeroClawConfig, skills_dirs: &[PathBuf]) -> Vec<
|
|||||||
dir,
|
dir,
|
||||||
config.skills.allow_scripts,
|
config.skills.allow_scripts,
|
||||||
));
|
));
|
||||||
|
|
||||||
let scene_skills_dir = resolve_scene_skills_dir_path(dir.clone());
|
|
||||||
if scene_skills_dir != *dir {
|
|
||||||
skills.extend(zeroclaw::skills::load_skills_from_directory(
|
|
||||||
&scene_skills_dir,
|
|
||||||
config.skills.allow_scripts,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
skills
|
skills
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
mod engine;
|
mod engine;
|
||||||
mod profile;
|
mod profile;
|
||||||
mod scene_registry;
|
|
||||||
mod tool_policy;
|
mod tool_policy;
|
||||||
|
|
||||||
pub use engine::{
|
pub use engine::{
|
||||||
is_zhihu_hotlist_task, is_zhihu_write_task, task_requests_zhihu_article_publish, RuntimeEngine,
|
is_zhihu_hotlist_task, is_zhihu_write_task, task_requests_zhihu_article_publish, RuntimeEngine,
|
||||||
};
|
};
|
||||||
pub use profile::RuntimeProfile;
|
pub use profile::RuntimeProfile;
|
||||||
pub use scene_registry::{
|
|
||||||
load_first_slice_scene_registry, load_scene_registry_from_root, match_scene_instruction,
|
|
||||||
match_scene_instruction_in_registry, DispatchMode, SceneRegistryEntry,
|
|
||||||
};
|
|
||||||
pub use tool_policy::ToolPolicy;
|
pub use tool_policy::ToolPolicy;
|
||||||
|
|||||||
@@ -1,242 +0,0 @@
|
|||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_json::{Map, Value};
|
|
||||||
|
|
||||||
const STAGED_SCENE_ROOT: &str = "D:/data/ideaSpace/rust/sgClaw/claw/claw/skills/skill_staging";
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum DispatchMode {
|
|
||||||
DirectBrowser,
|
|
||||||
AgentBrowser,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct SceneRegistryEntry {
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub summary: String,
|
|
||||||
pub tags: Vec<String>,
|
|
||||||
pub inputs: Vec<String>,
|
|
||||||
pub outputs: Vec<String>,
|
|
||||||
pub skill_package: String,
|
|
||||||
pub skill_tool: String,
|
|
||||||
pub skill_artifact_type: String,
|
|
||||||
pub dispatch_mode: DispatchMode,
|
|
||||||
pub expected_domain: String,
|
|
||||||
pub aliases: Vec<String>,
|
|
||||||
pub default_args: Map<String, Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct SceneMetadata {
|
|
||||||
id: String,
|
|
||||||
name: String,
|
|
||||||
summary: String,
|
|
||||||
#[serde(default)]
|
|
||||||
tags: Vec<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
inputs: Vec<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
outputs: Vec<String>,
|
|
||||||
skill: SceneSkillMetadata,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct SceneSkillMetadata {
|
|
||||||
package: String,
|
|
||||||
tool: String,
|
|
||||||
artifact_type: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
struct SceneMatchScore {
|
|
||||||
matched_terms: usize,
|
|
||||||
longest_term: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
struct SceneMatchResult {
|
|
||||||
score: SceneMatchScore,
|
|
||||||
has_strong_phrase_hit: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct SceneRuntimePolicy {
|
|
||||||
scene_id: &'static str,
|
|
||||||
dispatch_mode: DispatchMode,
|
|
||||||
expected_domain: &'static str,
|
|
||||||
aliases: &'static [&'static str],
|
|
||||||
}
|
|
||||||
|
|
||||||
const FIRST_SLICE_POLICIES: [SceneRuntimePolicy; 2] = [
|
|
||||||
SceneRuntimePolicy {
|
|
||||||
scene_id: "fault-details-report",
|
|
||||||
dispatch_mode: DispatchMode::DirectBrowser,
|
|
||||||
expected_domain: "sgcc.example.invalid",
|
|
||||||
aliases: &["故障明细", "故障明细报表", "导出故障明细"],
|
|
||||||
},
|
|
||||||
SceneRuntimePolicy {
|
|
||||||
scene_id: "95598-repair-city-dispatch",
|
|
||||||
dispatch_mode: DispatchMode::AgentBrowser,
|
|
||||||
expected_domain: "95598.example.invalid",
|
|
||||||
aliases: &["95598抢修市指", "市指抢修监测", "95598抢修队列", "95598抢修市指监测"],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
pub fn load_first_slice_scene_registry() -> Vec<SceneRegistryEntry> {
|
|
||||||
load_scene_registry_from_root(Path::new(STAGED_SCENE_ROOT))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_scene_registry_from_root(root: &Path) -> Vec<SceneRegistryEntry> {
|
|
||||||
let mut registry = Vec::new();
|
|
||||||
for policy in FIRST_SLICE_POLICIES {
|
|
||||||
if let Some(entry) = load_scene_entry(root, &policy) {
|
|
||||||
registry.push(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
registry
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match_scene_instruction(instruction: &str) -> Option<SceneRegistryEntry> {
|
|
||||||
let registry = load_first_slice_scene_registry();
|
|
||||||
match_scene_instruction_in_registry(®istry, instruction)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match_scene_instruction_in_registry(
|
|
||||||
registry: &[SceneRegistryEntry],
|
|
||||||
instruction: &str,
|
|
||||||
) -> Option<SceneRegistryEntry> {
|
|
||||||
let normalized_instruction = normalize_for_match(instruction);
|
|
||||||
if normalized_instruction.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut best_match: Option<(SceneMatchResult, &SceneRegistryEntry)> = None;
|
|
||||||
let mut ambiguous = false;
|
|
||||||
let mut strong_phrase_hits = 0;
|
|
||||||
|
|
||||||
for entry in registry {
|
|
||||||
let Some(result) = score_scene_instruction(entry, &normalized_instruction) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if result.has_strong_phrase_hit {
|
|
||||||
strong_phrase_hits += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
match &best_match {
|
|
||||||
None => {
|
|
||||||
best_match = Some((result, entry));
|
|
||||||
ambiguous = false;
|
|
||||||
}
|
|
||||||
Some((current_result, _)) if result.score > current_result.score => {
|
|
||||||
best_match = Some((result, entry));
|
|
||||||
ambiguous = false;
|
|
||||||
}
|
|
||||||
Some((current_result, current_entry)) if result.score == current_result.score => {
|
|
||||||
if result.has_strong_phrase_hit && current_result.has_strong_phrase_hit {
|
|
||||||
if current_entry.id != entry.id {
|
|
||||||
ambiguous = true;
|
|
||||||
}
|
|
||||||
} else if result.has_strong_phrase_hit && !current_result.has_strong_phrase_hit {
|
|
||||||
best_match = Some((result, entry));
|
|
||||||
ambiguous = false;
|
|
||||||
} else if current_result.has_strong_phrase_hit && !result.has_strong_phrase_hit {
|
|
||||||
ambiguous = false;
|
|
||||||
} else if current_entry.id != entry.id {
|
|
||||||
ambiguous = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(_) => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ambiguous || strong_phrase_hits > 1 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
best_match.map(|(_, entry)| entry.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_scene_entry(root: &Path, policy: &SceneRuntimePolicy) -> Option<SceneRegistryEntry> {
|
|
||||||
let scene_path = scene_json_path(root, policy.scene_id);
|
|
||||||
let contents = fs::read_to_string(scene_path).ok()?;
|
|
||||||
let metadata: SceneMetadata = serde_json::from_str(&contents).ok()?;
|
|
||||||
if metadata.id != policy.scene_id {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(SceneRegistryEntry {
|
|
||||||
id: policy.scene_id.to_string(),
|
|
||||||
name: metadata.name,
|
|
||||||
summary: metadata.summary,
|
|
||||||
tags: metadata.tags,
|
|
||||||
inputs: metadata.inputs,
|
|
||||||
outputs: metadata.outputs,
|
|
||||||
skill_package: metadata.skill.package,
|
|
||||||
skill_tool: metadata.skill.tool,
|
|
||||||
skill_artifact_type: metadata.skill.artifact_type,
|
|
||||||
dispatch_mode: policy.dispatch_mode,
|
|
||||||
expected_domain: policy.expected_domain.to_string(),
|
|
||||||
aliases: policy.aliases.iter().map(|alias| (*alias).to_string()).collect(),
|
|
||||||
default_args: Map::new(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scene_json_path(root: &Path, scene_id: &str) -> PathBuf {
|
|
||||||
root.join("scenes").join(scene_id).join("scene.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn score_scene_instruction(
|
|
||||||
entry: &SceneRegistryEntry,
|
|
||||||
normalized_instruction: &str,
|
|
||||||
) -> Option<SceneMatchResult> {
|
|
||||||
let mut matched_terms = 0;
|
|
||||||
let mut longest_term = 0;
|
|
||||||
let mut has_strong_phrase_hit = false;
|
|
||||||
|
|
||||||
for term in candidate_match_terms(entry) {
|
|
||||||
let normalized_term = normalize_for_match(&term);
|
|
||||||
if normalized_term.len() < 2 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if normalized_instruction.contains(&normalized_term) {
|
|
||||||
matched_terms += 1;
|
|
||||||
longest_term = longest_term.max(normalized_term.len());
|
|
||||||
if normalized_term.len() >= 6 {
|
|
||||||
has_strong_phrase_hit = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if matched_terms == 0 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(SceneMatchResult {
|
|
||||||
score: SceneMatchScore {
|
|
||||||
matched_terms,
|
|
||||||
longest_term,
|
|
||||||
},
|
|
||||||
has_strong_phrase_hit,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn candidate_match_terms(entry: &SceneRegistryEntry) -> Vec<String> {
|
|
||||||
let mut terms = Vec::new();
|
|
||||||
terms.push(entry.id.clone());
|
|
||||||
terms.push(entry.name.clone());
|
|
||||||
terms.push(entry.summary.clone());
|
|
||||||
terms.extend(entry.tags.iter().cloned());
|
|
||||||
terms.extend(entry.aliases.iter().cloned());
|
|
||||||
terms
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalize_for_match(value: &str) -> String {
|
|
||||||
value
|
|
||||||
.chars()
|
|
||||||
.filter(|ch| !ch.is_whitespace() && *ch != '-' && *ch != '_' && *ch != ':')
|
|
||||||
.flat_map(|ch| ch.to_lowercase())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,8 @@ use std::sync::{Mutex, OnceLock};
|
|||||||
|
|
||||||
use sgclaw::compat::config_adapter::{
|
use sgclaw::compat::config_adapter::{
|
||||||
build_zeroclaw_config, build_zeroclaw_config_from_settings,
|
build_zeroclaw_config, build_zeroclaw_config_from_settings,
|
||||||
build_zeroclaw_config_from_sgclaw_settings, resolve_scene_skills_dir_path, resolve_skills_dir,
|
build_zeroclaw_config_from_sgclaw_settings, resolve_skills_dir, zeroclaw_default_skills_dir,
|
||||||
zeroclaw_default_skills_dir, zeroclaw_workspace_dir,
|
zeroclaw_workspace_dir,
|
||||||
};
|
};
|
||||||
use sgclaw::config::{
|
use sgclaw::config::{
|
||||||
BrowserBackend, DeepSeekSettings, OfficeBackend, PlannerMode, SgClawSettings, SkillsPromptMode,
|
BrowserBackend, DeepSeekSettings, OfficeBackend, PlannerMode, SgClawSettings, SkillsPromptMode,
|
||||||
@@ -47,7 +47,7 @@ fn zeroclaw_config_adapter_uses_deterministic_workspace_dir() {
|
|||||||
api_key: "key".to_string(),
|
api_key: "key".to_string(),
|
||||||
base_url: "https://proxy.example.com/v1".to_string(),
|
base_url: "https://proxy.example.com/v1".to_string(),
|
||||||
model: "deepseek-reasoner".to_string(),
|
model: "deepseek-reasoner".to_string(),
|
||||||
skills_dir: Vec::new(),
|
skills_dir: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let workspace_dir = zeroclaw_workspace_dir(Path::new("/var/lib/sgclaw"));
|
let workspace_dir = zeroclaw_workspace_dir(Path::new("/var/lib/sgclaw"));
|
||||||
@@ -66,7 +66,7 @@ fn zeroclaw_config_adapter_uses_deterministic_workspace_dir() {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_skills_dir(Path::new("/var/lib/sgclaw"), &settings),
|
resolve_skills_dir(Path::new("/var/lib/sgclaw"), &settings),
|
||||||
vec![zeroclaw_default_skills_dir(Path::new("/var/lib/sgclaw"))]
|
zeroclaw_default_skills_dir(Path::new("/var/lib/sgclaw"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ fn deepseek_settings_reload_from_browser_config_path_after_file_changes() {
|
|||||||
assert_eq!(first.api_key, "sk-first");
|
assert_eq!(first.api_key, "sk-first");
|
||||||
assert_eq!(first.base_url, "https://api.deepseek.com");
|
assert_eq!(first.base_url, "https://api.deepseek.com");
|
||||||
assert_eq!(first.model, "deepseek-chat");
|
assert_eq!(first.model, "deepseek-chat");
|
||||||
assert!(first.skills_dir.is_empty());
|
assert!(first.skills_dir.is_none());
|
||||||
|
|
||||||
fs::write(
|
fs::write(
|
||||||
&config_path,
|
&config_path,
|
||||||
@@ -111,23 +111,23 @@ fn deepseek_settings_reload_from_browser_config_path_after_file_changes() {
|
|||||||
assert_eq!(second.api_key, "sk-second");
|
assert_eq!(second.api_key, "sk-second");
|
||||||
assert_eq!(second.base_url, "https://proxy.example.com/v1");
|
assert_eq!(second.base_url, "https://proxy.example.com/v1");
|
||||||
assert_eq!(second.model, "deepseek-reasoner");
|
assert_eq!(second.model, "deepseek-reasoner");
|
||||||
assert_eq!(second.skills_dir, vec![root.join("skill_lib")]);
|
assert_eq!(second.skills_dir, Some(root.join("skill_lib")));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_skills_dir_prefers_nested_skills_subdirectory_for_configured_repo_root() {
|
fn ws_cleanup_resolves_single_configured_skills_dir() {
|
||||||
let root = std::env::temp_dir().join(format!("sgclaw-skills-{}", Uuid::new_v4()));
|
let root = std::env::temp_dir().join(format!("sgclaw-skills-{}", Uuid::new_v4()));
|
||||||
fs::create_dir_all(root.join("skill_lib/skills")).unwrap();
|
fs::create_dir_all(root.join("skill_lib/skills")).unwrap();
|
||||||
let settings = DeepSeekSettings {
|
let settings = DeepSeekSettings {
|
||||||
api_key: "key".to_string(),
|
api_key: "key".to_string(),
|
||||||
base_url: "https://api.deepseek.com".to_string(),
|
base_url: "https://api.deepseek.com".to_string(),
|
||||||
model: "deepseek-chat".to_string(),
|
model: "deepseek-chat".to_string(),
|
||||||
skills_dir: vec![root.join("skill_lib")],
|
skills_dir: Some(root.join("skill_lib")),
|
||||||
};
|
};
|
||||||
|
|
||||||
let resolved = resolve_skills_dir(&root, &settings);
|
let resolved = resolve_skills_dir(&root, &settings);
|
||||||
|
|
||||||
assert_eq!(resolved, vec![root.join("skill_lib/skills")]);
|
assert_eq!(resolved, root.join("skill_lib/skills"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -139,41 +139,12 @@ fn resolve_skills_dir_preserves_absolute_configured_skills_directory() {
|
|||||||
api_key: "key".to_string(),
|
api_key: "key".to_string(),
|
||||||
base_url: "https://api.deepseek.com".to_string(),
|
base_url: "https://api.deepseek.com".to_string(),
|
||||||
model: "deepseek-chat".to_string(),
|
model: "deepseek-chat".to_string(),
|
||||||
skills_dir: vec![external_skills.clone()],
|
skills_dir: Some(external_skills.clone()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let resolved = resolve_skills_dir(&root, &settings);
|
let resolved = resolve_skills_dir(&root, &settings);
|
||||||
|
|
||||||
assert_eq!(resolved, vec![external_skills]);
|
assert_eq!(resolved, external_skills);
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_skills_dir_uses_skills_child_for_external_staged_root() {
|
|
||||||
let root = std::env::temp_dir().join(format!("sgclaw-skills-{}", Uuid::new_v4()));
|
|
||||||
let staged_root = root.join("external/skill_staging");
|
|
||||||
fs::create_dir_all(staged_root.join("skills")).unwrap();
|
|
||||||
fs::create_dir_all(staged_root.join("scenes")).unwrap();
|
|
||||||
let settings = DeepSeekSettings {
|
|
||||||
api_key: "key".to_string(),
|
|
||||||
base_url: "https://api.deepseek.com".to_string(),
|
|
||||||
model: "deepseek-chat".to_string(),
|
|
||||||
skills_dir: vec![staged_root.clone()],
|
|
||||||
};
|
|
||||||
|
|
||||||
let resolved = resolve_skills_dir(&root, &settings);
|
|
||||||
|
|
||||||
assert_eq!(resolved, vec![staged_root.join("skills")]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_scene_skills_dir_path_prefers_staged_skills_child_under_project_root() {
|
|
||||||
let root = std::env::temp_dir().join(format!("sgclaw-scene-skills-{}", Uuid::new_v4()));
|
|
||||||
let top_level_skills = root.join("project/skills");
|
|
||||||
fs::create_dir_all(top_level_skills.join("skill_staging/skills")).unwrap();
|
|
||||||
|
|
||||||
let resolved = resolve_scene_skills_dir_path(top_level_skills.clone());
|
|
||||||
|
|
||||||
assert_eq!(resolved, top_level_skills.join("skill_staging/skills"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -182,7 +153,7 @@ fn sgclaw_settings_default_to_compact_skills_and_browser_attached_profile() {
|
|||||||
"sk-test".to_string(),
|
"sk-test".to_string(),
|
||||||
"https://api.deepseek.com".to_string(),
|
"https://api.deepseek.com".to_string(),
|
||||||
"deepseek-chat".to_string(),
|
"deepseek-chat".to_string(),
|
||||||
Vec::new(),
|
None,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -216,10 +187,29 @@ fn sgclaw_settings_load_new_runtime_fields_from_browser_config() {
|
|||||||
|
|
||||||
assert_eq!(settings.runtime_profile, RuntimeProfile::GeneralAssistant);
|
assert_eq!(settings.runtime_profile, RuntimeProfile::GeneralAssistant);
|
||||||
assert_eq!(settings.skills_prompt_mode, SkillsPromptMode::Full);
|
assert_eq!(settings.skills_prompt_mode, SkillsPromptMode::Full);
|
||||||
assert_eq!(settings.skills_dir, vec![root.join("skill_lib")]);
|
assert_eq!(settings.skills_dir, Some(root.join("skill_lib")));
|
||||||
assert_eq!(config.skills.prompt_injection_mode, SkillsPromptMode::Full);
|
assert_eq!(config.skills.prompt_injection_mode, SkillsPromptMode::Full);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ws_cleanup_rejects_array_style_skills_dir_config() {
|
||||||
|
let root = std::env::temp_dir().join(format!("sgclaw-config-{}", uuid::Uuid::new_v4()));
|
||||||
|
std::fs::create_dir_all(&root).unwrap();
|
||||||
|
let config_path = root.join("sgclaw_config.json");
|
||||||
|
std::fs::write(
|
||||||
|
&config_path,
|
||||||
|
r#"{
|
||||||
|
"apiKey": "sk-test",
|
||||||
|
"baseUrl": "https://api.deepseek.com",
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"skillsDir": ["skill_lib", "skill_staging"]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(sgclaw::config::SgClawSettings::load(Some(config_path.as_path())).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sgclaw_settings_load_browser_ws_url_from_browser_config() {
|
fn sgclaw_settings_load_browser_ws_url_from_browser_config() {
|
||||||
let root = std::env::temp_dir().join(format!("sgclaw-browser-ws-config-{}", Uuid::new_v4()));
|
let root = std::env::temp_dir().join(format!("sgclaw-browser-ws-config-{}", Uuid::new_v4()));
|
||||||
@@ -280,7 +270,7 @@ fn browser_attached_config_uses_low_temperature_for_deterministic_execution() {
|
|||||||
"sk-test".to_string(),
|
"sk-test".to_string(),
|
||||||
"https://api.deepseek.com".to_string(),
|
"https://api.deepseek.com".to_string(),
|
||||||
"deepseek-chat".to_string(),
|
"deepseek-chat".to_string(),
|
||||||
Vec::new(),
|
None,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ async fn compat_cron_adapter_creates_lists_and_runs_due_agent_jobs() {
|
|||||||
api_key: "key".to_string(),
|
api_key: "key".to_string(),
|
||||||
base_url: "https://api.deepseek.com".to_string(),
|
base_url: "https://api.deepseek.com".to_string(),
|
||||||
model: "deepseek-chat".to_string(),
|
model: "deepseek-chat".to_string(),
|
||||||
skills_dir: Vec::new(),
|
skills_dir: None,
|
||||||
};
|
};
|
||||||
let workspace_root = workspace_root("sgclaw-cron");
|
let workspace_root = workspace_root("sgclaw-cron");
|
||||||
let config = build_zeroclaw_config_from_settings(Path::new(&workspace_root), &settings);
|
let config = build_zeroclaw_config_from_settings(Path::new(&workspace_root), &settings);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ async fn compat_memory_adapter_uses_workspace_local_sqlite_backend() {
|
|||||||
api_key: "key".to_string(),
|
api_key: "key".to_string(),
|
||||||
base_url: "https://api.deepseek.com".to_string(),
|
base_url: "https://api.deepseek.com".to_string(),
|
||||||
model: "deepseek-chat".to_string(),
|
model: "deepseek-chat".to_string(),
|
||||||
skills_dir: Vec::new(),
|
skills_dir: None,
|
||||||
};
|
};
|
||||||
let workspace_root = workspace_root("sgclaw-memory");
|
let workspace_root = workspace_root("sgclaw-memory");
|
||||||
let config = build_zeroclaw_config_from_settings(Path::new(&workspace_root), &settings);
|
let config = build_zeroclaw_config_from_settings(Path::new(&workspace_root), &settings);
|
||||||
|
|||||||
@@ -118,17 +118,6 @@ fn real_skill_lib_root() -> PathBuf {
|
|||||||
.join("skill_lib")
|
.join("skill_lib")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn staged_skill_root() -> PathBuf {
|
|
||||||
PathBuf::from("D:/data/ideaSpace/rust/sgClaw/claw/claw/skills/skill_staging")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn project_skills_root() -> PathBuf {
|
|
||||||
staged_skill_root()
|
|
||||||
.parent()
|
|
||||||
.expect("staged skill root should have parent")
|
|
||||||
.to_path_buf()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn success_browser_response(seq: u64, data: Value) -> BrowserMessage {
|
fn success_browser_response(seq: u64, data: Value) -> BrowserMessage {
|
||||||
BrowserMessage::Response {
|
BrowserMessage::Response {
|
||||||
seq,
|
seq,
|
||||||
@@ -423,7 +412,7 @@ fn compat_runtime_includes_default_workspace_skills_in_provider_request() {
|
|||||||
api_key: "deepseek-test-key".to_string(),
|
api_key: "deepseek-test-key".to_string(),
|
||||||
base_url,
|
base_url,
|
||||||
model: "deepseek-chat".to_string(),
|
model: "deepseek-chat".to_string(),
|
||||||
skills_dir: Vec::new(),
|
skills_dir: None,
|
||||||
};
|
};
|
||||||
let transport = Arc::new(MockTransport::new(vec![]));
|
let transport = Arc::new(MockTransport::new(vec![]));
|
||||||
let browser_tool = BrowserPipeTool::new(
|
let browser_tool = BrowserPipeTool::new(
|
||||||
@@ -961,7 +950,7 @@ fn compat_runtime_includes_prior_turns_in_follow_up_provider_request() {
|
|||||||
api_key: "deepseek-test-key".to_string(),
|
api_key: "deepseek-test-key".to_string(),
|
||||||
base_url,
|
base_url,
|
||||||
model: "deepseek-chat".to_string(),
|
model: "deepseek-chat".to_string(),
|
||||||
skills_dir: Vec::new(),
|
skills_dir: None,
|
||||||
};
|
};
|
||||||
let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response {
|
let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response {
|
||||||
seq: 1,
|
seq: 1,
|
||||||
@@ -1060,7 +1049,7 @@ fn compat_runtime_does_not_forward_raw_aom_snapshot_back_to_provider() {
|
|||||||
api_key: "deepseek-test-key".to_string(),
|
api_key: "deepseek-test-key".to_string(),
|
||||||
base_url,
|
base_url,
|
||||||
model: "deepseek-chat".to_string(),
|
model: "deepseek-chat".to_string(),
|
||||||
skills_dir: Vec::new(),
|
skills_dir: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let large_snapshot_marker = "snapshot-marker ".repeat(2048);
|
let large_snapshot_marker = "snapshot-marker ".repeat(2048);
|
||||||
@@ -1122,7 +1111,7 @@ fn compat_runtime_injects_browser_contract_and_page_context_into_provider_reques
|
|||||||
api_key: "deepseek-test-key".to_string(),
|
api_key: "deepseek-test-key".to_string(),
|
||||||
base_url,
|
base_url,
|
||||||
model: "deepseek-chat".to_string(),
|
model: "deepseek-chat".to_string(),
|
||||||
skills_dir: Vec::new(),
|
skills_dir: None,
|
||||||
};
|
};
|
||||||
let transport = Arc::new(MockTransport::new(vec![]));
|
let transport = Arc::new(MockTransport::new(vec![]));
|
||||||
let browser_tool = BrowserPipeTool::new(
|
let browser_tool = BrowserPipeTool::new(
|
||||||
@@ -1198,7 +1187,7 @@ fn compat_runtime_can_complete_a_text_only_turn_without_browser_tool_calls() {
|
|||||||
"deepseek-test-key".to_string(),
|
"deepseek-test-key".to_string(),
|
||||||
base_url,
|
base_url,
|
||||||
"deepseek-chat".to_string(),
|
"deepseek-chat".to_string(),
|
||||||
Vec::new(),
|
None,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
settings.runtime_profile = RuntimeProfile::GeneralAssistant;
|
settings.runtime_profile = RuntimeProfile::GeneralAssistant;
|
||||||
@@ -1276,7 +1265,7 @@ fn compat_runtime_allows_read_skill_under_compact_mode_policy() {
|
|||||||
"deepseek-test-key".to_string(),
|
"deepseek-test-key".to_string(),
|
||||||
base_url,
|
base_url,
|
||||||
"deepseek-chat".to_string(),
|
"deepseek-chat".to_string(),
|
||||||
Vec::new(),
|
None,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let transport = Arc::new(MockTransport::new(vec![]));
|
let transport = Arc::new(MockTransport::new(vec![]));
|
||||||
@@ -1361,7 +1350,7 @@ top_n = "How many hotlist rows to extract."
|
|||||||
"deepseek-test-key".to_string(),
|
"deepseek-test-key".to_string(),
|
||||||
base_url,
|
base_url,
|
||||||
"deepseek-chat".to_string(),
|
"deepseek-chat".to_string(),
|
||||||
Vec::new(),
|
None,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||||||
@@ -1468,7 +1457,7 @@ return {
|
|||||||
"deepseek-test-key".to_string(),
|
"deepseek-test-key".to_string(),
|
||||||
base_url,
|
base_url,
|
||||||
"deepseek-chat".to_string(),
|
"deepseek-chat".to_string(),
|
||||||
Vec::new(),
|
None,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||||||
@@ -1539,7 +1528,7 @@ fn zhihu_hotlist_browser_skill_flow_does_not_expose_shell_or_glob_tools() {
|
|||||||
"deepseek-test-key".to_string(),
|
"deepseek-test-key".to_string(),
|
||||||
base_url,
|
base_url,
|
||||||
"deepseek-chat".to_string(),
|
"deepseek-chat".to_string(),
|
||||||
vec![real_skill_lib_root()],
|
Some(real_skill_lib_root()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||||||
@@ -1593,7 +1582,7 @@ fn compat_runtime_browser_attached_profile_keeps_file_read_available_for_local_p
|
|||||||
"deepseek-test-key".to_string(),
|
"deepseek-test-key".to_string(),
|
||||||
base_url,
|
base_url,
|
||||||
"deepseek-chat".to_string(),
|
"deepseek-chat".to_string(),
|
||||||
Vec::new(),
|
None,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||||||
@@ -1644,7 +1633,7 @@ fn browser_attached_export_flow_exposes_browser_and_office_tools_only() {
|
|||||||
"deepseek-test-key".to_string(),
|
"deepseek-test-key".to_string(),
|
||||||
base_url,
|
base_url,
|
||||||
"deepseek-chat".to_string(),
|
"deepseek-chat".to_string(),
|
||||||
vec![real_skill_lib_root()],
|
Some(real_skill_lib_root()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||||||
@@ -1699,7 +1688,7 @@ fn compat_runtime_allows_zhihu_hotlist_screen_export_tool_in_browser_profile() {
|
|||||||
"deepseek-test-key".to_string(),
|
"deepseek-test-key".to_string(),
|
||||||
base_url,
|
base_url,
|
||||||
"deepseek-chat".to_string(),
|
"deepseek-chat".to_string(),
|
||||||
vec![real_skill_lib_root()],
|
Some(real_skill_lib_root()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||||||
@@ -1781,7 +1770,7 @@ fn compat_runtime_logs_read_skill_usage_with_skill_name() {
|
|||||||
"deepseek-test-key".to_string(),
|
"deepseek-test-key".to_string(),
|
||||||
base_url,
|
base_url,
|
||||||
"deepseek-chat".to_string(),
|
"deepseek-chat".to_string(),
|
||||||
Vec::new(),
|
None,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let transport = Arc::new(MockTransport::new(vec![]));
|
let transport = Arc::new(MockTransport::new(vec![]));
|
||||||
@@ -1918,7 +1907,7 @@ fn browser_attached_excel_request_uses_execution_contract_not_skill_source_stuff
|
|||||||
"deepseek-test-key".to_string(),
|
"deepseek-test-key".to_string(),
|
||||||
base_url,
|
base_url,
|
||||||
"deepseek-chat".to_string(),
|
"deepseek-chat".to_string(),
|
||||||
vec![real_skill_lib_root()],
|
Some(real_skill_lib_root()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||||||
@@ -1970,7 +1959,7 @@ fn browser_attached_publish_request_injects_confirmation_contract() {
|
|||||||
"deepseek-test-key".to_string(),
|
"deepseek-test-key".to_string(),
|
||||||
base_url,
|
base_url,
|
||||||
"deepseek-chat".to_string(),
|
"deepseek-chat".to_string(),
|
||||||
vec![real_skill_lib_root()],
|
Some(real_skill_lib_root()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||||||
@@ -2601,313 +2590,6 @@ fn browser_submit_path_prefers_zeroclaw_process_message_orchestrator_for_zhihu_p
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn handle_browser_message_exposes_project_skills_and_staged_scene_skills_together() {
|
|
||||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
|
||||||
|
|
||||||
let response = json!({
|
|
||||||
"choices": [{
|
|
||||||
"message": {
|
|
||||||
"content": "已同时看到顶层与场景技能"
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
|
||||||
|
|
||||||
let workspace_root = temp_workspace_root();
|
|
||||||
let config_path = write_deepseek_config_with_skills_dir(
|
|
||||||
&workspace_root,
|
|
||||||
"deepseek-test-key",
|
|
||||||
&base_url,
|
|
||||||
"deepseek-chat",
|
|
||||||
Some(project_skills_root().to_str().unwrap()),
|
|
||||||
);
|
|
||||||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
|
||||||
|
|
||||||
let transport = Arc::new(MockTransport::new(vec![]));
|
|
||||||
let browser_tool = BrowserPipeTool::new(
|
|
||||||
transport.clone(),
|
|
||||||
zhihu_test_policy(),
|
|
||||||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
|
||||||
)
|
|
||||||
.with_response_timeout(Duration::from_secs(1));
|
|
||||||
|
|
||||||
handle_browser_message_with_context(
|
|
||||||
transport.as_ref(),
|
|
||||||
&browser_tool,
|
|
||||||
&runtime_context,
|
|
||||||
BrowserMessage::SubmitTask {
|
|
||||||
instruction: "告诉我当前有哪些知乎和95598相关 skill".to_string(),
|
|
||||||
conversation_id: String::new(),
|
|
||||||
messages: vec![],
|
|
||||||
page_url: "https://example.net/".to_string(),
|
|
||||||
page_title: "Example Domain".to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
server_handle.join().unwrap();
|
|
||||||
|
|
||||||
let sent = transport.sent_messages();
|
|
||||||
let request_bodies = requests.lock().unwrap().clone();
|
|
||||||
let first_request = request_bodies[0].to_string();
|
|
||||||
let loaded_skills_message = sent
|
|
||||||
.iter()
|
|
||||||
.find_map(|message| match message {
|
|
||||||
AgentMessage::LogEntry { level, message }
|
|
||||||
if level == "info" && message.starts_with("loaded skills: ") =>
|
|
||||||
{
|
|
||||||
Some(message.clone())
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.expect("expected loaded skills log entry");
|
|
||||||
|
|
||||||
assert!(sent.iter().any(|message| {
|
|
||||||
matches!(
|
|
||||||
message,
|
|
||||||
AgentMessage::TaskComplete { success, summary }
|
|
||||||
if *success && summary == "已同时看到顶层与场景技能"
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
assert!(loaded_skills_message.contains("zhihu-hotlist@0.1.0"));
|
|
||||||
assert!(loaded_skills_message.contains("zhihu-write@0.1.0"));
|
|
||||||
assert!(loaded_skills_message.contains("fault-details-report@0.1.0"));
|
|
||||||
assert!(loaded_skills_message.contains("95598-repair-city-dispatch@0.1.0"));
|
|
||||||
assert!(first_request.contains("zhihu-hotlist"));
|
|
||||||
assert!(first_request.contains("zhihu-write"));
|
|
||||||
assert!(first_request.contains("fault-details-report"));
|
|
||||||
assert!(first_request.contains("95598-repair-city-dispatch"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fault_details_route_finds_staged_scene_skill_under_project_skills_root() {
|
|
||||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
|
||||||
|
|
||||||
let workspace_root = temp_workspace_root();
|
|
||||||
let config_path = write_deepseek_config_with_skills_dir(
|
|
||||||
&workspace_root,
|
|
||||||
"deepseek-test-key",
|
|
||||||
"http://127.0.0.1:9",
|
|
||||||
"deepseek-chat",
|
|
||||||
Some(project_skills_root().to_str().unwrap()),
|
|
||||||
);
|
|
||||||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
|
||||||
|
|
||||||
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
|
|
||||||
1,
|
|
||||||
json!({
|
|
||||||
"text": {
|
|
||||||
"sheet_name": "故障明细",
|
|
||||||
"rows": [["2026-04", "已完成"]]
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)]));
|
|
||||||
let browser_tool = BrowserPipeTool::new(
|
|
||||||
transport.clone(),
|
|
||||||
policy_for_domains(&["example.invalid"]),
|
|
||||||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
|
||||||
)
|
|
||||||
.with_response_timeout(Duration::from_secs(1));
|
|
||||||
|
|
||||||
handle_browser_message_with_context(
|
|
||||||
transport.as_ref(),
|
|
||||||
&browser_tool,
|
|
||||||
&runtime_context,
|
|
||||||
BrowserMessage::SubmitTask {
|
|
||||||
instruction: "导出 2026-04 故障明细".to_string(),
|
|
||||||
conversation_id: String::new(),
|
|
||||||
messages: vec![],
|
|
||||||
page_url: "https://example.invalid/workbench".to_string(),
|
|
||||||
page_title: "业务台账".to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let sent = transport.sent_messages();
|
|
||||||
assert!(sent.iter().any(|message| {
|
|
||||||
matches!(
|
|
||||||
message,
|
|
||||||
AgentMessage::TaskComplete { success, summary }
|
|
||||||
if *success && summary.contains("sheet_name") && summary.contains("故障明细")
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
assert!(sent.iter().any(|message| {
|
|
||||||
matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn handle_browser_message_exposes_staged_95598_scene_skills_and_contract_on_agent_path() {
|
|
||||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
|
||||||
|
|
||||||
let response = json!({
|
|
||||||
"choices": [{
|
|
||||||
"message": {
|
|
||||||
"content": "已按95598场景进入通用代理路径"
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
|
||||||
|
|
||||||
let workspace_root = temp_workspace_root();
|
|
||||||
let config_path = write_deepseek_config_with_skills_dir(
|
|
||||||
&workspace_root,
|
|
||||||
"deepseek-test-key",
|
|
||||||
&base_url,
|
|
||||||
"deepseek-chat",
|
|
||||||
Some(staged_skill_root().to_str().unwrap()),
|
|
||||||
);
|
|
||||||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
|
||||||
|
|
||||||
let transport = Arc::new(MockTransport::new(vec![]));
|
|
||||||
let browser_tool = BrowserPipeTool::new(
|
|
||||||
transport.clone(),
|
|
||||||
policy_for_domains(&["95598.example.invalid"]),
|
|
||||||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
|
||||||
)
|
|
||||||
.with_response_timeout(Duration::from_secs(1));
|
|
||||||
|
|
||||||
handle_browser_message_with_context(
|
|
||||||
transport.as_ref(),
|
|
||||||
&browser_tool,
|
|
||||||
&runtime_context,
|
|
||||||
BrowserMessage::SubmitTask {
|
|
||||||
instruction: "请处理95598-repair-city-dispatch场景,查看抢修市指派单并汇总当前队列"
|
|
||||||
.to_string(),
|
|
||||||
conversation_id: String::new(),
|
|
||||||
messages: vec![],
|
|
||||||
page_url: "https://95598.example.invalid/dispatch".to_string(),
|
|
||||||
page_title: "95598抢修市指监测".to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
server_handle.join().unwrap();
|
|
||||||
|
|
||||||
let sent = transport.sent_messages();
|
|
||||||
let request_bodies = requests.lock().unwrap().clone();
|
|
||||||
let first_request = request_bodies[0].to_string();
|
|
||||||
let loaded_skills_message = sent
|
|
||||||
.iter()
|
|
||||||
.find_map(|message| match message {
|
|
||||||
AgentMessage::LogEntry { level, message }
|
|
||||||
if level == "info" && message.starts_with("loaded skills: ") =>
|
|
||||||
{
|
|
||||||
Some(message.clone())
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.expect("expected loaded skills log entry");
|
|
||||||
|
|
||||||
assert!(sent.iter().any(|message| {
|
|
||||||
matches!(
|
|
||||||
message,
|
|
||||||
AgentMessage::LogEntry { level, message }
|
|
||||||
if level == "mode" && message == "compat_llm_primary"
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
assert!(sent.iter().any(|message| {
|
|
||||||
matches!(
|
|
||||||
message,
|
|
||||||
AgentMessage::TaskComplete { success, summary }
|
|
||||||
if *success && summary == "已按95598场景进入通用代理路径"
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
assert!(loaded_skills_message.contains("fault-details-report@0.1.0"));
|
|
||||||
assert!(loaded_skills_message.contains("95598-repair-city-dispatch@0.1.0"));
|
|
||||||
assert_eq!(request_bodies.len(), 1);
|
|
||||||
assert!(first_request.contains("95598-repair-city-dispatch.collect_repair_orders"));
|
|
||||||
assert!(first_request.contains("Current page URL: https://95598.example.invalid/dispatch"));
|
|
||||||
assert!(first_request.contains("Current page title: 95598抢修市指监测"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn browser_surface_disabled_fault_details_turn_uses_general_assistant_provider_path() {
|
|
||||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
|
||||||
|
|
||||||
let response = json!({
|
|
||||||
"choices": [{
|
|
||||||
"message": {
|
|
||||||
"content": "已走通用助手路径"
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
|
||||||
|
|
||||||
let workspace_root = temp_workspace_root();
|
|
||||||
let config_path = workspace_root.join("sgclaw_config.json");
|
|
||||||
fs::write(
|
|
||||||
&config_path,
|
|
||||||
serde_json::to_string_pretty(&json!({
|
|
||||||
"apiKey": "deepseek-test-key",
|
|
||||||
"baseUrl": base_url,
|
|
||||||
"model": "deepseek-chat",
|
|
||||||
"runtimeProfile": "generalAssistant",
|
|
||||||
"skillsDir": staged_skill_root(),
|
|
||||||
}))
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
|
||||||
|
|
||||||
let transport = Arc::new(MockTransport::new(vec![]));
|
|
||||||
let browser_tool = BrowserPipeTool::new(
|
|
||||||
transport.clone(),
|
|
||||||
policy_for_domains(&["example.invalid"]),
|
|
||||||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
|
||||||
)
|
|
||||||
.with_response_timeout(Duration::from_secs(1));
|
|
||||||
|
|
||||||
handle_browser_message_with_context(
|
|
||||||
transport.as_ref(),
|
|
||||||
&browser_tool,
|
|
||||||
&runtime_context,
|
|
||||||
BrowserMessage::SubmitTask {
|
|
||||||
instruction: "导出 2026-04 故障明细".to_string(),
|
|
||||||
conversation_id: String::new(),
|
|
||||||
messages: vec![],
|
|
||||||
page_url: "https://example.invalid/workbench".to_string(),
|
|
||||||
page_title: "业务台账".to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
server_handle.join().unwrap();
|
|
||||||
|
|
||||||
let sent = transport.sent_messages();
|
|
||||||
let request_bodies = requests.lock().unwrap().clone();
|
|
||||||
let first_request = request_bodies[0].to_string();
|
|
||||||
let loaded_skills_message = sent.iter().find_map(|message| match message {
|
|
||||||
AgentMessage::LogEntry { level, message }
|
|
||||||
if level == "info" && message.starts_with("loaded skills: ") =>
|
|
||||||
{
|
|
||||||
Some(message.clone())
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
|
|
||||||
assert!(sent.iter().any(|message| {
|
|
||||||
matches!(
|
|
||||||
message,
|
|
||||||
AgentMessage::LogEntry { level, message }
|
|
||||||
if level == "mode" && message == "compat_llm_primary"
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
assert!(sent.iter().any(|message| {
|
|
||||||
matches!(
|
|
||||||
message,
|
|
||||||
AgentMessage::TaskComplete { success, summary }
|
|
||||||
if *success && summary == "已走通用助手路径"
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
assert_eq!(request_bodies.len(), 1);
|
|
||||||
assert!(!first_request.contains("95598-repair-city-dispatch.collect_repair_orders"));
|
|
||||||
assert!(!first_request.contains("browser workflow, not a text-only task"));
|
|
||||||
assert!(!first_request.contains("generic browser probing only after"));
|
|
||||||
assert!(loaded_skills_message.is_none());
|
|
||||||
assert!(!sent.iter().any(|message| {
|
|
||||||
matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn browser_attached_zhihu_hotlist_request_keeps_zhihu_contract_without_scene_injection() {
|
fn browser_attached_zhihu_hotlist_request_keeps_zhihu_contract_without_scene_injection() {
|
||||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||||||
@@ -2926,7 +2608,7 @@ fn browser_attached_zhihu_hotlist_request_keeps_zhihu_contract_without_scene_inj
|
|||||||
"deepseek-test-key".to_string(),
|
"deepseek-test-key".to_string(),
|
||||||
base_url,
|
base_url,
|
||||||
"deepseek-chat".to_string(),
|
"deepseek-chat".to_string(),
|
||||||
vec![real_skill_lib_root()],
|
Some(real_skill_lib_root()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||||||
@@ -2964,348 +2646,28 @@ fn browser_attached_zhihu_hotlist_request_keeps_zhihu_contract_without_scene_inj
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fault_details_direct_browser_scene_matches_primary_orchestration_gate() {
|
fn ws_cleanup_no_longer_detects_fault_details_scene_route() {
|
||||||
assert!(sgclaw::compat::orchestration::should_use_primary_orchestration(
|
use sgclaw::compat::workflow_executor::detect_route;
|
||||||
"导出故障明细",
|
|
||||||
Some("https://example.invalid/workbench"),
|
|
||||||
Some("业务台账"),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fault_details_direct_browser_scene_detects_direct_route() {
|
|
||||||
use sgclaw::compat::workflow_executor::{detect_route, WorkflowRoute};
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
detect_route(
|
detect_route(
|
||||||
"导出故障明细",
|
"导出故障明细",
|
||||||
Some("https://example.invalid/workbench"),
|
Some("https://example.invalid/workbench"),
|
||||||
Some("业务台账")
|
Some("业务台账"),
|
||||||
),
|
),
|
||||||
Some(WorkflowRoute::FaultDetailsReport)
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_scene_metadata_keeps_unrelated_primary_routing_unchanged() {
|
fn ws_cleanup_scene_keywords_do_not_trigger_primary_orchestration() {
|
||||||
let registry = [sgclaw::runtime::SceneRegistryEntry {
|
|
||||||
id: "unrelated-scene".to_string(),
|
|
||||||
name: "无关场景".to_string(),
|
|
||||||
summary: "与故障明细无关。".to_string(),
|
|
||||||
tags: vec!["other".to_string()],
|
|
||||||
inputs: vec!["period".to_string()],
|
|
||||||
outputs: vec!["artifact".to_string()],
|
|
||||||
skill_package: "unrelated-skill".to_string(),
|
|
||||||
skill_tool: "run_other".to_string(),
|
|
||||||
skill_artifact_type: "artifact".to_string(),
|
|
||||||
dispatch_mode: sgclaw::runtime::DispatchMode::DirectBrowser,
|
|
||||||
expected_domain: "other.example.invalid".to_string(),
|
|
||||||
aliases: vec!["别的事情".to_string()],
|
|
||||||
default_args: serde_json::Map::new(),
|
|
||||||
}];
|
|
||||||
|
|
||||||
assert!(sgclaw::runtime::match_scene_instruction_in_registry(®istry, "别的事情").is_some());
|
|
||||||
assert!(sgclaw::runtime::match_scene_instruction_in_registry(®istry, "导出故障明细").is_none());
|
|
||||||
assert!(!sgclaw::compat::orchestration::should_use_primary_orchestration(
|
assert!(!sgclaw::compat::orchestration::should_use_primary_orchestration(
|
||||||
"帮我汇总今天待办",
|
"请处理95598抢修市指监测",
|
||||||
Some("https://example.invalid/workbench"),
|
Some("https://95598.example.invalid/dispatch"),
|
||||||
Some("业务台账"),
|
Some("95598抢修市指监测"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fault_details_route_returns_clear_failure_when_period_cannot_be_derived() {
|
|
||||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
|
||||||
|
|
||||||
let workspace_root = temp_workspace_root();
|
|
||||||
let config_path = write_deepseek_config_with_skills_dir(
|
|
||||||
&workspace_root,
|
|
||||||
"deepseek-test-key",
|
|
||||||
"http://127.0.0.1:9",
|
|
||||||
"deepseek-chat",
|
|
||||||
Some(staged_skill_root().to_str().unwrap()),
|
|
||||||
);
|
|
||||||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
|
||||||
|
|
||||||
let transport = Arc::new(MockTransport::new(vec![]));
|
|
||||||
let browser_tool = BrowserPipeTool::new(
|
|
||||||
transport.clone(),
|
|
||||||
zhihu_test_policy(),
|
|
||||||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
|
||||||
)
|
|
||||||
.with_response_timeout(Duration::from_secs(1));
|
|
||||||
|
|
||||||
handle_browser_message_with_context(
|
|
||||||
transport.as_ref(),
|
|
||||||
&browser_tool,
|
|
||||||
&runtime_context,
|
|
||||||
BrowserMessage::SubmitTask {
|
|
||||||
instruction: "导出故障明细".to_string(),
|
|
||||||
conversation_id: String::new(),
|
|
||||||
messages: vec![],
|
|
||||||
page_url: "https://example.invalid/workbench".to_string(),
|
|
||||||
page_title: "业务台账".to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let sent = transport.sent_messages();
|
|
||||||
assert!(sent.iter().any(|message| {
|
|
||||||
matches!(
|
|
||||||
message,
|
|
||||||
AgentMessage::LogEntry { level, message }
|
|
||||||
if level == "mode" && message == "zeroclaw_process_message_primary"
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
assert!(sent.iter().any(|message| {
|
|
||||||
matches!(
|
|
||||||
message,
|
|
||||||
AgentMessage::TaskComplete { success, summary }
|
|
||||||
if !*success && summary.contains("period") && summary.contains("无法")
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
assert!(!sent.iter().any(|message| {
|
|
||||||
matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fault_details_route_uses_current_page_host_as_expected_domain() {
|
|
||||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
|
||||||
|
|
||||||
let workspace_root = temp_workspace_root();
|
|
||||||
let config_path = write_deepseek_config_with_skills_dir(
|
|
||||||
&workspace_root,
|
|
||||||
"deepseek-test-key",
|
|
||||||
"http://127.0.0.1:9",
|
|
||||||
"deepseek-chat",
|
|
||||||
Some(staged_skill_root().to_str().unwrap()),
|
|
||||||
);
|
|
||||||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
|
||||||
|
|
||||||
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
|
|
||||||
1,
|
|
||||||
json!({
|
|
||||||
"text": {
|
|
||||||
"sheet_name": "故障明细",
|
|
||||||
"rows": [["2026-04", "已完成"]]
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)]));
|
|
||||||
let browser_tool = BrowserPipeTool::new(
|
|
||||||
transport.clone(),
|
|
||||||
policy_for_domains(&["example.invalid"]),
|
|
||||||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
|
||||||
)
|
|
||||||
.with_response_timeout(Duration::from_secs(1));
|
|
||||||
|
|
||||||
handle_browser_message_with_context(
|
|
||||||
transport.as_ref(),
|
|
||||||
&browser_tool,
|
|
||||||
&runtime_context,
|
|
||||||
BrowserMessage::SubmitTask {
|
|
||||||
instruction: "导出 2026-04 故障明细".to_string(),
|
|
||||||
conversation_id: String::new(),
|
|
||||||
messages: vec![],
|
|
||||||
page_url: "https://example.invalid/workbench".to_string(),
|
|
||||||
page_title: "业务台账".to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let sent = transport.sent_messages();
|
|
||||||
assert!(sent.iter().any(|message| {
|
|
||||||
matches!(
|
|
||||||
message,
|
|
||||||
AgentMessage::TaskComplete { success, summary }
|
|
||||||
if *success && summary.contains("sheet_name") && summary.contains("故障明细")
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
let eval_command = sent.iter().find_map(|message| match message {
|
|
||||||
AgentMessage::Command {
|
|
||||||
action,
|
|
||||||
params,
|
|
||||||
security,
|
|
||||||
..
|
|
||||||
} if action == &Action::Eval => Some((params.clone(), security.expected_domain.clone())),
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
let (params, expected_domain) = eval_command.expect("direct route should call browser eval");
|
|
||||||
assert_eq!(expected_domain, "example.invalid");
|
|
||||||
let script = params["script"].as_str().unwrap_or_default();
|
|
||||||
assert!(script.contains("const args = {\"period\":\"2026-04\"};"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fault_details_route_uses_packaged_browser_script_from_configured_skills_root() {
|
|
||||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
|
||||||
|
|
||||||
let workspace_root = temp_workspace_root();
|
|
||||||
let staged_root = workspace_root.join("custom_fault_details_staging");
|
|
||||||
let custom_skills_dir = staged_root.join("skills");
|
|
||||||
let skill_dir = write_skill_manifest_package(
|
|
||||||
&custom_skills_dir,
|
|
||||||
"fault-details-report",
|
|
||||||
r#"
|
|
||||||
[skill]
|
|
||||||
name = "fault-details-report"
|
|
||||||
description = "Collect fault detail rows via a packaged browser script."
|
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
[[tools]]
|
|
||||||
name = "collect_fault_details"
|
|
||||||
description = "Collect fault detail rows for the target period."
|
|
||||||
kind = "browser_script"
|
|
||||||
command = "scripts/custom_fault_details.js"
|
|
||||||
|
|
||||||
[tools.args]
|
|
||||||
period = "Target report period."
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
write_skill_script(
|
|
||||||
&skill_dir,
|
|
||||||
"scripts/custom_fault_details.js",
|
|
||||||
r#"
|
|
||||||
return {
|
|
||||||
sheet_name: "故障明细",
|
|
||||||
rows: [[args.period || "unknown", "CUSTOM_FAULT_DETAILS_MARKER"]],
|
|
||||||
marker: "CUSTOM_FAULT_DETAILS_MARKER"
|
|
||||||
};
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
let config_path = write_deepseek_config_with_skills_dir(
|
|
||||||
&workspace_root,
|
|
||||||
"deepseek-test-key",
|
|
||||||
"http://127.0.0.1:9",
|
|
||||||
"deepseek-chat",
|
|
||||||
Some(staged_root.to_str().unwrap()),
|
|
||||||
);
|
|
||||||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
|
||||||
|
|
||||||
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
|
|
||||||
1,
|
|
||||||
json!({
|
|
||||||
"text": {
|
|
||||||
"sheet_name": "故障明细",
|
|
||||||
"rows": [["2026-04", "已完成"]]
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)]));
|
|
||||||
let browser_tool = BrowserPipeTool::new(
|
|
||||||
transport.clone(),
|
|
||||||
policy_for_domains(&["example.invalid"]),
|
|
||||||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
|
||||||
)
|
|
||||||
.with_response_timeout(Duration::from_secs(1));
|
|
||||||
|
|
||||||
handle_browser_message_with_context(
|
|
||||||
transport.as_ref(),
|
|
||||||
&browser_tool,
|
|
||||||
&runtime_context,
|
|
||||||
BrowserMessage::SubmitTask {
|
|
||||||
instruction: "导出 2026-04 故障明细".to_string(),
|
|
||||||
conversation_id: String::new(),
|
|
||||||
messages: vec![],
|
|
||||||
page_url: "https://example.invalid/workbench".to_string(),
|
|
||||||
page_title: "业务台账".to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let sent = transport.sent_messages();
|
|
||||||
assert!(sent.iter().any(|message| {
|
|
||||||
matches!(
|
|
||||||
message,
|
|
||||||
AgentMessage::TaskComplete { success, summary }
|
|
||||||
if *success && summary.contains("sheet_name") && summary.contains("故障明细")
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
let eval_command = sent.iter().find_map(|message| match message {
|
|
||||||
AgentMessage::Command {
|
|
||||||
action,
|
|
||||||
params,
|
|
||||||
security,
|
|
||||||
..
|
|
||||||
} if action == &Action::Eval => Some((params.clone(), security.expected_domain.clone())),
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
let (params, expected_domain) = eval_command.expect("direct route should call browser eval");
|
|
||||||
assert_eq!(expected_domain, "example.invalid");
|
|
||||||
let script = params["script"].as_str().unwrap_or_default();
|
|
||||||
assert!(script.contains("const args = {\"period\":\"2026-04\"};"));
|
|
||||||
assert!(script.contains("CUSTOM_FAULT_DETAILS_MARKER"));
|
|
||||||
assert!(!script.contains("collect_fault_details.js"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fault_details_route_executes_browser_script_eval_when_period_is_derived() {
|
|
||||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
|
||||||
|
|
||||||
let workspace_root = temp_workspace_root();
|
|
||||||
let config_path = write_deepseek_config_with_skills_dir(
|
|
||||||
&workspace_root,
|
|
||||||
"deepseek-test-key",
|
|
||||||
"http://127.0.0.1:9",
|
|
||||||
"deepseek-chat",
|
|
||||||
Some(staged_skill_root().to_str().unwrap()),
|
|
||||||
);
|
|
||||||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
|
||||||
|
|
||||||
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
|
|
||||||
1,
|
|
||||||
json!({
|
|
||||||
"text": {
|
|
||||||
"sheet_name": "故障明细",
|
|
||||||
"rows": [["2026-04", "已完成"]]
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)]));
|
|
||||||
let browser_tool = BrowserPipeTool::new(
|
|
||||||
transport.clone(),
|
|
||||||
policy_for_domains(&["example.invalid"]),
|
|
||||||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
|
||||||
)
|
|
||||||
.with_response_timeout(Duration::from_secs(1));
|
|
||||||
|
|
||||||
handle_browser_message_with_context(
|
|
||||||
transport.as_ref(),
|
|
||||||
&browser_tool,
|
|
||||||
&runtime_context,
|
|
||||||
BrowserMessage::SubmitTask {
|
|
||||||
instruction: "导出 2026-04 故障明细".to_string(),
|
|
||||||
conversation_id: String::new(),
|
|
||||||
messages: vec![],
|
|
||||||
page_url: "https://example.invalid/workbench".to_string(),
|
|
||||||
page_title: "业务台账".to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let sent = transport.sent_messages();
|
|
||||||
assert!(sent.iter().any(|message| {
|
|
||||||
matches!(
|
|
||||||
message,
|
|
||||||
AgentMessage::TaskComplete { success, summary }
|
|
||||||
if *success && summary.contains("sheet_name") && summary.contains("故障明细")
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
let eval_command = sent.iter().find_map(|message| match message {
|
|
||||||
AgentMessage::Command {
|
|
||||||
action,
|
|
||||||
params,
|
|
||||||
security,
|
|
||||||
..
|
|
||||||
} if action == &Action::Eval => Some((params.clone(), security.expected_domain.clone())),
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
let (params, expected_domain) = eval_command.expect("direct route should call browser eval");
|
|
||||||
assert_eq!(expected_domain, "example.invalid");
|
|
||||||
let script = params["script"].as_str().unwrap_or_default();
|
|
||||||
assert!(script.contains("const args = {\"period\":\"2026-04\"};"));
|
|
||||||
assert!(script.contains("sheet_name") || script.contains("return JSON.stringify") || script.contains("rows"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn zhihu_generated_auto_publish_matches_primary_orchestration_gate() {
|
fn zhihu_generated_auto_publish_matches_primary_orchestration_gate() {
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ fn deepseek_settings_load_defaults_from_env() {
|
|||||||
assert_eq!(settings.api_key, "test-key");
|
assert_eq!(settings.api_key, "test-key");
|
||||||
assert_eq!(settings.base_url, "https://api.deepseek.com");
|
assert_eq!(settings.base_url, "https://api.deepseek.com");
|
||||||
assert_eq!(settings.model, "deepseek-chat");
|
assert_eq!(settings.model, "deepseek-chat");
|
||||||
assert!(settings.skills_dir.is_empty());
|
assert!(settings.skills_dir.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -30,7 +30,7 @@ fn deepseek_request_shape_matches_openai_compatible_chat_format() {
|
|||||||
api_key: "test-key".to_string(),
|
api_key: "test-key".to_string(),
|
||||||
base_url: "https://api.deepseek.com".to_string(),
|
base_url: "https://api.deepseek.com".to_string(),
|
||||||
model: "deepseek-chat".to_string(),
|
model: "deepseek-chat".to_string(),
|
||||||
skills_dir: Vec::new(),
|
skills_dir: None,
|
||||||
});
|
});
|
||||||
let messages = vec![
|
let messages = vec![
|
||||||
ChatMessage {
|
ChatMessage {
|
||||||
|
|||||||
@@ -50,13 +50,13 @@ fn loaded_skills_excludes_browser_script_tools_when_browser_surface_is_unavailab
|
|||||||
));
|
));
|
||||||
fs::create_dir_all(&workspace_root).unwrap();
|
fs::create_dir_all(&workspace_root).unwrap();
|
||||||
let skill_root = temp_skill_root();
|
let skill_root = temp_skill_root();
|
||||||
write_browser_script_skill(&skill_root, "fault-details-report");
|
write_browser_script_skill(&skill_root, "workspace-browser-skill");
|
||||||
|
|
||||||
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
|
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
|
||||||
"sk-test".to_string(),
|
"sk-test".to_string(),
|
||||||
"https://api.deepseek.com".to_string(),
|
"https://api.deepseek.com".to_string(),
|
||||||
"deepseek-chat".to_string(),
|
"deepseek-chat".to_string(),
|
||||||
vec![skill_root.clone()],
|
Some(skill_root.clone()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
settings.runtime_profile = RuntimeProfile::GeneralAssistant;
|
settings.runtime_profile = RuntimeProfile::GeneralAssistant;
|
||||||
@@ -64,7 +64,7 @@ fn loaded_skills_excludes_browser_script_tools_when_browser_surface_is_unavailab
|
|||||||
let skills_dir = resolve_skills_dir_from_sgclaw_settings(&workspace_root, &settings);
|
let skills_dir = resolve_skills_dir_from_sgclaw_settings(&workspace_root, &settings);
|
||||||
let engine = RuntimeEngine::new(RuntimeProfile::GeneralAssistant);
|
let engine = RuntimeEngine::new(RuntimeProfile::GeneralAssistant);
|
||||||
|
|
||||||
let loaded_skills = engine.loaded_skills(&config, &skills_dir);
|
let loaded_skills = engine.loaded_skills(&config, std::slice::from_ref(&skills_dir));
|
||||||
|
|
||||||
assert!(loaded_skills.is_empty());
|
assert!(loaded_skills.is_empty());
|
||||||
}
|
}
|
||||||
@@ -125,19 +125,16 @@ fn browser_attached_publish_prompt_requires_explicit_confirmation_before_clickin
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn browser_attached_95598_scene_prompt_requires_scene_tool_before_generic_browser_probing() {
|
fn ws_cleanup_browser_profile_does_not_inject_95598_scene_contract() {
|
||||||
let engine = RuntimeEngine::new(RuntimeProfile::BrowserAttached);
|
let engine = RuntimeEngine::new(RuntimeProfile::BrowserAttached);
|
||||||
|
|
||||||
let instruction = engine.build_instruction(
|
let instruction = engine.build_instruction(
|
||||||
"请处理95598-repair-city-dispatch场景,查看抢修市指派单并汇总当前队列",
|
"请处理95598抢修市指监测,查看抢修市指派单并汇总当前队列",
|
||||||
Some("https://95598.example.invalid/dispatch"),
|
Some("https://95598.example.invalid/dispatch"),
|
||||||
Some("95598抢修市指监测"),
|
Some("95598抢修市指监测"),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(instruction.contains("95598-repair-city-dispatch.collect_repair_orders"));
|
assert!(!instruction.contains("collect_repair_orders"));
|
||||||
assert!(instruction.contains("browser workflow, not a text-only task"));
|
|
||||||
assert!(instruction.contains("generic browser probing only after"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -151,7 +148,7 @@ fn browser_attached_unrelated_task_does_not_receive_95598_scene_contract() {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(!instruction.contains("95598-repair-city-dispatch.collect_repair_orders"));
|
assert!(!instruction.contains("collect_repair_orders"));
|
||||||
assert!(!instruction.contains("browser workflow, not a text-only task"));
|
assert!(!instruction.contains("browser workflow, not a text-only task"));
|
||||||
assert!(!instruction.contains("generic browser probing only after"));
|
assert!(!instruction.contains("generic browser probing only after"));
|
||||||
}
|
}
|
||||||
@@ -161,13 +158,13 @@ fn general_assistant_95598_scene_prompt_does_not_receive_browser_scene_contract(
|
|||||||
let engine = RuntimeEngine::new(RuntimeProfile::GeneralAssistant);
|
let engine = RuntimeEngine::new(RuntimeProfile::GeneralAssistant);
|
||||||
|
|
||||||
let instruction = engine.build_instruction(
|
let instruction = engine.build_instruction(
|
||||||
"请处理95598-repair-city-dispatch场景,查看抢修市指派单并汇总当前队列",
|
"请处理95598抢修市指监测,查看抢修市指派单并汇总当前队列",
|
||||||
Some("https://95598.example.invalid/dispatch"),
|
Some("https://95598.example.invalid/dispatch"),
|
||||||
Some("95598抢修市指监测"),
|
Some("95598抢修市指监测"),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(!instruction.contains("95598-repair-city-dispatch.collect_repair_orders"));
|
assert!(!instruction.contains("collect_repair_orders"));
|
||||||
assert!(!instruction.contains("browser workflow, not a text-only task"));
|
assert!(!instruction.contains("browser workflow, not a text-only task"));
|
||||||
assert!(!instruction.contains("generic browser probing only after"));
|
assert!(!instruction.contains("generic browser probing only after"));
|
||||||
}
|
}
|
||||||
@@ -178,7 +175,7 @@ fn legacy_settings_default_to_plan_first_superrpa_and_openxml_backends() {
|
|||||||
"sk-test".to_string(),
|
"sk-test".to_string(),
|
||||||
"https://api.deepseek.com".to_string(),
|
"https://api.deepseek.com".to_string(),
|
||||||
"deepseek-chat".to_string(),
|
"deepseek-chat".to_string(),
|
||||||
Vec::new(),
|
None,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -1,223 +0,0 @@
|
|||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use sgclaw::runtime::{
|
|
||||||
load_scene_registry_from_root, match_scene_instruction_in_registry, DispatchMode,
|
|
||||||
};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn scene_registry_loads_first_slice_dispatch_policies() {
|
|
||||||
let root = TempSceneRoot::new();
|
|
||||||
write_first_slice_scenes(&root);
|
|
||||||
|
|
||||||
let registry = load_scene_registry_from_root(root.path());
|
|
||||||
|
|
||||||
let fault_details = registry
|
|
||||||
.iter()
|
|
||||||
.find(|entry| entry.id == "fault-details-report")
|
|
||||||
.expect("fault-details-report scene should load");
|
|
||||||
assert_eq!(fault_details.dispatch_mode, DispatchMode::DirectBrowser);
|
|
||||||
assert_eq!(fault_details.expected_domain, "sgcc.example.invalid");
|
|
||||||
assert_eq!(fault_details.skill_package, "fault-details-report");
|
|
||||||
assert_eq!(fault_details.skill_tool, "collect_fault_details");
|
|
||||||
|
|
||||||
let repair_dispatch = registry
|
|
||||||
.iter()
|
|
||||||
.find(|entry| entry.id == "95598-repair-city-dispatch")
|
|
||||||
.expect("95598-repair-city-dispatch scene should load");
|
|
||||||
assert_eq!(repair_dispatch.dispatch_mode, DispatchMode::AgentBrowser);
|
|
||||||
assert_eq!(repair_dispatch.skill_package, "95598-repair-city-dispatch");
|
|
||||||
assert_eq!(repair_dispatch.skill_tool, "collect_repair_orders");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn scene_registry_matches_fault_details_natural_language_instruction() {
|
|
||||||
let root = TempSceneRoot::new();
|
|
||||||
write_first_slice_scenes(&root);
|
|
||||||
let registry = load_scene_registry_from_root(root.path());
|
|
||||||
|
|
||||||
let matched =
|
|
||||||
match_scene_instruction_in_registry(®istry, "请帮我导出故障明细").expect("scene should match");
|
|
||||||
|
|
||||||
assert_eq!(matched.id, "fault-details-report");
|
|
||||||
assert_eq!(matched.dispatch_mode, DispatchMode::DirectBrowser);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn scene_registry_matches_city_dispatch_natural_language_instruction() {
|
|
||||||
let root = TempSceneRoot::new();
|
|
||||||
write_first_slice_scenes(&root);
|
|
||||||
let registry = load_scene_registry_from_root(root.path());
|
|
||||||
|
|
||||||
let matched = match_scene_instruction_in_registry(®istry, "帮我看一下95598抢修市指监测")
|
|
||||||
.expect("scene should match");
|
|
||||||
|
|
||||||
assert_eq!(matched.id, "95598-repair-city-dispatch");
|
|
||||||
assert_eq!(matched.dispatch_mode, DispatchMode::AgentBrowser);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn scene_registry_matches_rephrased_instruction_via_alias_terms() {
|
|
||||||
let root = TempSceneRoot::new();
|
|
||||||
write_first_slice_scenes(&root);
|
|
||||||
let registry = load_scene_registry_from_root(root.path());
|
|
||||||
|
|
||||||
let matched = match_scene_instruction_in_registry(®istry, "想看市指那边的95598抢修队列")
|
|
||||||
.expect("scene should match");
|
|
||||||
|
|
||||||
assert_eq!(matched.id, "95598-repair-city-dispatch");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn scene_registry_returns_none_for_unrelated_instruction() {
|
|
||||||
let root = TempSceneRoot::new();
|
|
||||||
write_first_slice_scenes(&root);
|
|
||||||
let registry = load_scene_registry_from_root(root.path());
|
|
||||||
|
|
||||||
assert!(match_scene_instruction_in_registry(®istry, "今天上海天气怎么样").is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn scene_registry_ignores_missing_or_broken_scene_files() {
|
|
||||||
let root = TempSceneRoot::new();
|
|
||||||
root.write_scene(
|
|
||||||
"fault-details-report",
|
|
||||||
r#"{
|
|
||||||
"id": "fault-details-report",
|
|
||||||
"name": "故障明细",
|
|
||||||
"summary": "查询故障明细行并生成结构化报表。",
|
|
||||||
"inputs": ["period"],
|
|
||||||
"outputs": ["report-artifact"],
|
|
||||||
"tags": ["fault", "report"],
|
|
||||||
"skill": {
|
|
||||||
"package": "fault-details-report",
|
|
||||||
"tool": "collect_fault_details",
|
|
||||||
"artifact_type": "report-artifact"
|
|
||||||
}
|
|
||||||
}"#,
|
|
||||||
);
|
|
||||||
root.write_scene("95598-repair-city-dispatch", "{ broken json");
|
|
||||||
|
|
||||||
let registry = load_scene_registry_from_root(root.path());
|
|
||||||
|
|
||||||
assert_eq!(registry.len(), 1);
|
|
||||||
assert_eq!(registry[0].id, "fault-details-report");
|
|
||||||
assert_eq!(registry[0].dispatch_mode, DispatchMode::DirectBrowser);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn scene_registry_ignores_mismatched_scene_metadata_id() {
|
|
||||||
let root = TempSceneRoot::new();
|
|
||||||
root.write_scene(
|
|
||||||
"fault-details-report",
|
|
||||||
r#"{
|
|
||||||
"id": "wrong-scene-id",
|
|
||||||
"name": "故障明细",
|
|
||||||
"summary": "查询故障明细行并生成结构化报表。",
|
|
||||||
"inputs": ["period"],
|
|
||||||
"outputs": ["report-artifact"],
|
|
||||||
"tags": ["fault", "report"],
|
|
||||||
"skill": {
|
|
||||||
"package": "fault-details-report",
|
|
||||||
"tool": "collect_fault_details",
|
|
||||||
"artifact_type": "report-artifact"
|
|
||||||
}
|
|
||||||
}"#,
|
|
||||||
);
|
|
||||||
root.write_scene(
|
|
||||||
"95598-repair-city-dispatch",
|
|
||||||
r#"{
|
|
||||||
"id": "95598-repair-city-dispatch",
|
|
||||||
"name": "95598抢修市指监测",
|
|
||||||
"summary": "采集95598抢修市指监测列表。",
|
|
||||||
"inputs": ["period"],
|
|
||||||
"outputs": ["repair-orders"],
|
|
||||||
"tags": ["95598", "repair", "dispatch"],
|
|
||||||
"skill": {
|
|
||||||
"package": "95598-repair-city-dispatch",
|
|
||||||
"tool": "collect_repair_orders",
|
|
||||||
"artifact_type": "repair-orders"
|
|
||||||
}
|
|
||||||
}"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
let registry = load_scene_registry_from_root(root.path());
|
|
||||||
|
|
||||||
assert_eq!(registry.len(), 1);
|
|
||||||
assert_eq!(registry[0].id, "95598-repair-city-dispatch");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn scene_registry_returns_none_for_ambiguous_instruction() {
|
|
||||||
let root = TempSceneRoot::new();
|
|
||||||
write_first_slice_scenes(&root);
|
|
||||||
let registry = load_scene_registry_from_root(root.path());
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
match_scene_instruction_in_registry(®istry, "请同时处理导出故障明细和95598抢修市指监测").is_none()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TempSceneRoot {
|
|
||||||
root: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TempSceneRoot {
|
|
||||||
fn new() -> Self {
|
|
||||||
let root = std::env::temp_dir().join(format!("scene-registry-test-{}", Uuid::new_v4()));
|
|
||||||
fs::create_dir_all(root.join("scenes")).expect("temp scene root should be created");
|
|
||||||
Self { root }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn path(&self) -> &Path {
|
|
||||||
&self.root
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_scene(&self, scene_id: &str, contents: &str) {
|
|
||||||
let scene_dir = self.root.join("scenes").join(scene_id);
|
|
||||||
fs::create_dir_all(&scene_dir).expect("scene directory should be created");
|
|
||||||
fs::write(scene_dir.join("scene.json"), contents).expect("scene file should be written");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_first_slice_scenes(root: &TempSceneRoot) {
|
|
||||||
root.write_scene(
|
|
||||||
"fault-details-report",
|
|
||||||
r#"{
|
|
||||||
"id": "fault-details-report",
|
|
||||||
"name": "故障明细",
|
|
||||||
"summary": "查询故障明细行并生成结构化报表。",
|
|
||||||
"inputs": ["period"],
|
|
||||||
"outputs": ["report-artifact"],
|
|
||||||
"tags": ["fault", "report"],
|
|
||||||
"skill": {
|
|
||||||
"package": "fault-details-report",
|
|
||||||
"tool": "collect_fault_details",
|
|
||||||
"artifact_type": "report-artifact"
|
|
||||||
}
|
|
||||||
}"#,
|
|
||||||
);
|
|
||||||
root.write_scene(
|
|
||||||
"95598-repair-city-dispatch",
|
|
||||||
r#"{
|
|
||||||
"id": "95598-repair-city-dispatch",
|
|
||||||
"name": "95598抢修市指监测",
|
|
||||||
"summary": "采集95598抢修市指监测列表。",
|
|
||||||
"inputs": ["period"],
|
|
||||||
"outputs": ["repair-orders"],
|
|
||||||
"tags": ["95598", "repair", "dispatch"],
|
|
||||||
"skill": {
|
|
||||||
"package": "95598-repair-city-dispatch",
|
|
||||||
"tool": "collect_repair_orders",
|
|
||||||
"artifact_type": "repair-orders"
|
|
||||||
}
|
|
||||||
}"#,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for TempSceneRoot {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let _ = fs::remove_dir_all(&self.root);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user