# Zhihu Hotlist Post-Export Auto-Open Design

## Background

The current Zhihu hotlist workflows already support two separate artifact outputs:

- `openxml_office` generates a local `.xlsx` file for hotlist export
- `screen_html_export` generates a local `.html` dashboard for presentation

Today, the workflow stops after artifact generation and returns a summary string such as:

- `已导出知乎热榜 Excel <path>`
- `已生成知乎热榜大屏 <path>`

That means the user still has to manually open the generated file.

The user wants one additional post-export action, but only one at a time:

1. for Excel-oriented tasks, automatically open the generated `.xlsx` with the system default spreadsheet application
2. for dashboard-oriented tasks, automatically open the generated local dashboard HTML inside the running sgBrowser session

This is an exclusive choice, not a combined mode.

## Current Runtime Facts

The implementation must match the current browser/runtime boundary that already exists in the repo:

- the active service submit path in `src/service/server.rs` constructs `BrowserCallbackBackend`
- `BrowserCallbackBackend::invoke(Action::Navigate, ...)` currently emits `sgBrowerserOpenPage`, which opens a new visible browser tab and keeps the helper page alive
- `WsBrowserBackend::invoke(Action::Navigate, ...)` has different semantics and a different transport path from the callback-host service path
- `MacPolicy::validate(...)` currently rejects empty or non-domain values, so a raw `file://...` navigation cannot pass through the normal domain validation path today
- `screen_html_export` already returns `presentation.url`, which is the existing `file://` presentation URL contract for the generated dashboard

Those facts mean the design must not promise "replace the helper page" or "reuse identical tab behavior across all backends". The required success path for this slice is narrower: open the generated dashboard automatically in the current callback-host-backed sgBrowser service session without adding a new user-facing surface.

## Problem Statement

The existing workflow logic in `src/compat/workflow_executor.rs` already separates hotlist export from dashboard generation, but it treats both routes as artifact-only flows. The last mile is missing:

- the Excel route does not auto-open the generated file
- the dashboard route does not consume the generated dashboard presentation URL and open it automatically in the browser runtime

The risk is scope drift. This change must not:

- turn Excel-open and dashboard-open into a combined workflow
- add new help/help-like user-visible surfaces
- move orchestration into `frontend/service-console/`
- modify the websocket protocol
- modify `browser-helper.html`
- modify callback-host HTTP endpoints or their contracts
- change the artifact-generation contract of `openxml_office` or `screen_html_export`

## Goal

Extend the existing Zhihu hotlist post-export behavior so that:

- Excel tasks generate `.xlsx` and then auto-open it with the local system default spreadsheet application
- dashboard tasks generate `.html` and then auto-open that generated dashboard inside sgBrowser

On the current callback-host service path, "inside sgBrowser" means opening the generated dashboard in a new visible browser tab while the helper page stays alive. The user does not need to open the file manually.

## Non-goals

This slice does not include:

- opening Excel and dashboard in the same run
- adding a new combined route that auto-opens both artifacts
- adding any new help, helper, or user-visible assistance surface
- modifying `frontend/service-console/sg_claw_service_console.html`
- modifying `src/service/protocol.rs`
- modifying `browser-helper.html`
- modifying `/sgclaw/callback/*` contracts
- turning the browser backend into a general-purpose local filesystem browser
- changing the artifact-generation JSON contract of `openxml_office` or `screen_html_export`

## Chosen Approach

Keep the current two workflow routes, but add one route-specific post-export action to each:

- `ZhihuHotlistExportXlsx` -> generate `.xlsx`, then open it locally with the OS default app
- `ZhihuHotlistScreen` -> generate `.html`, then open the generated dashboard presentation URL in the browser runtime

For the dashboard route, use the existing `presentation.url` returned by `screen_html_export` as the authoritative browser-open URL. Do not invent a separate normal-path URL conversion layer when the tool already returns the presentation contract.

The compat opener must emit one exact navigate request shape for this case.

- `action`: `Action::Navigate`
- `expected_domain`: the exact literal `__sgclaw_local_dashboard__`
- `params.url`: the exact `presentation.url` returned by `screen_html_export`
- `params.sgclaw_local_dashboard_open.source`: the exact literal `compat.workflow_executor`
- `params.sgclaw_local_dashboard_open.kind`: the exact literal `zhihu_hotlist_screen`
- `params.sgclaw_local_dashboard_open.output_path`: the generated local dashboard artifact path
- `params.sgclaw_local_dashboard_open.presentation_url`: the same `file://` URL stored in `params.url`

On the current callback-host-backed service path, only that exact request shape is approved for the local-dashboard special case. A plain `Action::Navigate` with an arbitrary `file://...` URL, or a request missing any one of the required marker fields above, must continue to be rejected.

