Skip to content

Commit 3125e3d

Browse files
committed
feat: add End User lookup, Human Input Form, Workflow Resume Events APIs (v1.4.0)
Track dify 1.16.0-rc1 service_api additions: - GET /end-users/{end_user_id}: resolve end-user details from IDs returned by other APIs (e.g., created_by from file upload) - GET/POST /form/human_input/{form_token}: fetch and submit paused Human Input forms, completing the HITL loop started by 1.3.0 events - GET /workflow/{workflow_run_id}/events: SSE stream for resuming workflow events after a human-input pause Introduces a reusable executeGetStreamRequest helper for future GET-SSE endpoints. Bumps version to 1.4.0.
1 parent 23acf8b commit 3125e3d

12 files changed

Lines changed: 341 additions & 12 deletions

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@ Dify Java Client 提供以下核心功能:
6060
<dependency>
6161
<groupId>io.github.imfangs</groupId>
6262
<artifactId>dify-java-client</artifactId>
63-
<version>1.3.0</version>
63+
<version>1.4.0</version>
6464
</dependency>
6565
```
6666

6767
### Gradle
6868

6969
```groovy
70-
implementation 'io.github.imfangs:dify-java-client:1.3.0'
70+
implementation 'io.github.imfangs:dify-java-client:1.4.0'
7171
```
7272

7373
## 快速开始

‎README_EN.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@ Dify Java Client provides the following core features:
6060
<dependency>
6161
<groupId>io.github.imfangs</groupId>
6262
<artifactId>dify-java-client</artifactId>
63-
<version>1.3.0</version>
63+
<version>1.4.0</version>
6464
</dependency>
6565
```
6666

6767
### Gradle
6868

6969
```groovy
70-
implementation 'io.github.imfangs:dify-java-client:1.3.0'
70+
implementation 'io.github.imfangs:dify-java-client:1.4.0'
7171
```
7272

7373
## Quick Start

