This commit is contained in:
非法操作 2026-03-24 12:29:13 +08:00 committed by GitHub
commit 4f5f6447b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1657 additions and 0 deletions

View File

@ -23,9 +23,11 @@ from .app import (
conversation,
file,
file_preview,
human_input_form,
message,
site,
workflow,
workflow_events,
)
from .dataset import (
dataset,
@ -50,6 +52,7 @@ __all__ = [
"file",
"file_preview",
"hit_testing",
"human_input_form",
"index",
"message",
"metadata",
@ -58,6 +61,7 @@ __all__ = [
"segment",
"site",
"workflow",
"workflow_events",
]
api.add_namespace(service_api_ns)

View File

@ -0,0 +1,133 @@
"""
Service API human input form endpoints.
This module exposes app-token authenticated APIs for fetching and submitting
paused human input forms in workflow/chatflow runs.
"""
import json
import logging
from datetime import datetime
from typing import Any
from flask import Response
from flask_restx import Resource
from pydantic import BaseModel
from werkzeug.exceptions import InternalServerError, NotFound
from controllers.common.schema import register_schema_models
from controllers.service_api import service_api_ns
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
from extensions.ext_database import db
from models.model import App, EndUser
from services.human_input_service import Form, FormNotFoundError, HumanInputService
logger = logging.getLogger(__name__)
class HumanInputFormSubmitPayload(BaseModel):
inputs: dict[str, Any]
action: str
register_schema_models(service_api_ns, HumanInputFormSubmitPayload)
def _stringify_default_values(values: dict[str, object]) -> dict[str, str]:
result: dict[str, str] = {}
for key, value in values.items():
if value is None:
result[key] = ""
elif isinstance(value, (dict, list)):
result[key] = json.dumps(value, ensure_ascii=False)
else:
result[key] = str(value)
return result
def _to_timestamp(value: datetime) -> int:
return int(value.timestamp())
def _jsonify_form_definition(form: Form) -> Response:
definition_payload = form.get_definition().model_dump()
payload = {
"form_content": definition_payload["rendered_content"],
"inputs": definition_payload["inputs"],
"resolved_default_values": _stringify_default_values(definition_payload["default_values"]),
"user_actions": definition_payload["user_actions"],
"expiration_time": _to_timestamp(form.expiration_time),
}
return Response(json.dumps(payload, ensure_ascii=False), mimetype="application/json")
def _ensure_form_belongs_to_app(form: Form, app_model: App) -> None:
if form.app_id != app_model.id or form.tenant_id != app_model.tenant_id:
raise NotFound("Form not found")
@service_api_ns.route("/form/human_input/<string:form_token>")
class WorkflowHumanInputFormApi(Resource):
@service_api_ns.doc("get_human_input_form")
@service_api_ns.doc(description="Get a paused human input form by token")
@service_api_ns.doc(params={"form_token": "Human input form token"})
@service_api_ns.doc(
responses={
200: "Form retrieved successfully",
401: "Unauthorized - invalid API token",
404: "Form not found",
412: "Form already submitted or expired",
}
)
@validate_app_token
def get(self, app_model: App, form_token: str):
service = HumanInputService(db.engine)
form = service.get_form_by_token(form_token)
if form is None:
raise NotFound("Form not found")
_ensure_form_belongs_to_app(form, app_model)
service.ensure_form_active(form)
return _jsonify_form_definition(form)
@service_api_ns.expect(service_api_ns.models[HumanInputFormSubmitPayload.__name__])
@service_api_ns.doc("submit_human_input_form")
@service_api_ns.doc(description="Submit a paused human input form by token")
@service_api_ns.doc(params={"form_token": "Human input form token"})
@service_api_ns.doc(
responses={
200: "Form submitted successfully",
400: "Bad request - invalid submission data",
401: "Unauthorized - invalid API token",
404: "Form not found",
412: "Form already submitted or expired",
}
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True))
def post(self, app_model: App, end_user: EndUser, form_token: str):
payload = HumanInputFormSubmitPayload.model_validate(service_api_ns.payload or {})
service = HumanInputService(db.engine)
form = service.get_form_by_token(form_token)
if form is None:
raise NotFound("Form not found")
_ensure_form_belongs_to_app(form, app_model)
recipient_type = form.recipient_type
if recipient_type is None:
logger.warning("Recipient type is None for form, form_id=%s", form.id)
raise InternalServerError("Form recipient type is invalid")
try:
service.submit_form_by_token(
recipient_type=recipient_type,
form_token=form_token,
selected_action_id=payload.action,
form_data=payload.inputs,
submission_end_user_id=end_user.id,
)
except FormNotFoundError:
raise NotFound("Form not found")
return {}, 200

View File

@ -0,0 +1,125 @@
"""
Service API workflow resume event stream endpoints.
"""
import json
from collections.abc import Generator
from flask import Response, request
from flask_restx import Resource
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import NotFound
from controllers.service_api import service_api_ns
from controllers.service_api.app.error import NotWorkflowAppError
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
from core.app.apps.advanced_chat.app_generator import AdvancedChatAppGenerator
from core.app.apps.base_app_generator import BaseAppGenerator
from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter
from core.app.apps.message_generator import MessageGenerator
from core.app.apps.workflow.app_generator import WorkflowAppGenerator
from extensions.ext_database import db
from models.enums import CreatorUserRole
from models.model import App, AppMode, EndUser
from repositories.factory import DifyAPIRepositoryFactory
from services.workflow_event_snapshot_service import build_workflow_event_stream
@service_api_ns.route("/workflow/<string:task_id>/events")
class WorkflowEventsApi(Resource):
"""Service API for getting workflow execution events after resume."""
@service_api_ns.doc("get_workflow_events")
@service_api_ns.doc(description="Get workflow execution events stream after resume")
@service_api_ns.doc(
params={
"task_id": "Workflow run ID",
"user": "End user identifier (query param)",
"include_state_snapshot": "Whether to replay from persisted state snapshot",
}
)
@service_api_ns.doc(
responses={
200: "SSE event stream",
401: "Unauthorized - invalid API token",
404: "Workflow run not found",
}
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY, required=True))
def get(self, app_model: App, end_user: EndUser, task_id: str):
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.WORKFLOW, AppMode.ADVANCED_CHAT}:
raise NotWorkflowAppError()
session_maker = sessionmaker(db.engine)
repo = DifyAPIRepositoryFactory.create_api_workflow_run_repository(session_maker)
workflow_run = repo.get_workflow_run_by_id_and_tenant_id(
tenant_id=app_model.tenant_id,
run_id=task_id,
)
if workflow_run is None:
raise NotFound("Workflow run not found")
if workflow_run.app_id != app_model.id:
raise NotFound("Workflow run not found")
if workflow_run.created_by_role != CreatorUserRole.END_USER:
raise NotFound("Workflow run not found")
if workflow_run.created_by != end_user.id:
raise NotFound("Workflow run not found")
workflow_run_entity = workflow_run
if workflow_run_entity.finished_at is not None:
response = WorkflowResponseConverter.workflow_run_result_to_finish_response(
task_id=workflow_run_entity.id,
workflow_run=workflow_run_entity,
creator_user=end_user,
)
payload = response.model_dump(mode="json")
payload["event"] = response.event.value
def _generate_finished_events() -> Generator[str, None, None]:
yield f"data: {json.dumps(payload)}\n\n"
event_generator = _generate_finished_events
else:
msg_generator = MessageGenerator()
generator: BaseAppGenerator
if app_mode == AppMode.ADVANCED_CHAT:
generator = AdvancedChatAppGenerator()
elif app_mode == AppMode.WORKFLOW:
generator = WorkflowAppGenerator()
else:
raise NotWorkflowAppError()
include_state_snapshot = request.args.get("include_state_snapshot", "false").lower() == "true"
def _generate_stream_events():
if include_state_snapshot:
return generator.convert_to_event_stream(
build_workflow_event_stream(
app_mode=app_mode,
workflow_run=workflow_run_entity,
tenant_id=app_model.tenant_id,
app_id=app_model.id,
session_maker=session_maker,
)
)
return generator.convert_to_event_stream(
msg_generator.retrieve_events(app_mode, workflow_run_entity.id),
)
event_generator = _generate_stream_events
return Response(
event_generator(),
mimetype="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
)