Because normal `MacPolicy` domain validation cannot accept `file://...`, add a narrow local-dashboard presentation allowance in the browser backend/security boundary. That allowance must be limited to this one case:

- only for `Action::Navigate`
- only for generated local dashboard presentation URLs
- only for local HTML presentation, not arbitrary local paths or generic file browsing

Why this approach:

- it preserves the existing mutual exclusivity between Excel export and dashboard presentation
- it keeps artifact generation in the existing tools
- it keeps browser opening inside the existing browser backend boundary
- it uses the existing `screen_html_export` presentation contract instead of duplicating it
- it avoids pushing orchestration into the service console or protocol layer
- it stays compatible with the current callback-host runtime, where visible navigation is new-tab based
- it limits the guaranteed browser-open behavior in this slice to the callback-host-backed service path that the user is using today

Rejected alternatives:

- add a combined "Excel + dashboard" route: explicitly rejected by user behavior
- let `frontend/service-console/` decide when to open generated files: wrong layer; the console is only a submit/view surface
- add help UI to expose output choices: explicitly unwanted by the user
- change `browser-helper.html` so the helper page itself becomes the dashboard: this would break the current helper-page persistence model
- promise a backend-agnostic "replace the current page" behavior: inaccurate because callback-host and websocket backends do not share identical navigate semantics
- require the websocket backend to gain matching local-dashboard visible-open behavior in this slice: outside the narrow current-service-path goal

## File Responsibilities

### `src/compat/workflow_executor.rs`

Continue to own:

- route detection for Zhihu hotlist workflows
- artifact generation orchestration
- post-export summary construction

New responsibilities in this slice:

- parse the successful artifact payloads after `openxml_office` and `screen_html_export`
- call the route-specific post-export opener only after artifact creation succeeds
- for the dashboard route, consume `presentation.url` from the `screen_html_export` result payload
- keep generation success and post-export open success/failure distinct in the returned summary

### `src/compat/artifact_open.rs`

New helper module to keep side effects out of `workflow_executor.rs`.

Responsibilities:

- open a generated local `.xlsx` with the system default application
- open a generated local dashboard presentation URL through the existing `BrowserBackend`
- construct the exact approved dashboard navigate request shape used by this slice
- define the narrow local-dashboard presentation token/constants used by the compat layer and backend compatibility path
- return narrow success/failure results so `workflow_executor.rs` can produce accurate summaries

This module must stay small and focused. It is not a general launcher framework.

### `src/browser/callback_backend.rs`

New narrow responsibility in this slice:

- at the `BrowserCallbackBackend::invoke(Action::Navigate, params, expected_domain)` entrypoint, recognize only the exact approved local-dashboard presentation request shape
- preserve the current callback-host behavior of using `sgBrowerserOpenPage`, which opens a new visible tab and keeps the helper page alive
- reject local-file navigate attempts that do not include the exact post-export marker payload from the compat layer

This slice must not change callback-host polling, helper bootstrap, or callback endpoint behavior.

### `src/browser/ws_backend.rs`

No required behavior change in this slice.

Notes:

- websocket transport semantics differ from the callback-host service path
- this spec does not require websocket backend local-dashboard visible-open support
- websocket-specific parity can be designed later as a separate slice if needed

### `src/security/mac_policy.rs`

New narrow responsibility in this slice:

- expose a small validation helper for the approved local-dashboard presentation case
- validate the real local presentation URL and artifact path for that case rather than treating `file://` as a normal allowed domain
- keep the normal domain-based validation path unchanged for ordinary remote navigation

The policy layer must not turn `file://` into a generally allowed "domain". This is an explicit special case for generated local dashboard presentation only.

### `src/compat/mod.rs`

Expose the new helper module.

## Route Semantics

### Excel export route

Trigger examples:

- `读取知乎热榜数据，并导出 excel 文件`
- `导出知乎热榜 xlsx`

Expected behavior:

1. collect hotlist rows
2. call `openxml_office`
3. obtain `output_path`
4. open the generated `.xlsx` using the local OS default spreadsheet application
5. return a success summary reflecting both generation and open state

Summary rules:

- open succeeded -> `已导出并打开知乎热榜 Excel <path>`
- open failed but file exists -> `已导出知乎热榜 Excel <path>，但自动打开失败：<reason>`

The workflow still counts artifact generation as successful even if the post-export open step fails.

### Dashboard route

Trigger examples:

- `读取知乎热榜数据并生成领导演示大屏`
- `生成知乎热榜 dashboard`
- `展示知乎热榜大屏`

Expected behavior:

1. collect hotlist rows
2. call `screen_html_export`
3. obtain `output_path`
4. obtain `presentation.url` from the tool result payload
5. invoke the browser opener through the existing `BrowserBackend`
6. return a success summary reflecting both generation and browser-open state