‎README_JP.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@ Dify Java Clientは以下の主要機能を提供します:
6060
<dependency>
6161
<groupId>io.github.imfangs</groupId>
6262
<artifactId>dify-java-client</artifactId>
63-
<version>1.3.0</version>
63+
<version>1.4.0</version>
6464
</dependency>
6565
```
6666

6767
### Gradle
6868

6969
```groovy
70-
implementation 'io.github.imfangs:dify-java-client:1.3.0'
70+
implementation 'io.github.imfangs:dify-java-client:1.4.0'
7171
```
7272

7373
## クイックスタート

‎RELEASE_NOTES.md‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,26 @@
11
## Changes
22

3-
- Add Human Input (HITL) workflow event support for Dify 1.14.2+ (#162)
4-
- New events: `human_input_required`, `workflow_paused`, `human_input_form_filled`, `human_input_form_timeout`
5-
- New event models: `HumanInputRequiredEvent`, `WorkflowPausedEvent`, `HumanInputFormFilledEvent`, `HumanInputFormTimeoutEvent`
6-
- New callback hooks on `WorkflowStreamCallback` and `ChatflowStreamCallback`: `onHumanInputRequired`, `onWorkflowPaused`, `onHumanInputFormFilled`, `onHumanInputFormTimeout`
3+
- Add Service API for **End User** lookup (`GET /end-users/{end_user_id}`)
4+
- New method on `DifyBaseClient` (available on chat/chatflow/workflow/completion clients): `EndUserResponse getEndUser(String endUserId)`
5+
- New model: `EndUserResponse` (id, tenant_id, app_id, type, external_user_id, name, is_anonymous, session_id, created_at, updated_at)
6+
- Use case: when other APIs return `created_by` as an end-user ID, resolve `external_user_id` / `session_id`
7+
- Add Service API for **Human Input Form** (companion to the Human Input events shipped in 1.3.0)
8+
- `HumanInputFormResponse getHumanInputForm(String formToken)` — `GET /form/human_input/{form_token}`, fetch a paused form definition
9+
- `void submitHumanInputForm(String formToken, HumanInputFormSubmitRequest request)` — `POST /form/human_input/{form_token}`, submit inputs + selected action + user to resume the workflow
10+
- Methods live on `DifyBaseClient` so both workflow and chatflow apps can call them
11+
- New models: `HumanInputFormResponse`, `HumanInputFormSubmitRequest`
12+
- Add Service API for **Workflow Resume Events** stream (`GET /workflow/{workflow_run_id}/events`)
13+
- New method on `DifyWorkflowClient`: `void streamWorkflowEvents(String workflowRunId, String user, Boolean includeStateSnapshot, Boolean continueOnPause, WorkflowStreamCallback callback)`
14+
- After submitting a human-input form, use this endpoint to re-subscribe to the resumed workflow's events (SSE)
15+
- `include_state_snapshot=true` replays a status summary of already-executed nodes; `continue_on_pause=true` keeps the stream open across multiple pauses
16+
- Introduce reusable GET-SSE helper (`executeGetStreamRequest`) for future streaming endpoints
717

18+
## Installation
19+
20+
```xml
21+
<dependency>
22+
<groupId>io.github.imfangs</groupId>
23+
<artifactId>dify-java-client</artifactId>
24+
<version>1.4.0</version>
25+
</dependency>
26+
```

‎pom.xml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<groupId>io.github.imfangs</groupId>
77
<artifactId>dify-java-client</artifactId>
8-
<version>1.3.0</version>
8+
<version>1.4.0</version>
99
<name>dify-java-client</name>
1010
<description>Java Client for Dify</description>
1111
<packaging>jar</packaging>

‎src/main/java/io/github/imfangs/dify/client/DifyBaseClient.java‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
import io.github.imfangs.dify.client.model.chat.AppInfoResponse;
55
import io.github.imfangs.dify.client.model.chat.AppParametersResponse;
66
import io.github.imfangs.dify.client.model.chat.AppWebAppSettingResponse;
7+
import io.github.imfangs.dify.client.model.common.EndUserResponse;
78
import io.github.imfangs.dify.client.model.file.FileUploadRequest;
89
import io.github.imfangs.dify.client.model.file.FileUploadResponse;
910
import io.github.imfangs.dify.client.model.file.FilePreviewResponse;
11+
import io.github.imfangs.dify.client.model.workflow.HumanInputFormResponse;
12+
import io.github.imfangs.dify.client.model.workflow.HumanInputFormSubmitRequest;
1013

1114
import java.io.File;
1215
import java.io.IOException;
@@ -102,6 +105,41 @@ public interface DifyBaseClient extends AutoCloseable {
102105
*/
103106
FilePreviewResponse previewFile(String fileId, boolean asAttachment) throws IOException, DifyApiException;
104107

108+
/**
109+
* 根据 ID 查询终端用户(End User)详情
110+
* 常见用法:其他接口(如文件上传)返回 created_by 是 end-user ID 时,可回查 external_user_id / session_id / 匿名标记。
111+
* 该接口按当前 App Token 所属租户/应用范围鉴权,防止跨租户/应用访问。
112+
*
113+
* @param endUserId 终端用户 ID
114+
* @return 终端用户详情
115+
* @throws IOException IO异常
116+
* @throws DifyApiException API异常
117+
*/
118+
EndUserResponse getEndUser(String endUserId) throws IOException, DifyApiException;
119+
120+
/**
121+
* 获取暂停中的 Human Input 表单
122+
* 通过 human_input_required 事件中的 form_token 获取表单定义。
123+
* 适用于所有支持人工介入的应用类型(Workflow / Chatflow)。
124+
*
125+
* @param formToken 表单 token
126+
* @return 表单响应(含 form_content、inputs、user_actions、expiration_time 等)
127+
* @throws IOException IO异常
128+
* @throws DifyApiException API异常
129+
*/
130+
HumanInputFormResponse getHumanInputForm(String formToken) throws IOException, DifyApiException;
131+
132+
/**
133+
* 提交 Human Input 表单,工作流将从暂停处恢复执行。
134+
* 提交成功后可通过工作流事件流订阅后续执行事件。
135+
*
136+
* @param formToken 表单 token
137+
* @param request 提交内容(inputs / action / user)
138+
* @throws IOException IO异常
139+
* @throws DifyApiException API异常
140+
*/
141+
void submitHumanInputForm(String formToken, HumanInputFormSubmitRequest request) throws IOException, DifyApiException;
142+
105143
/**
106144
* 关闭客户端资源
107145
* 重写AutoCloseable.close()方法,确保不抛出受检异常

‎src/main/java/io/github/imfangs/dify/client/DifyWorkflowClient.java‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,22 @@ WorkflowLogsResponse getWorkflowLogs(String keyword,
100100
String createdByAccount,
101101
Integer page,
102102
Integer limit) throws IOException, DifyApiException;
103+
104+
/**
105+
* 订阅工作流事件流(SSE)
106+
* 常用于人工介入表单提交后,恢复订阅工作流后续事件。
107+
*
108+
* @param workflowRunId 工作流运行实例 ID
109+
* @param user 终端用户标识(须与触发运行的 user 相同)
110+
* @param includeStateSnapshot 是否重放已执行节点的状态快照
111+
* @param continueOnPause 遇到 workflow_paused 时是否继续保持连接(多次人工介入场景)
112+
* @param callback 事件回调
113+
* @throws IOException IO异常
114+
* @throws DifyApiException API异常
115+
*/
116+
void streamWorkflowEvents(String workflowRunId,
117+
String user,
118+
Boolean includeStateSnapshot,
119+
Boolean continueOnPause,
120+
WorkflowStreamCallback callback) throws IOException, DifyApiException;
103121
}

‎src/main/java/io/github/imfangs/dify/client/impl/DefaultDifyClient.java‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public class DefaultDifyClient extends DifyBaseClientImpl implements DifyClient
6060
private static final String WORKFLOWS_RUN_PATH = "/workflows/run";
6161
private static final String WORKFLOWS_TASKS_PATH = "/workflows/tasks";
6262
private static final String WORKFLOWS_LOGS_PATH = "/workflows/logs";
63+
private static final String WORKFLOW_EVENTS_PATH = "/workflow";
6364

6465
//标注应用相关路径
6566
private static final String APPS_ANNOTATIONS_PATH = "/apps/annotations";
@@ -386,6 +387,34 @@ public WorkflowLogsResponse getWorkflowLogs(String keyword,
386387
return executeRequest(request, WorkflowLogsResponse.class);
387388
}
388389

390+
@Override
391+
public void streamWorkflowEvents(String workflowRunId,
392+
String user,
393+
Boolean includeStateSnapshot,
394+
Boolean continueOnPause,
395+
WorkflowStreamCallback callback) throws IOException, DifyApiException {
396+
if (workflowRunId == null || workflowRunId.trim().isEmpty()) {
397+
throw new IllegalArgumentException("workflowRunId 不能为空");
398+
}
399+
if (user == null || user.trim().isEmpty()) {
400+
throw new IllegalArgumentException("user 不能为空");
401+
}
402+
log.debug("订阅工作流事件流: workflowRunId={}, user={}, includeStateSnapshot={}, continueOnPause={}",
403+
workflowRunId, user, includeStateSnapshot, continueOnPause);
404+
Map<String, Object> params = new HashMap<>();
405+
params.put("user", user);
406+
if (includeStateSnapshot != null) {
407+
params.put("include_state_snapshot", includeStateSnapshot);
408+
}
409+
if (continueOnPause != null) {
410+
params.put("continue_on_pause", continueOnPause);
411+
}
412+
String url = buildUrlWithParams(WORKFLOW_EVENTS_PATH + "/" + workflowRunId.trim() + "/events", params);
413+
executeGetStreamRequest(url, (line) -> processStreamLine(line, callback, WORKFLOW_TERMINAL_EVENTS, (data, eventType) -> {
414+
StreamEventDispatcher.dispatchWorkflowEvent(callback, data);
415+
}), callback::onException);
416+
}
417+
389418
/**
390419
* 执行流式请求
391420
*
@@ -399,7 +428,27 @@ private void executeStreamRequest(String path, Object body, LineProcessor linePr
399428
RequestBody requestBody = createJsonRequestBody(body);
400429
Request httpRequest = new Request.Builder().url(baseUrl + path).post(requestBody).header("Authorization", "Bearer " + apiKey).header("Content-Type", "application/json").header("Accept", "text/event-stream").build();
401430

402-
// 执行请求并处理流式响应
431+
executeStreamCall(httpRequest, lineProcessor, errorHandler);
432+
}
433+
434+
/**
435+
* 执行 GET 流式请求
436+
*
437+
* @param path 请求路径(含 query)
438+
* @param lineProcessor 行处理器
439+
* @param errorHandler 错误处理器
440+
*/
441+
private void executeGetStreamRequest(String path, LineProcessor lineProcessor, Consumer<Exception> errorHandler) {
442+
Request httpRequest = new Request.Builder()
443+
.url(baseUrl + path)
444+
.get()
445+
.header("Authorization", "Bearer " + apiKey)
446+
.header("Accept", "text/event-stream")
447+
.build();
448+
executeStreamCall(httpRequest, lineProcessor, errorHandler);
449+
}
450+
451+
private void executeStreamCall(Request httpRequest, LineProcessor lineProcessor, Consumer<Exception> errorHandler) {
403452
Call call = httpClient.newCall(httpRequest);
404453
call.enqueue(new Callback() {
405454
@Override

‎src/main/java/io/github/imfangs/dify/client/impl/DifyBaseClientImpl.java‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
import io.github.imfangs.dify.client.model.chat.AppInfoResponse;
66
import io.github.imfangs.dify.client.model.chat.AppParametersResponse;
77
import io.github.imfangs.dify.client.model.chat.AppWebAppSettingResponse;
8+
import io.github.imfangs.dify.client.model.common.EndUserResponse;
89
import io.github.imfangs.dify.client.model.file.FileUploadRequest;
910
import io.github.imfangs.dify.client.model.file.FileUploadResponse;
1011
import io.github.imfangs.dify.client.model.file.FilePreviewResponse;
12+
import io.github.imfangs.dify.client.model.workflow.HumanInputFormResponse;
13+
import io.github.imfangs.dify.client.model.workflow.HumanInputFormSubmitRequest;
1114
import lombok.extern.slf4j.Slf4j;
1215
import okhttp3.*;
1316

@@ -26,6 +29,8 @@ public class DifyBaseClientImpl extends AbstractDifyClient implements DifyBaseCl
2629
private static final String INFO_PATH = "/info";
2730
private static final String PARAMETERS_PATH = "/parameters";
2831
private static final String SITE_PATH = "/site";
32+
private static final String END_USERS_PATH = "/end-users";
33+
private static final String HUMAN_INPUT_FORM_PATH = "/form/human_input";
2934

3035

3136
/**
@@ -117,6 +122,36 @@ public AppWebAppSettingResponse getAppWebAppSettings() throws IOException, DifyA
117122
return executeGet(SITE_PATH, AppWebAppSettingResponse.class);
118123
}
119124

125+
@Override
126+
public EndUserResponse getEndUser(String endUserId) throws IOException, DifyApiException {
127+
if (endUserId == null || endUserId.trim().isEmpty()) {
128+
throw new IllegalArgumentException("终端用户 ID 不能为空");
129+
}
130+
String path = END_USERS_PATH + "/" + endUserId.trim();
131+
return executeGet(path, EndUserResponse.class);
132+
}
133+
134+
@Override
135+
public HumanInputFormResponse getHumanInputForm(String formToken) throws IOException, DifyApiException {
136+
if (formToken == null || formToken.trim().isEmpty()) {
137+
throw new IllegalArgumentException("表单 token 不能为空");
138+
}
139+
log.debug("获取 Human Input 表单: formToken={}", formToken);
140+
return executeGet(HUMAN_INPUT_FORM_PATH + "/" + formToken.trim(), HumanInputFormResponse.class);
141+
}
142+
143+
@Override
144+
public void submitHumanInputForm(String formToken, HumanInputFormSubmitRequest request) throws IOException, DifyApiException {
145+
if (formToken == null || formToken.trim().isEmpty()) {
146+
throw new IllegalArgumentException("表单 token 不能为空");
147+
}
148+
if (request == null || request.getUser() == null || request.getUser().trim().isEmpty()) {
149+
throw new IllegalArgumentException("user 不能为空");
150+
}
151+
log.debug("提交 Human Input 表单: formToken={}, action={}", formToken, request.getAction());
152+
executePost(HUMAN_INPUT_FORM_PATH + "/" + formToken.trim(), request, Object.class);
153+
}
154+
120155
@Override
121156
public FilePreviewResponse previewFile(String fileId) throws IOException, DifyApiException {
122157
return previewFile(fileId, false);
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package io.github.imfangs.dify.client.model.common;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Data;
8+
import lombok.NoArgsConstructor;
9+
10+
/**
11+
* 终端用户(End User)响应
12+
* 对应后端 EndUserDetail
13+
*/
14+
@Data
15+
@Builder
16+
@NoArgsConstructor
17+
@AllArgsConstructor
18+
@JsonIgnoreProperties(ignoreUnknown = true)
19+
public class EndUserResponse {
20+
21+
/**
22+
* 终端用户 ID
23+
*/
24+
private String id;
25+
26+
/**
27+
* 租户 ID
28+
*/
29+
@JsonProperty("tenant_id")
30+
private String tenantId;
31+
32+
/**
33+
* 应用 ID
34+
*/
35+
@JsonProperty("app_id")
36+
private String appId;
37+
38+
/**
39+
* 终端用户类型
40+
*/
41+
private String type;
42+
43+
/**
44+
* 外部用户 ID(调用方传入的 user 标识)
45+
*/
46+
@JsonProperty("external_user_id")
47+
private String externalUserId;
48+
49+
/**
50+
* 显示名称
51+
*/
52+
private String name;
53+
54+
/**
55+
* 是否为匿名用户
56+
*/
57+
@JsonProperty("is_anonymous")
58+
private Boolean isAnonymous;
59+
60+
/**
61+
* 会话 ID
62+
*/
63+
@JsonProperty("session_id")
64+
private String sessionId;
65+
66+
/**
67+
* 创建时间(ISO 8601 字符串)
68+
*/
69+
@JsonProperty("created_at")
70+
private String createdAt;
71+
72+
/**
73+
* 更新时间(ISO 8601 字符串)
74+
*/
75+
@JsonProperty("updated_at")
76+
private String updatedAt;
77+
}

0 commit comments

Comments
 (0)