View File

@ -0,0 +1,111 @@
"""Unit tests for Service API human input form endpoints."""
from __future__ import annotations
import json
import sys
from datetime import UTC, datetime
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from werkzeug.exceptions import NotFound
from controllers.service_api.app.human_input_form import WorkflowHumanInputFormApi
from models.human_input import RecipientType
from tests.unit_tests.controllers.service_api.conftest import _unwrap
class TestWorkflowHumanInputFormApi:
def test_get_success(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
definition = SimpleNamespace(
model_dump=lambda: {
"rendered_content": "Rendered form content",
"inputs": [{"output_variable_name": "name"}],
"default_values": {"name": "Alice", "age": 30, "meta": {"k": "v"}},
"user_actions": [{"id": "approve", "title": "Approve"}],
}
)
form = SimpleNamespace(
app_id="app-1",
tenant_id="tenant-1",
expiration_time=datetime(2099, 1, 1, tzinfo=UTC),
get_definition=lambda: definition,
)
service_mock = Mock()
service_mock.get_form_by_token.return_value = form
workflow_module = sys.modules["controllers.service_api.app.human_input_form"]
monkeypatch.setattr(workflow_module, "HumanInputService", lambda _engine: service_mock)
monkeypatch.setattr(workflow_module, "db", SimpleNamespace(engine=object()))
api = WorkflowHumanInputFormApi()
handler = _unwrap(api.get)
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1")
with app.test_request_context("/form/human_input/token-1", method="GET"):
response = handler(api, app_model=app_model, form_token="token-1")
payload = json.loads(response.get_data(as_text=True))
assert payload == {
"form_content": "Rendered form content",
"inputs": [{"output_variable_name": "name"}],
"resolved_default_values": {"name": "Alice", "age": "30", "meta": '{"k": "v"}'},
"user_actions": [{"id": "approve", "title": "Approve"}],
"expiration_time": int(form.expiration_time.timestamp()),
}
service_mock.get_form_by_token.assert_called_once_with("token-1")
service_mock.ensure_form_active.assert_called_once_with(form)
def test_get_form_not_in_app(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
form = SimpleNamespace(
app_id="another-app",
tenant_id="tenant-1",
expiration_time=datetime(2099, 1, 1, tzinfo=UTC),
)
service_mock = Mock()
service_mock.get_form_by_token.return_value = form
workflow_module = sys.modules["controllers.service_api.app.human_input_form"]
monkeypatch.setattr(workflow_module, "HumanInputService", lambda _engine: service_mock)
monkeypatch.setattr(workflow_module, "db", SimpleNamespace(engine=object()))
api = WorkflowHumanInputFormApi()
handler = _unwrap(api.get)
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1")
with app.test_request_context("/form/human_input/token-1", method="GET"):
with pytest.raises(NotFound):
handler(api, app_model=app_model, form_token="token-1")
def test_post_success(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
form = SimpleNamespace(
app_id="app-1",
tenant_id="tenant-1",
recipient_type=RecipientType.BACKSTAGE,
)
service_mock = Mock()
service_mock.get_form_by_token.return_value = form
workflow_module = sys.modules["controllers.service_api.app.human_input_form"]
monkeypatch.setattr(workflow_module, "HumanInputService", lambda _engine: service_mock)
monkeypatch.setattr(workflow_module, "db", SimpleNamespace(engine=object()))
api = WorkflowHumanInputFormApi()
handler = _unwrap(api.post)
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1")
end_user = SimpleNamespace(id="end-user-1")
with app.test_request_context(
"/form/human_input/token-1",
method="POST",
json={"inputs": {"name": "Alice"}, "action": "approve", "user": "external-1"},
):
response, status = handler(api, app_model=app_model, end_user=end_user, form_token="token-1")
assert response == {}
assert status == 200
service_mock.submit_form_by_token.assert_called_once_with(
recipient_type=RecipientType.BACKSTAGE,
form_token="token-1",
selected_action_id="approve",
form_data={"name": "Alice"},
submission_end_user_id="end-user-1",
)

View File

@ -0,0 +1,162 @@
"""Unit tests for Service API workflow event stream endpoints."""
from __future__ import annotations
import json
import sys
from datetime import UTC, datetime
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from werkzeug.exceptions import NotFound
from controllers.service_api.app.error import NotWorkflowAppError
from controllers.service_api.app.workflow_events import WorkflowEventsApi
from models.enums import CreatorUserRole
from models.model import AppMode
from tests.unit_tests.controllers.service_api.conftest import _unwrap
def _mock_repo_for_run(monkeypatch: pytest.MonkeyPatch, workflow_run):
workflow_events_module = sys.modules["controllers.service_api.app.workflow_events"]
repo = SimpleNamespace(get_workflow_run_by_id_and_tenant_id=lambda **_kwargs: workflow_run)
monkeypatch.setattr(
workflow_events_module.DifyAPIRepositoryFactory,
"create_api_workflow_run_repository",
lambda *_args, **_kwargs: repo,
)
monkeypatch.setattr(workflow_events_module, "db", SimpleNamespace(engine=object()))
return workflow_events_module
class TestWorkflowEventsApi:
def test_wrong_app_mode(self, app) -> None:
api = WorkflowEventsApi()
handler = _unwrap(api.get)
app_model = SimpleNamespace(mode=AppMode.CHAT.value)
end_user = SimpleNamespace(id="end-user-1")
with app.test_request_context("/workflow/run-1/events?user=u1", method="GET"):
with pytest.raises(NotWorkflowAppError):
handler(api, app_model=app_model, end_user=end_user, task_id="run-1")
def test_workflow_run_not_found(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
_mock_repo_for_run(monkeypatch, workflow_run=None)
api = WorkflowEventsApi()
handler = _unwrap(api.get)
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW.value)
end_user = SimpleNamespace(id="end-user-1")
with app.test_request_context("/workflow/run-1/events?user=u1", method="GET"):
with pytest.raises(NotFound):
handler(api, app_model=app_model, end_user=end_user, task_id="run-1")
def test_workflow_run_permission_denied(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
workflow_run = SimpleNamespace(
id="run-1",
app_id="app-1",
created_by_role=CreatorUserRole.ACCOUNT,
created_by="another-user",
finished_at=None,
)
_mock_repo_for_run(monkeypatch, workflow_run=workflow_run)
api = WorkflowEventsApi()
handler = _unwrap(api.get)
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW.value)
end_user = SimpleNamespace(id="end-user-1")
with app.test_request_context("/workflow/run-1/events?user=u1", method="GET"):
with pytest.raises(NotFound):
handler(api, app_model=app_model, end_user=end_user, task_id="run-1")
def test_finished_run_returns_sse(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
workflow_run = SimpleNamespace(
id="run-1",
app_id="app-1",
created_by_role=CreatorUserRole.END_USER,
created_by="end-user-1",
finished_at=datetime(2099, 1, 1, tzinfo=UTC),
)
workflow_events_module = _mock_repo_for_run(monkeypatch, workflow_run=workflow_run)
monkeypatch.setattr(
workflow_events_module.WorkflowResponseConverter,
"workflow_run_result_to_finish_response",
lambda **_kwargs: SimpleNamespace(
model_dump=lambda mode="json": {"task_id": "run-1", "status": "succeeded"},
event=SimpleNamespace(value="workflow_finished"),
),
)
api = WorkflowEventsApi()
handler = _unwrap(api.get)
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW.value)
end_user = SimpleNamespace(id="end-user-1")
with app.test_request_context("/workflow/run-1/events?user=u1", method="GET"):
response = handler(api, app_model=app_model, end_user=end_user, task_id="run-1")
assert response.mimetype == "text/event-stream"
body = response.get_data(as_text=True).strip()
assert body.startswith("data: ")
payload = json.loads(body[len("data: ") :])
assert payload["task_id"] == "run-1"
assert payload["event"] == "workflow_finished"
def test_running_run_streams_events(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
workflow_run = SimpleNamespace(
id="run-1",
app_id="app-1",
created_by_role=CreatorUserRole.END_USER,
created_by="end-user-1",
finished_at=None,
)
workflow_events_module = _mock_repo_for_run(monkeypatch, workflow_run=workflow_run)
msg_generator = Mock()
msg_generator.retrieve_events.return_value = ["raw-event"]
workflow_generator = Mock()
workflow_generator.convert_to_event_stream.return_value = iter(["data: streamed\n\n"])
monkeypatch.setattr(workflow_events_module, "MessageGenerator", lambda: msg_generator)
monkeypatch.setattr(workflow_events_module, "WorkflowAppGenerator", lambda: workflow_generator)
api = WorkflowEventsApi()
handler = _unwrap(api.get)
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW.value)
end_user = SimpleNamespace(id="end-user-1")
with app.test_request_context("/workflow/run-1/events?user=u1", method="GET"):
response = handler(api, app_model=app_model, end_user=end_user, task_id="run-1")
assert response.get_data(as_text=True) == "data: streamed\n\n"
msg_generator.retrieve_events.assert_called_once_with(AppMode.WORKFLOW, "run-1")
workflow_generator.convert_to_event_stream.assert_called_once_with(["raw-event"])
def test_running_run_with_snapshot(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
workflow_run = SimpleNamespace(
id="run-1",
app_id="app-1",
created_by_role=CreatorUserRole.END_USER,
created_by="end-user-1",
finished_at=None,
)
workflow_events_module = _mock_repo_for_run(monkeypatch, workflow_run=workflow_run)
msg_generator = Mock()
workflow_generator = Mock()
workflow_generator.convert_to_event_stream.return_value = iter(["data: snapshot\n\n"])
snapshot_builder = Mock(return_value=["snapshot-events"])
monkeypatch.setattr(workflow_events_module, "MessageGenerator", lambda: msg_generator)
monkeypatch.setattr(workflow_events_module, "WorkflowAppGenerator", lambda: workflow_generator)
monkeypatch.setattr(workflow_events_module, "build_workflow_event_stream", snapshot_builder)
api = WorkflowEventsApi()
handler = _unwrap(api.get)
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW.value)
end_user = SimpleNamespace(id="end-user-1")
with app.test_request_context("/workflow/run-1/events?user=u1&include_state_snapshot=true", method="GET"):
response = handler(api, app_model=app_model, end_user=end_user, task_id="run-1")
assert response.get_data(as_text=True) == "data: snapshot\n\n"
msg_generator.retrieve_events.assert_not_called()
snapshot_builder.assert_called_once()
workflow_generator.convert_to_event_stream.assert_called_once_with(["snapshot-events"])

View File

@ -191,6 +191,24 @@ Chat applications support session persistence, allowing previous chat history to
- `total_price` (decimal) optional Total cost
- `currency` (string) optional e.g. `USD` / `RMB`
- `created_at` (timestamp) timestamp of start, e.g., 1705395332
- `event: human_input_required` Workflow paused and requires Human-in-the-Loop input
- `task_id` (string) Task ID, used for request tracking
- `workflow_run_id` (string) Unique ID of workflow execution
- `event` (string) fixed to `human_input_required`
- `data` (object) detail
- `form_id` (string) Human input form ID
- `node_id` (string) Human input node ID
- `node_title` (string) Human input node title
- `form_content` (string) Rendered form content
- `inputs` (array[object]) Input field definitions
- `actions` (array[object]) User action buttons
- `id` (string) Action ID
- `title` (string) Button text
- `button_style` (string) Button style
- `display_in_ui` (bool) Whether this form should be shown in UI
- `form_token` (string) Token used by `/form/human_input/:form_token` APIs
- `resolved_default_values` (object) Runtime-resolved default values
- `expiration_time` (timestamp) Form expiration time (Unix seconds)
- `event: workflow_finished` workflow execution ends, success or failure in different states in the same event
- `task_id` (string) Task ID, used for request tracking and the below Stop Generate API
- `workflow_run_id` (string) Unique ID of workflow execution
@ -578,6 +596,166 @@ Chat applications support session persistence, allowing previous chat history to
---
<Heading
url='/form/human_input/:form_token'
method='GET'
title='Get Human Input Form'
name='#get-human-input-form'
/>
<Row>
<Col>
Retrieve a pending Human-in-the-Loop form by `form_token`.
Use this endpoint when streaming returns `human_input_required` with a `form_token`.
### Path
- `form_token` (string) Required, token returned by the pause event.
### Response
- `form_content` (string) Rendered form content (markdown/plain text)
- `inputs` (array[object]) Form input definitions
- `resolved_default_values` (object) Default values resolved to strings
- `user_actions` (array[object]) Action buttons
- `expiration_time` (timestamp) Form expiration time (Unix seconds)
### Errors
- 404, form not found or does not belong to current app
- 412, `human_input_form_submitted`, form already submitted
- 412, `human_input_form_expired`, form expired
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/form/human_input/:form_token"
targetCode={`curl -X GET '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
--header 'Authorization: Bearer {api_key}'`}
/>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"form_content": "Please confirm the final answer: {{#$output.answer#}}",
"inputs": [
{
"label": "Answer",
"type": "text-input",
"required": true,
"output_variable_name": "answer"
}
],
"resolved_default_values": {
"answer": "Initial value"
},
"user_actions": [
{ "id": "approve", "title": "Approve", "button_style": "primary" },
{ "id": "reject", "title": "Reject", "button_style": "warning" }
],
"expiration_time": 1735689600
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/form/human_input/:form_token'
method='POST'
title='Submit Human Input Form'
name='#submit-human-input-form'
/>
<Row>
<Col>
Submit a pending Human-in-the-Loop form.
### Path
- `form_token` (string) Required, token returned by the pause event.
### Request Body
- `inputs` (object) Required, key/value pairs for form fields.
- `action` (string) Required, selected action ID from `user_actions`.
- `user` (string) Required, end-user identifier.
### Response
Returns an empty object on success.
### Errors
- 400, `invalid_form_data`, submitted data does not match the form schema
- 404, form not found or does not belong to current app
- 412, `human_input_form_submitted`, form already submitted
- 412, `human_input_form_expired`, form expired
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/form/human_input/:form_token"
targetCode={`curl -X POST '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
--header 'Authorization: Bearer {api_key}' \\
--header 'Content-Type: application/json' \\
--data-raw '{
"inputs": {"answer": "Approved answer"},
"action": "approve",
"user": "abc-123"
}'`}
/>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/workflow/:task_id/events'
method='GET'
title='Get Workflow Resume Events'
name='#get-workflow-resume-events'
/>
<Row>
<Col>
Continue receiving workflow events after submitting a human input form.
This endpoint returns `text/event-stream` and can be used to observe resumed execution until completion.
### Path
- `task_id` (string) Required, workflow run ID (`workflow_run_id`).
### Query
- `user` (string) Required, end-user identifier.
- `include_state_snapshot` (bool) Optional, set to `true` to replay from persisted state snapshot before continuing with live events.
### Response
Server-Sent Events stream (`text/event-stream`).
Typical events include `node_started`, `node_finished`, `human_input_form_filled`, `human_input_form_timeout`, and `workflow_finished`.
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/workflow/:task_id/events"
targetCode={`curl -N -X GET '${props.appDetail.api_base_url}/workflow/{workflow_run_id}/events?user=abc-123' \\
--header 'Authorization: Bearer {api_key}'`}
/>
<CodeGroup title="Response">
```streaming {{ title: 'Response' }}
data: {"event":"human_input_form_filled","task_id":"run-1","workflow_run_id":"run-1","data":{"node_id":"human_input_1","node_title":"Human Input","rendered_content":"Approved answer","action_id":"approve","action_text":"Approve"}}
data: {"event":"node_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"node-execution-id","node_id":"llm_1","node_type":"llm","title":"LLM","index":5,"status":"succeeded","created_at":1735689601}}
data: {"event":"workflow_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"run-1","workflow_id":"workflow-id","status":"succeeded","outputs":{"text":"Done"},"created_at":1735689590,"finished_at":1735689602,"elapsed_time":12.0,"total_tokens":1000,"total_steps":6}}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/messages/:message_id/feedbacks'
method='POST'

View File

@ -191,6 +191,24 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
- `total_price` (decimal) オプションの合計コスト
- `currency` (string) オプション、例:`USD` / `RMB`
- `created_at` (timestamp) 開始のタイムスタンプ、例1705395332
- `event: human_input_required` ワークフローが一時停止し、Human-in-the-Loop 入力が必要
- `task_id` (string) タスク ID、リクエスト追跡に使用
- `workflow_run_id` (string) ワークフロー実行の一意 ID
- `event` (string) `human_input_required`に固定
- `data` (object) 詳細
- `form_id` (string) ヒューマン入力フォーム ID
- `node_id` (string) Human Input ノード ID
- `node_title` (string) Human Input ノードタイトル
- `form_content` (string) レンダリング済みフォーム内容
- `inputs` (array[object]) フォーム入力項目の定義
- `actions` (array[object]) ユーザーが選択できるアクションボタン
- `id` (string) アクション ID
- `title` (string) ボタンラベル
- `button_style` (string) ボタンスタイル
- `display_in_ui` (bool) UI にこのフォームを表示するかどうか
- `form_token` (string) `/form/human_input/:form_token` API で使用するトークン
- `resolved_default_values` (object) 実行時に解決されたデフォルト値
- `expiration_time` (timestamp) フォームの有効期限Unix 秒)
- `event: workflow_finished` ワークフロー実行が終了、成功または失敗は同じイベント内で異なる状態で示されます
- `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用
- `workflow_run_id` (string) ワークフロー実行の一意ID
@ -579,6 +597,166 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
---
<Heading
url='/form/human_input/:form_token'
method='GET'
title='Human Input フォームを取得'
name='#get-human-input-form'
/>
<Row>
<Col>
`form_token` から保留中の Human-in-the-Loop フォームを取得します。
ストリーミングイベントで `human_input_required``form_token` を含む)が返された際に使用します。
### パス
- `form_token` (string) 必須、一時停止イベントで返されたフォームトークン
### 応答
- `form_content` (string) レンダリング済みフォーム内容markdown/plain text
- `inputs` (array[object]) 入力項目定義
- `resolved_default_values` (object) 解決済みデフォルト値(文字列)
- `user_actions` (array[object]) アクションボタン一覧
- `expiration_time` (timestamp) フォーム有効期限Unix 秒)
### エラー
- 404, フォームが存在しない、または現在のアプリに属していない
- 412, `human_input_form_submitted`, 既に送信済み
- 412, `human_input_form_expired`, 期限切れ
</Col>
<Col sticky>
<CodeGroup
title="リクエスト"
tag="GET"
label="/form/human_input/:form_token"
targetCode={`curl -X GET '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
--header 'Authorization: Bearer {api_key}'`}
/>
<CodeGroup title="応答">
```json {{ title: '応答' }}
{
"form_content": "最終回答を確認してください: {{#$output.answer#}}",
"inputs": [
{
"label": "回答",
"type": "text-input",
"required": true,
"output_variable_name": "answer"
}
],
"resolved_default_values": {
"answer": "初期値"
},
"user_actions": [
{ "id": "approve", "title": "承認", "button_style": "primary" },
{ "id": "reject", "title": "却下", "button_style": "warning" }
],
"expiration_time": 1735689600
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/form/human_input/:form_token'
method='POST'
title='Human Input フォームを送信'
name='#submit-human-input-form'
/>
<Row>
<Col>
保留中の Human-in-the-Loop フォームを送信します。
### パス
- `form_token` (string) 必須、一時停止イベントで返されたフォームトークン
### リクエストボディ
- `inputs` (object) 必須、フォーム項目の key/value
- `action` (string) 必須、`user_actions` から選択したアクション ID
- `user` (string) 必須、エンドユーザー識別子
### 応答
成功時は空オブジェクトを返します。
### エラー
- 400, `invalid_form_data`, 送信データがフォームスキーマに一致しない
- 404, フォームが存在しない、または現在のアプリに属していない
- 412, `human_input_form_submitted`, 既に送信済み
- 412, `human_input_form_expired`, 期限切れ
</Col>
<Col sticky>
<CodeGroup
title="リクエスト"
tag="POST"
label="/form/human_input/:form_token"
targetCode={`curl -X POST '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
--header 'Authorization: Bearer {api_key}' \\
--header 'Content-Type: application/json' \\
--data-raw '{
"inputs": {"answer": "承認済み回答"},
"action": "approve",
"user": "abc-123"
}'`}
/>
<CodeGroup title="応答">
```json {{ title: '応答' }}
{}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/workflow/:task_id/events'
method='GET'
title='再開後の Workflow イベントを取得'
name='#get-workflow-resume-events'
/>
<Row>
<Col>
Human Input フォーム送信後に、ワークフロー再開後のイベントを継続受信します。
このエンドポイントは `text/event-stream` を返し、完了までイベントを購読できます。
### パス
- `task_id` (string) 必須、workflow 実行 ID`workflow_run_id`
### クエリ
- `user` (string) 必須、エンドユーザー識別子
- `include_state_snapshot` (bool) 任意、`true` の場合は永続化済み状態スナップショットを先に再生してからリアルタイムイベントへ移行
### 応答
Server-Sent Events ストリーム(`text/event-stream`)。
主なイベントは `node_started`、`node_finished`、`human_input_form_filled`、`human_input_form_timeout`、`workflow_finished` です。
</Col>
<Col sticky>
<CodeGroup
title="リクエスト"
tag="GET"
label="/workflow/:task_id/events"
targetCode={`curl -N -X GET '${props.appDetail.api_base_url}/workflow/{workflow_run_id}/events?user=abc-123' \\
--header 'Authorization: Bearer {api_key}'`}
/>
<CodeGroup title="応答">
```streaming {{ title: '応答' }}
data: {"event":"human_input_form_filled","task_id":"run-1","workflow_run_id":"run-1","data":{"node_id":"human_input_1","node_title":"Human Input","rendered_content":"承認済み回答","action_id":"approve","action_text":"承認"}}
data: {"event":"node_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"node-execution-id","node_id":"llm_1","node_type":"llm","title":"LLM","index":5,"status":"succeeded","created_at":1735689601}}
data: {"event":"workflow_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"run-1","workflow_id":"workflow-id","status":"succeeded","outputs":{"text":"Done"},"created_at":1735689590,"finished_at":1735689602,"elapsed_time":12.0,"total_tokens":1000,"total_steps":6}}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/messages/:message_id/feedbacks'
method='POST'

View File

@ -189,6 +189,24 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
- `total_price` (decimal) optional 总费用
- `currency` (string) optional 货币,如 `USD` / `RMB`
- `created_at` (timestamp) 开始时间
- `event: human_input_required` Workflow 已暂停,等待 Human-in-the-Loop 输入
- `task_id` (string) 任务 ID用于请求跟踪
- `workflow_run_id` (string) workflow 执行 ID
- `event` (string) 固定为 `human_input_required`
- `data` (object) 详细内容
- `form_id` (string) 人工输入表单 ID
- `node_id` (string) Human Input 节点 ID
- `node_title` (string) Human Input 节点标题
- `form_content` (string) 渲染后的表单内容
- `inputs` (array[object]) 表单输入项定义
- `actions` (array[object]) 用户可选动作按钮
- `id` (string) 动作 ID
- `title` (string) 按钮文案
- `button_style` (string) 按钮样式
- `display_in_ui` (bool) 是否需要在 UI 展示该表单
- `form_token` (string) 用于 `/form/human_input/:form_token` 接口的令牌
- `resolved_default_values` (object) 运行时解析后的默认值
- `expiration_time` (timestamp) 表单过期时间Unix 秒级时间戳)
- `event: workflow_finished` workflow 执行结束,成功失败同一事件中不同状态
- `task_id` (string) 任务 ID用于请求跟踪和下方的停止响应接口
- `workflow_run_id` (string) workflow 执行 ID
@ -572,6 +590,166 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
</Row>
---
<Heading
url='/form/human_input/:form_token'
method='GET'
title='获取人工输入表单'
name='#get-human-input-form'
/>
<Row>
<Col>
通过 `form_token` 获取待处理的 Human-in-the-Loop 表单。
当流式事件返回 `human_input_required`(包含 `form_token`)时,可调用此接口拉取表单详情。
### Path
- `form_token` (string) 必填,暂停事件返回的表单 token
### Response
- `form_content` (string) 已渲染的表单内容markdown/plain text
- `inputs` (array[object]) 表单输入项定义
- `resolved_default_values` (object) 已解析的默认值(字符串)
- `user_actions` (array[object]) 操作按钮列表
- `expiration_time` (timestamp) 表单过期时间Unix 秒)
### Errors
- 404表单不存在或不属于当前应用
- 412`human_input_form_submitted`,表单已被提交
- 412`human_input_form_expired`,表单已过期
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/form/human_input/:form_token"
targetCode={`curl -X GET '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
--header 'Authorization: Bearer {api_key}'`}
/>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"form_content": "请确认最终结果:{{#$output.answer#}}",
"inputs": [
{
"label": "答案",
"type": "text-input",
"required": true,
"output_variable_name": "answer"
}
],
"resolved_default_values": {
"answer": "初始值"
},
"user_actions": [
{ "id": "approve", "title": "通过", "button_style": "primary" },
{ "id": "reject", "title": "拒绝", "button_style": "warning" }
],
"expiration_time": 1735689600
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/form/human_input/:form_token'
method='POST'
title='提交人工输入表单'
name='#submit-human-input-form'
/>
<Row>
<Col>
提交待处理的 Human-in-the-Loop 表单。
### Path
- `form_token` (string) 必填,暂停事件返回的表单 token
### Request Body
- `inputs` (object) 必填,表单字段的 key/value
- `action` (string) 必填,从 `user_actions` 中选择的动作 ID
- `user` (string) 必填,终端用户标识
### Response
成功时返回空对象。
### Errors
- 400`invalid_form_data`,提交数据与表单 schema 不匹配
- 404表单不存在或不属于当前应用
- 412`human_input_form_submitted`,表单已被提交
- 412`human_input_form_expired`,表单已过期
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/form/human_input/:form_token"
targetCode={`curl -X POST '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
--header 'Authorization: Bearer {api_key}' \\
--header 'Content-Type: application/json' \\
--data-raw '{
"inputs": {"answer": "已确认答案"},
"action": "approve",
"user": "abc-123"
}'`}
/>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/workflow/:task_id/events'
method='GET'
title='获取恢复后的 Workflow 事件流'
name='#get-workflow-resume-events'
/>
<Row>
<Col>
在提交人工输入表单后,继续订阅工作流后续执行事件。
返回 `text/event-stream`,可持续接收直到工作流结束。
### Path
- `task_id` (string) 必填workflow 运行 ID`workflow_run_id`
### Query
- `user` (string) 必填,终端用户标识
- `include_state_snapshot` (bool) 可选,设为 `true` 时会先回放持久化状态快照,再继续实时事件
### Response
Server-Sent Events 流(`text/event-stream`)。
常见事件包括 `node_started`、`node_finished`、`human_input_form_filled`、`human_input_form_timeout`、`workflow_finished`。
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/workflow/:task_id/events"
targetCode={`curl -N -X GET '${props.appDetail.api_base_url}/workflow/{workflow_run_id}/events?user=abc-123' \\
--header 'Authorization: Bearer {api_key}'`}
/>
<CodeGroup title="Response">
```streaming {{ title: 'Response' }}
data: {"event":"human_input_form_filled","task_id":"run-1","workflow_run_id":"run-1","data":{"node_id":"human_input_1","node_title":"Human Input","rendered_content":"已确认答案","action_id":"approve","action_text":"通过"}}
data: {"event":"node_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"node-execution-id","node_id":"llm_1","node_type":"llm","title":"LLM","index":5,"status":"succeeded","created_at":1735689601}}
data: {"event":"workflow_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"run-1","workflow_id":"workflow-id","status":"succeeded","outputs":{"text":"Done"},"created_at":1735689590,"finished_at":1735689602,"elapsed_time":12.0,"total_tokens":1000,"total_steps":6}}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/messages/:message_id/feedbacks'
method='POST'

View File

@ -146,6 +146,24 @@ Workflow applications offers non-session support and is ideal for translation, a
- `total_price` (decimal) optional Total cost
- `currency` (string) optional e.g. `USD` / `RMB`
- `created_at` (timestamp) timestamp of start, e.g., 1705395332
- `event: human_input_required` Workflow paused and requires Human-in-the-Loop input
- `task_id` (string) Task ID, used for request tracking
- `workflow_run_id` (string) Unique ID of workflow execution
- `event` (string) fixed to `human_input_required`
- `data` (object) detail
- `form_id` (string) Human input form ID
- `node_id` (string) Human input node ID
- `node_title` (string) Human input node title
- `form_content` (string) Rendered form content
- `inputs` (array[object]) Input field definitions
- `actions` (array[object]) User action buttons
- `id` (string) Action ID
- `title` (string) Button text
- `button_style` (string) Button style
- `display_in_ui` (bool) Whether this form should be shown in UI
- `form_token` (string) Token used by `/form/human_input/:form_token` APIs
- `resolved_default_values` (object) Runtime-resolved default values
- `expiration_time` (timestamp) Form expiration time (Unix seconds)
- `event: workflow_finished` workflow execution ends, success or failure in different states in the same event
- `task_id` (string) Task ID, used for request tracking and the below Stop Generate API
- `workflow_run_id` (string) Unique ID of workflow execution
@ -457,6 +475,24 @@ Workflow applications offers non-session support and is ideal for translation, a
- `total_price` (decimal) optional total cost
- `currency` (string) optional currency, such as `USD` / `RMB`
- `created_at` (timestamp) timestamp of start, e.g., 1705395332
- `event: human_input_required` Workflow paused and requires Human-in-the-Loop input
- `task_id` (string) Task ID, used for request tracking
- `workflow_run_id` (string) Unique ID of workflow execution
- `event` (string) fixed to `human_input_required`
- `data` (object) detail
- `form_id` (string) Human input form ID
- `node_id` (string) Human input node ID
- `node_title` (string) Human input node title
- `form_content` (string) Rendered form content
- `inputs` (array[object]) Input field definitions
- `actions` (array[object]) User action buttons
- `id` (string) Action ID
- `title` (string) Button text
- `button_style` (string) Button style
- `display_in_ui` (bool) Whether this form should be shown in UI
- `form_token` (string) Token used by `/form/human_input/:form_token` APIs
- `resolved_default_values` (object) Runtime-resolved default values
- `expiration_time` (timestamp) Form expiration time (Unix seconds)
- `event: workflow_finished` workflow execution finished, success and failure are different states in the same event
- `task_id` (string) Task ID, used for request tracking and the below Stop Generate API
- `workflow_run_id` (string) Unique ID of workflow execution
@ -666,6 +702,166 @@ Workflow applications offers non-session support and is ideal for translation, a
---
<Heading
url='/form/human_input/:form_token'
method='GET'
title='Get Human Input Form'
name='#get-human-input-form'
/>
<Row>
<Col>
Retrieve a pending Human-in-the-Loop form by `form_token`.
Use this endpoint when a workflow pauses with `human_input_required` and returns a `form_token`.
### Path
- `form_token` (string) Required, token returned by the pause event.
### Response
- `form_content` (string) Rendered form content (markdown/plain text)
- `inputs` (array[object]) Form input definitions
- `resolved_default_values` (object) Default values resolved to strings
- `user_actions` (array[object]) Action buttons
- `expiration_time` (timestamp) Form expiration time (Unix seconds)
### Errors
- 404, form not found or does not belong to current app
- 412, `human_input_form_submitted`, form already submitted
- 412, `human_input_form_expired`, form expired
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/form/human_input/:form_token"
targetCode={`curl -X GET '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
--header 'Authorization: Bearer {api_key}'`}
/>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"form_content": "Please confirm the final answer: {{#$output.answer#}}",
"inputs": [
{
"label": "Answer",
"type": "text-input",
"required": true,
"output_variable_name": "answer"
}
],
"resolved_default_values": {
"answer": "Initial value"
},
"user_actions": [
{ "id": "approve", "title": "Approve", "button_style": "primary" },
{ "id": "reject", "title": "Reject", "button_style": "warning" }
],
"expiration_time": 1735689600
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/form/human_input/:form_token'
method='POST'
title='Submit Human Input Form'
name='#submit-human-input-form'
/>
<Row>
<Col>
Submit a pending Human-in-the-Loop form.
### Path
- `form_token` (string) Required, token returned by the pause event.
### Request Body
- `inputs` (object) Required, key/value pairs for form fields.
- `action` (string) Required, selected action ID from `user_actions`.
- `user` (string) Required, end-user identifier.
### Response
Returns an empty object on success.
### Errors
- 400, `invalid_form_data`, submitted data does not match the form schema
- 404, form not found or does not belong to current app
- 412, `human_input_form_submitted`, form already submitted
- 412, `human_input_form_expired`, form expired
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/form/human_input/:form_token"
targetCode={`curl -X POST '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
--header 'Authorization: Bearer {api_key}' \\
--header 'Content-Type: application/json' \\
--data-raw '{
"inputs": {"answer": "Approved answer"},
"action": "approve",
"user": "abc-123"
}'`}
/>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/workflow/:task_id/events'
method='GET'
title='Get Workflow Resume Events'
name='#get-workflow-resume-events'
/>
<Row>
<Col>
Continue receiving workflow events after submitting a human input form.
This endpoint returns `text/event-stream` and can be used to observe the resumed run until completion.
### Path
- `task_id` (string) Required, workflow run ID (`workflow_run_id`).
### Query
- `user` (string) Required, end-user identifier.
- `include_state_snapshot` (bool) Optional, set to `true` to replay from persisted state snapshot before continuing with live events.
### Response
Server-Sent Events stream (`text/event-stream`).
Typical events include `node_started`, `node_finished`, `human_input_form_filled`, `human_input_form_timeout`, and `workflow_finished`.
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/workflow/:task_id/events"
targetCode={`curl -N -X GET '${props.appDetail.api_base_url}/workflow/{workflow_run_id}/events?user=abc-123' \\
--header 'Authorization: Bearer {api_key}'`}
/>
<CodeGroup title="Response">
```streaming {{ title: 'Response' }}
data: {"event":"human_input_form_filled","task_id":"run-1","workflow_run_id":"run-1","data":{"node_id":"human_input_1","node_title":"Human Input","rendered_content":"Approved answer","action_id":"approve","action_text":"Approve"}}
data: {"event":"node_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"node-execution-id","node_id":"llm_1","node_type":"llm","title":"LLM","index":5,"status":"succeeded","created_at":1735689601}}
data: {"event":"workflow_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"run-1","workflow_id":"workflow-id","status":"succeeded","outputs":{"text":"Done"},"created_at":1735689590,"finished_at":1735689602,"elapsed_time":12.0,"total_tokens":1000,"total_steps":6}}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/files/upload'
method='POST'

View File

@ -146,6 +146,24 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
- `total_price` (decimal) オプションの総コスト
- `currency` (string) オプション 例:`USD` / `RMB`
- `created_at` (timestamp) 開始のタイムスタンプ、例1705395332
- `event: human_input_required` ワークフローが一時停止し、Human-in-the-Loop 入力が必要
- `task_id` (string) タスク ID、リクエスト追跡に使用
- `workflow_run_id` (string) ワークフロー実行の一意の ID
- `event` (string) `human_input_required`に固定
- `data` (object) 詳細
- `form_id` (string) ヒューマン入力フォーム ID
- `node_id` (string) Human Input ノード ID
- `node_title` (string) Human Input ノードタイトル
- `form_content` (string) レンダリング済みフォーム内容
- `inputs` (array[object]) フォーム入力項目の定義
- `actions` (array[object]) ユーザーが選択できるアクションボタン
- `id` (string) アクション ID
- `title` (string) ボタンラベル
- `button_style` (string) ボタンスタイル
- `display_in_ui` (bool) UI にこのフォームを表示するかどうか
- `form_token` (string) `/form/human_input/:form_token` API で使用するトークン
- `resolved_default_values` (object) 実行時に解決されたデフォルト値
- `expiration_time` (timestamp) フォームの有効期限Unix 秒)
- `event: workflow_finished` ワークフロー実行終了、同じイベントで異なる状態で成功または失敗
- `task_id` (string) タスク ID、リクエスト追跡と以下の Stop Generate API に使用
- `workflow_run_id` (string) ワークフロー実行の一意の ID
@ -452,6 +470,24 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
- `total_price` (decimal) オプション 総費用
- `currency` (string) オプション 通貨、例:`USD` / `RMB`
- `created_at` (timestamp) 開始時間
- `event: human_input_required` ワークフローが一時停止し、Human-in-the-Loop 入力が必要
- `task_id` (string) タスク ID、リクエスト追跡に使用
- `workflow_run_id` (string) ワークフロー実行 ID
- `event` (string) `human_input_required` に固定
- `data` (object) 詳細内容
- `form_id` (string) ヒューマン入力フォーム ID
- `node_id` (string) Human Input ノード ID
- `node_title` (string) Human Input ノードタイトル
- `form_content` (string) レンダリング済みフォーム内容
- `inputs` (array[object]) フォーム入力項目の定義
- `actions` (array[object]) ユーザーが選択できるアクションボタン
- `id` (string) アクション ID
- `title` (string) ボタンラベル
- `button_style` (string) ボタンスタイル
- `display_in_ui` (bool) UI にこのフォームを表示するかどうか
- `form_token` (string) `/form/human_input/:form_token` API で使用するトークン
- `resolved_default_values` (object) 実行時に解決されたデフォルト値
- `expiration_time` (timestamp) フォームの有効期限Unix 秒)
- `event: workflow_finished` ワークフロー実行終了、成功と失敗は同じイベント内の異なる状態
- `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用
- `workflow_run_id` (string) ワークフロー実行ID
@ -661,6 +697,166 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
---
<Heading
url='/form/human_input/:form_token'
method='GET'
title='Human Input フォームを取得'
name='#get-human-input-form'
/>
<Row>
<Col>
`form_token` から保留中の Human-in-the-Loop フォームを取得します。
Workflow が `human_input_required``form_token` を含む)で一時停止した際に使用します。
### パス
- `form_token` (string) 必須、一時停止イベントで返されたフォームトークン
### 応答
- `form_content` (string) レンダリング済みフォーム内容markdown/plain text
- `inputs` (array[object]) 入力項目定義
- `resolved_default_values` (object) 解決済みデフォルト値(文字列)
- `user_actions` (array[object]) アクションボタン一覧
- `expiration_time` (timestamp) フォーム有効期限Unix 秒)
### エラー
- 404, フォームが存在しない、または現在のアプリに属していない
- 412, `human_input_form_submitted`, 既に送信済み
- 412, `human_input_form_expired`, 期限切れ
</Col>
<Col sticky>
<CodeGroup
title="リクエスト"
tag="GET"
label="/form/human_input/:form_token"
targetCode={`curl -X GET '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
--header 'Authorization: Bearer {api_key}'`}
/>
<CodeGroup title="応答">
```json {{ title: '応答' }}
{
"form_content": "最終回答を確認してください: {{#$output.answer#}}",
"inputs": [
{
"label": "回答",
"type": "text-input",
"required": true,
"output_variable_name": "answer"
}
],
"resolved_default_values": {
"answer": "初期値"
},
"user_actions": [
{ "id": "approve", "title": "承認", "button_style": "primary" },
{ "id": "reject", "title": "却下", "button_style": "warning" }
],
"expiration_time": 1735689600
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/form/human_input/:form_token'
method='POST'
title='Human Input フォームを送信'
name='#submit-human-input-form'
/>
<Row>
<Col>
保留中の Human-in-the-Loop フォームを送信します。
### パス
- `form_token` (string) 必須、一時停止イベントで返されたフォームトークン
### リクエストボディ
- `inputs` (object) 必須、フォーム項目の key/value
- `action` (string) 必須、`user_actions` から選択したアクション ID
- `user` (string) 必須、エンドユーザー識別子
### 応答
成功時は空オブジェクトを返します。
### エラー
- 400, `invalid_form_data`, 送信データがフォームスキーマに一致しない
- 404, フォームが存在しない、または現在のアプリに属していない
- 412, `human_input_form_submitted`, 既に送信済み
- 412, `human_input_form_expired`, 期限切れ
</Col>
<Col sticky>
<CodeGroup
title="リクエスト"
tag="POST"
label="/form/human_input/:form_token"
targetCode={`curl -X POST '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
--header 'Authorization: Bearer {api_key}' \\
--header 'Content-Type: application/json' \\
--data-raw '{
"inputs": {"answer": "承認済み回答"},
"action": "approve",
"user": "abc-123"
}'`}
/>
<CodeGroup title="応答">
```json {{ title: '応答' }}
{}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/workflow/:task_id/events'
method='GET'
title='再開後の Workflow イベントを取得'
name='#get-workflow-resume-events'
/>
<Row>
<Col>
Human Input フォーム送信後に、ワークフロー再開後のイベントを継続受信します。
このエンドポイントは `text/event-stream` を返し、完了までイベントを購読できます。
### パス
- `task_id` (string) 必須、workflow 実行 ID`workflow_run_id`
### クエリ
- `user` (string) 必須、エンドユーザー識別子
- `include_state_snapshot` (bool) 任意、`true` の場合は永続化済み状態スナップショットを先に再生してからリアルタイムイベントへ移行
### 応答
Server-Sent Events ストリーム(`text/event-stream`)。
主なイベントは `node_started`、`node_finished`、`human_input_form_filled`、`human_input_form_timeout`、`workflow_finished` です。
</Col>
<Col sticky>
<CodeGroup
title="リクエスト"
tag="GET"
label="/workflow/:task_id/events"
targetCode={`curl -N -X GET '${props.appDetail.api_base_url}/workflow/{workflow_run_id}/events?user=abc-123' \\
--header 'Authorization: Bearer {api_key}'`}
/>
<CodeGroup title="応答">
```streaming {{ title: '応答' }}
data: {"event":"human_input_form_filled","task_id":"run-1","workflow_run_id":"run-1","data":{"node_id":"human_input_1","node_title":"Human Input","rendered_content":"承認済み回答","action_id":"approve","action_text":"承認"}}
data: {"event":"node_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"node-execution-id","node_id":"llm_1","node_type":"llm","title":"LLM","index":5,"status":"succeeded","created_at":1735689601}}
data: {"event":"workflow_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"run-1","workflow_id":"workflow-id","status":"succeeded","outputs":{"text":"Done"},"created_at":1735689590,"finished_at":1735689602,"elapsed_time":12.0,"total_tokens":1000,"total_steps":6}}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/files/upload'
method='POST'

View File

@ -136,6 +136,24 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
- `total_price` (decimal) optional 总费用
- `currency` (string) optional 货币,如 `USD` / `RMB`
- `created_at` (timestamp) 开始时间
- `event: human_input_required` Workflow 已暂停,等待 Human-in-the-Loop 输入
- `task_id` (string) 任务 ID用于请求跟踪
- `workflow_run_id` (string) workflow 执行 ID
- `event` (string) 固定为 `human_input_required`
- `data` (object) 详细内容
- `form_id` (string) 人工输入表单 ID
- `node_id` (string) Human Input 节点 ID
- `node_title` (string) Human Input 节点标题
- `form_content` (string) 渲染后的表单内容
- `inputs` (array[object]) 表单输入项定义
- `actions` (array[object]) 用户可选动作按钮
- `id` (string) 动作 ID
- `title` (string) 按钮文案
- `button_style` (string) 按钮样式
- `display_in_ui` (bool) 是否需要在 UI 展示该表单
- `form_token` (string) 用于 `/form/human_input/:form_token` 接口的令牌
- `resolved_default_values` (object) 运行时解析后的默认值
- `expiration_time` (timestamp) 表单过期时间Unix 秒级时间戳)
- `event: workflow_finished` workflow 执行结束,成功失败同一事件中不同状态
- `task_id` (string) 任务 ID用于请求跟踪和下方的停止响应接口
- `workflow_run_id` (string) workflow 执行 ID
@ -445,6 +463,24 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
- `total_price` (decimal) optional 总费用
- `currency` (string) optional 货币,如 `USD` / `RMB`
- `created_at` (timestamp) 开始时间
- `event: human_input_required` Workflow 已暂停,等待 Human-in-the-Loop 输入
- `task_id` (string) 任务 ID用于请求跟踪
- `workflow_run_id` (string) workflow 执行 ID
- `event` (string) 固定为 `human_input_required`
- `data` (object) 详细内容
- `form_id` (string) 人工输入表单 ID
- `node_id` (string) Human Input 节点 ID
- `node_title` (string) Human Input 节点标题
- `form_content` (string) 渲染后的表单内容
- `inputs` (array[object]) 表单输入项定义
- `actions` (array[object]) 用户可选动作按钮
- `id` (string) 动作 ID
- `title` (string) 按钮文案
- `button_style` (string) 按钮样式
- `display_in_ui` (bool) 是否需要在 UI 展示该表单
- `form_token` (string) 用于 `/form/human_input/:form_token` 接口的令牌
- `resolved_default_values` (object) 运行时解析后的默认值
- `expiration_time` (timestamp) 表单过期时间Unix 秒级时间戳)
- `event: workflow_finished` workflow 执行结束,成功失败同一事件中不同状态
- `task_id` (string) 任务 ID用于请求跟踪和下方的停止响应接口
- `workflow_run_id` (string) workflow 执行 ID
@ -654,6 +690,166 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
---
<Heading
url='/form/human_input/:form_token'
method='GET'
title='获取人工输入表单'
name='#get-human-input-form'
/>
<Row>
<Col>
通过 `form_token` 获取待处理的 Human-in-the-Loop 表单。
当 Workflow 在流式事件中返回 `human_input_required`(包含 `form_token`)时,可调用此接口拉取表单详情。
### Path
- `form_token` (string) 必填,暂停事件返回的表单 token
### Response
- `form_content` (string) 已渲染的表单内容markdown/plain text
- `inputs` (array[object]) 表单输入项定义
- `resolved_default_values` (object) 已解析的默认值(字符串)
- `user_actions` (array[object]) 操作按钮列表
- `expiration_time` (timestamp) 表单过期时间Unix 秒)
### Errors
- 404表单不存在或不属于当前应用
- 412`human_input_form_submitted`,表单已被提交
- 412`human_input_form_expired`,表单已过期
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/form/human_input/:form_token"
targetCode={`curl -X GET '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
--header 'Authorization: Bearer {api_key}'`}
/>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"form_content": "请确认最终结果:{{#$output.answer#}}",
"inputs": [
{
"label": "答案",
"type": "text-input",
"required": true,
"output_variable_name": "answer"
}
],
"resolved_default_values": {
"answer": "初始值"
},
"user_actions": [
{ "id": "approve", "title": "通过", "button_style": "primary" },
{ "id": "reject", "title": "拒绝", "button_style": "warning" }
],
"expiration_time": 1735689600
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/form/human_input/:form_token'
method='POST'
title='提交人工输入表单'
name='#submit-human-input-form'
/>
<Row>
<Col>
提交待处理的 Human-in-the-Loop 表单。
### Path
- `form_token` (string) 必填,暂停事件返回的表单 token
### Request Body
- `inputs` (object) 必填,表单字段的 key/value
- `action` (string) 必填,从 `user_actions` 中选择的动作 ID
- `user` (string) 必填,终端用户标识
### Response
成功时返回空对象。
### Errors
- 400`invalid_form_data`,提交数据与表单 schema 不匹配
- 404表单不存在或不属于当前应用
- 412`human_input_form_submitted`,表单已被提交
- 412`human_input_form_expired`,表单已过期
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/form/human_input/:form_token"
targetCode={`curl -X POST '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
--header 'Authorization: Bearer {api_key}' \\
--header 'Content-Type: application/json' \\
--data-raw '{
"inputs": {"answer": "已确认答案"},
"action": "approve",
"user": "abc-123"
}'`}
/>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/workflow/:task_id/events'
method='GET'
title='获取恢复后的 Workflow 事件流'
name='#get-workflow-resume-events'
/>
<Row>
<Col>
在提交人工输入表单后,继续订阅工作流后续执行事件。
返回 `text/event-stream`,可持续接收直到工作流结束。
### Path
- `task_id` (string) 必填workflow 运行 ID`workflow_run_id`
### Query
- `user` (string) 必填,终端用户标识
- `include_state_snapshot` (bool) 可选,设为 `true` 时会先回放持久化状态快照,再继续实时事件
### Response
Server-Sent Events 流(`text/event-stream`)。
常见事件包括 `node_started`、`node_finished`、`human_input_form_filled`、`human_input_form_timeout`、`workflow_finished`。
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/workflow/:task_id/events"
targetCode={`curl -N -X GET '${props.appDetail.api_base_url}/workflow/{workflow_run_id}/events?user=abc-123' \\
--header 'Authorization: Bearer {api_key}'`}
/>
<CodeGroup title="Response">
```streaming {{ title: 'Response' }}
data: {"event":"human_input_form_filled","task_id":"run-1","workflow_run_id":"run-1","data":{"node_id":"human_input_1","node_title":"Human Input","rendered_content":"已确认答案","action_id":"approve","action_text":"通过"}}
data: {"event":"node_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"node-execution-id","node_id":"llm_1","node_type":"llm","title":"LLM","index":5,"status":"succeeded","created_at":1735689601}}
data: {"event":"workflow_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"run-1","workflow_id":"workflow-id","status":"succeeded","outputs":{"text":"Done"},"created_at":1735689590,"finished_at":1735689602,"elapsed_time":12.0,"total_tokens":1000,"total_steps":6}}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/files/upload'
method='POST'