Summary rules:

- browser open succeeded -> `已在浏览器中打开知乎热榜大屏 <path>`
- browser open failed but file exists -> `已生成知乎热榜大屏 <path>，但浏览器自动打开失败：<reason>`

The workflow still counts artifact generation as successful even if the browser-open step fails.

## Browser Boundary

This slice must preserve the current browser/runtime boundary.

Allowed:

- use the existing `BrowserBackend`
- use the existing `Action::Navigate`
- use the existing `screen_html_export` `presentation.url`
- add a narrow compatibility path so local generated dashboard presentation can pass backend validation

Not allowed:

- change `browser-helper.html`
- introduce a new callback-host endpoint
- move file-opening responsibility into the frontend service console
- add a new browser-side bootstrap flow
- require websocket protocol changes

Important semantic note:

- on the current service callback-host path, dashboard open is expected to use `sgBrowerserOpenPage`, so the generated dashboard appears in a new visible browser tab while the helper page remains available for later tasks
- websocket-backed browser execution may continue to differ; this slice does not require matching visible-open semantics there

## Local Dashboard Presentation Allowance

The local dashboard browser-open path needs an explicit narrow validation rule because `file://...` cannot pass the normal domain allowlist.

Requirements for the narrow allowance:

- only approved for `Action::Navigate`
- only approved for the exact compat marker payload described above
- only approved for generated local dashboard presentation URLs
- only approved when the validated local artifact path points to the generated dashboard HTML artifact returned by the same `screen_html_export` success payload
- only approved for local HTML presentation, not arbitrary executables or unrelated local files
- ordinary remote navigation must continue using the existing `MacPolicy::validate(...)` domain rules unchanged

This keeps the behavior small and auditable while still satisfying the user-visible dashboard auto-open requirement.

## Local File Opening Boundary

The Excel auto-open action is a local runtime side effect, not a browser action.

Requirements:

- use the system default application for `.xlsx`
- support the current Windows environment first
- keep the implementation minimal and focused on the generated artifact path

Not required in this slice:

- a cross-platform abstraction beyond the minimal shape needed for the current repo environment
- opening arbitrary user-selected files
- exposing local file opening to the service websocket protocol

## Error Handling

### Excel route

If `.xlsx` generation fails:

- return the existing export failure

If `.xlsx` generation succeeds but auto-open fails:

- keep the artifact path in the summary
- mark only the auto-open step as failed
- do not delete the generated file

### Dashboard route

If `.html` generation fails:

- return the existing screen export failure

If `.html` generation succeeds but browser open fails:

- keep the artifact path in the summary
- mark only the browser-open step as failed
- do not delete the generated file

If the tool result is missing `presentation.url`:

- treat that as a protocol error in the post-export open step for this route
- keep the generated artifact path in the summary if it is available
- do not silently invent a different contract in the normal path

## Test Strategy

### Workflow tests

Update or add focused workflow coverage so that:

- Excel workflow still calls `openxml_office`
- dashboard workflow still calls `screen_html_export`
- the two routes remain mutually exclusive
- dashboard workflow consumes the tool's existing `presentation.url`

### New Excel post-export test

Add a focused regression proving:

- an Excel-oriented hotlist request triggers export
- the generated `.xlsx` path is passed into the local default-app opener
- no browser dashboard navigate is triggered for that route

### New dashboard post-export test

Add a focused regression proving:

- a dashboard-oriented hotlist request triggers HTML generation
- the generated tool payload `presentation.url` is used for browser open
- the browser backend receives a local-dashboard navigate request through the approved compat path
- no local spreadsheet opener is triggered for that route

### Backend/security compatibility tests

Add focused regressions proving:

- callback backend accepts the approved local-dashboard navigate case and still emits `sgBrowerserOpenPage`
- the narrow local-dashboard allowance rejects non-local or malformed URLs
- ordinary domain validation behavior remains unchanged for normal remote navigation

### Existing boundary tests remain unchanged

Do not change the service-console boundary guard. This slice is runtime behavior only.

## Acceptance Criteria

The slice is complete when all of the following are true:

1. Excel hotlist export still generates a local `.xlsx` artifact.
2. Excel hotlist export auto-opens that `.xlsx` with the system default spreadsheet application.
3. Dashboard hotlist export still generates a local `.html` artifact.
4. Dashboard hotlist export consumes the existing `screen_html_export` `presentation.url` and auto-opens it in the current callback-host-backed sgBrowser service session.
5. On the current callback-host service path, the dashboard opens automatically in a visible browser tab without breaking the helper-page runtime.
6. Excel-open and dashboard-open remain separate user-chosen flows, not a combined mode.
7. No new help/help-like user-visible surface is added.
8. The service console, websocket protocol, `browser-helper.html`, and callback-host endpoint surface remain untouched.
