diff --git a/pom.xml b/pom.xml
index d13d4bffc..09d30e185 100644
--- a/pom.xml
+++ b/pom.xml
@@ -124,6 +124,7 @@
weixin-java-miniapp
weixin-java-open
weixin-java-qidian
+ weixin-java-aispeech
weixin-java-channel
spring-boot-starters
solon-plugins
diff --git a/weixin-java-aispeech/pom.xml b/weixin-java-aispeech/pom.xml
new file mode 100644
index 000000000..6ba64a873
--- /dev/null
+++ b/weixin-java-aispeech/pom.xml
@@ -0,0 +1,52 @@
+
+
+ 4.0.0
+
+ com.github.binarywang
+ wx-java
+ 4.8.3.B
+
+
+ weixin-java-aispeech
+ WxJava - Aispeech Java SDK
+ 微信智能对话 Java SDK
+
+
+
+ com.github.binarywang
+ weixin-java-common
+ ${project.version}
+
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+
+
+
+ org.testng
+ testng
+ test
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ src/test/resources/testng.xml
+
+
+
+
+
+
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechDialogService.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechDialogService.java
new file mode 100644
index 000000000..51d46562c
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechDialogService.java
@@ -0,0 +1,23 @@
+package me.chanjar.weixin.aispeech.api;
+
+import java.util.List;
+import me.chanjar.weixin.aispeech.bean.dialog.AsyncTaskResult;
+import me.chanjar.weixin.aispeech.bean.dialog.BotIntent;
+import me.chanjar.weixin.aispeech.bean.dialog.DialogQueryRequest;
+import me.chanjar.weixin.aispeech.bean.dialog.DialogResult;
+import me.chanjar.weixin.aispeech.bean.dialog.PublishProgress;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+public interface WxAispeechDialogService {
+ String getAccessToken(String appid, String account) throws WxErrorException;
+
+ String importBotJson(int mode, List data) throws WxErrorException;
+
+ String publishBot() throws WxErrorException;
+
+ PublishProgress getPublishProgress(String env) throws WxErrorException;
+
+ AsyncTaskResult queryAsyncTask(String taskId) throws WxErrorException;
+
+ DialogResult query(DialogQueryRequest request) throws WxErrorException;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechKnowledgeService.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechKnowledgeService.java
new file mode 100644
index 000000000..fa27d4823
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechKnowledgeService.java
@@ -0,0 +1,51 @@
+package me.chanjar.weixin.aispeech.api;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeInfo;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeManualCreateRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveProgress;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeTagRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUpdateRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUrlCreateRequest;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+public interface WxAispeechKnowledgeService {
+ KnowledgeInfo createKnowledgeByFile(String knowledgeBaseId, File file, String title, String description, String metadata)
+ throws WxErrorException;
+
+ KnowledgeInfo createKnowledgeByUrl(String knowledgeBaseId, KnowledgeUrlCreateRequest request) throws WxErrorException;
+
+ KnowledgeInfo createKnowledgeByManual(String knowledgeBaseId, KnowledgeManualCreateRequest request) throws WxErrorException;
+
+ List listKnowledge(String knowledgeBaseId, Integer page, Integer pageSize) throws WxErrorException;
+
+ List listKnowledgeByIds(List knowledgeIds) throws WxErrorException;
+
+ KnowledgeInfo getKnowledge(String knowledgeId) throws WxErrorException;
+
+ KnowledgeInfo updateKnowledge(String knowledgeId, KnowledgeUpdateRequest request) throws WxErrorException;
+
+ KnowledgeInfo updateManualKnowledge(String knowledgeId, KnowledgeManualCreateRequest request) throws WxErrorException;
+
+ boolean deleteKnowledge(String knowledgeId) throws WxErrorException;
+
+ boolean updateKnowledgeTags(List knowledgeIds, Long tagId) throws WxErrorException;
+
+ List searchKnowledge(String keyword, String knowledgeBaseId, Integer page, Integer pageSize)
+ throws WxErrorException;
+
+ String moveKnowledge(KnowledgeMoveRequest request) throws WxErrorException;
+
+ KnowledgeMoveProgress getMoveProgress(String taskId) throws WxErrorException;
+
+ boolean createKnowledgeBaseTag(String knowledgeBaseId, KnowledgeTagRequest request) throws WxErrorException;
+
+ boolean updateKnowledgeBaseTag(String knowledgeBaseId, String tagId, KnowledgeTagRequest request) throws WxErrorException;
+
+ String postRaw(String path, Object requestBody) throws WxErrorException;
+
+ String getRaw(String path, Map queryParams) throws WxErrorException;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechService.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechService.java
new file mode 100644
index 000000000..08ccf837e
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechService.java
@@ -0,0 +1,13 @@
+package me.chanjar.weixin.aispeech.api;
+
+import me.chanjar.weixin.aispeech.config.WxAispeechConfigStorage;
+
+public interface WxAispeechService {
+ WxAispeechDialogService getDialogService();
+
+ WxAispeechKnowledgeService getKnowledgeService();
+
+ WxAispeechConfigStorage getConfigStorage();
+
+ void setConfigStorage(WxAispeechConfigStorage configStorage);
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechDialogServiceImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechDialogServiceImpl.java
new file mode 100644
index 000000000..9bd53b454
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechDialogServiceImpl.java
@@ -0,0 +1,129 @@
+package me.chanjar.weixin.aispeech.api.impl;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import me.chanjar.weixin.aispeech.api.WxAispeechDialogService;
+import me.chanjar.weixin.aispeech.bean.dialog.AispeechApiResponse;
+import me.chanjar.weixin.aispeech.bean.dialog.AsyncTaskResult;
+import me.chanjar.weixin.aispeech.bean.dialog.BotIntent;
+import me.chanjar.weixin.aispeech.bean.dialog.DialogQueryRequest;
+import me.chanjar.weixin.aispeech.bean.dialog.DialogResult;
+import me.chanjar.weixin.aispeech.bean.dialog.PublishProgress;
+import me.chanjar.weixin.aispeech.util.WxAispeechSignUtil;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+import org.apache.commons.lang3.StringUtils;
+
+public class WxAispeechDialogServiceImpl implements WxAispeechDialogService {
+ private final WxAispeechServiceImpl service;
+
+ public WxAispeechDialogServiceImpl(WxAispeechServiceImpl service) {
+ this.service = service;
+ }
+
+ @Override
+ public String getAccessToken(String appid, String account) throws WxErrorException {
+ Map request = new HashMap<>();
+ if (StringUtils.isNotBlank(account)) {
+ request.put("account", account);
+ }
+
+ String response = service.executeDialogPost("/v2/token", request, false, appid);
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type);
+ ensureSuccess(result);
+ String token = result.getData().get("access_token").getAsString();
+ service.getConfigStorage().setOpenAiToken(token);
+ return token;
+ }
+
+ @Override
+ public String importBotJson(int mode, List data) throws WxErrorException {
+ Map request = new HashMap<>();
+ request.put("mode", mode);
+ request.put("data", data);
+
+ String response = service.executeDialogPost("/v2/bot/import/json", request, true, null);
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type);
+ ensureSuccess(result);
+ return result.getData().get("task_id").getAsString();
+ }
+
+ @Override
+ public String publishBot() throws WxErrorException {
+ String response = service.executeDialogPost("/v2/bot/publish", "{}", true, null);
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type);
+ ensureSuccess(result);
+ return result.getRequestId();
+ }
+
+ @Override
+ public PublishProgress getPublishProgress(String env) throws WxErrorException {
+ Map request = new HashMap<>();
+ request.put("env", env);
+
+ String response = service.executeDialogPost("/v2/bot/effective_progress", request, true, null);
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type);
+ ensureSuccess(result);
+ return result.getData();
+ }
+
+ @Override
+ public AsyncTaskResult queryAsyncTask(String taskId) throws WxErrorException {
+ Map request = new HashMap<>();
+ request.put("task_id", taskId);
+
+ String response = service.executeDialogPost("/v2/async/fetch", request, true, null);
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type);
+ ensureSuccess(result);
+ return result.getData();
+ }
+
+ @Override
+ public DialogResult query(DialogQueryRequest request) throws WxErrorException {
+ String json = WxGsonBuilder.create().toJson(request);
+ String encrypted = WxAispeechSignUtil.encryptAesCbcToBase64(json, service.getConfigStorage().getAesKey());
+ String response = service.executeDialogPost("/v2/bot/query", encrypted, true, null);
+
+ String responseJson = response;
+ if (!looksLikeJson(response)) {
+ responseJson = WxAispeechSignUtil.decryptAesCbcFromBase64(response, service.getConfigStorage().getAesKey());
+ }
+
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(responseJson, type);
+ ensureSuccess(result);
+
+ DialogResult dialogResult = result.getData();
+ if (dialogResult != null && looksLikeJson(dialogResult.getAnswer())) {
+ dialogResult.setRawAnswer(WxGsonBuilder.create().fromJson(dialogResult.getAnswer(), JsonElement.class));
+ }
+ return dialogResult;
+ }
+
+ private boolean looksLikeJson(String value) {
+ return StringUtils.isNotBlank(value) && (value.startsWith("{") || value.startsWith("["));
+ }
+
+ private void ensureSuccess(AispeechApiResponse> response) throws WxErrorException {
+ if (response == null) {
+ throw new WxErrorException("响应为空");
+ }
+ if (response.getCode() == null || response.getCode() != 0) {
+ throw new WxErrorException(WxError.builder()
+ .errorCode(response.getCode() == null ? -1 : response.getCode())
+ .errorMsg(response.getMsg())
+ .build());
+ }
+ }
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImpl.java
new file mode 100644
index 000000000..c52550f6b
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImpl.java
@@ -0,0 +1,185 @@
+package me.chanjar.weixin.aispeech.api.impl;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.reflect.TypeToken;
+import java.io.File;
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+import me.chanjar.weixin.aispeech.api.WxAispeechKnowledgeService;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeInfo;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeListResult;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeManualCreateRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveProgress;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeTagRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUpdateRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUrlCreateRequest;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+import org.apache.commons.lang3.StringUtils;
+
+public class WxAispeechKnowledgeServiceImpl implements WxAispeechKnowledgeService {
+ private final WxAispeechServiceImpl service;
+
+ public WxAispeechKnowledgeServiceImpl(WxAispeechServiceImpl service) {
+ this.service = service;
+ }
+
+ @Override
+ public KnowledgeInfo createKnowledgeByFile(String knowledgeBaseId, File file, String title, String description, String metadata)
+ throws WxErrorException {
+ String response = service.executeKnowledgeMultipartPost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge/file",
+ file, title, description, metadata);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ public KnowledgeInfo createKnowledgeByUrl(String knowledgeBaseId, KnowledgeUrlCreateRequest request)
+ throws WxErrorException {
+ String response = service.executeKnowledgePost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge/url", request);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ public KnowledgeInfo createKnowledgeByManual(String knowledgeBaseId, KnowledgeManualCreateRequest request)
+ throws WxErrorException {
+ String response = service.executeKnowledgePost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge/manual", request);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ public List listKnowledge(String knowledgeBaseId, Integer page, Integer pageSize)
+ throws WxErrorException {
+ Map query = new HashMap<>();
+ query.put("page", page == null ? null : String.valueOf(page));
+ query.put("page_size", pageSize == null ? null : String.valueOf(pageSize));
+ String response = service.executeKnowledgeGet("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge", query);
+ KnowledgeListResult result = WxGsonBuilder.create().fromJson(response, KnowledgeListResult.class);
+ return result == null ? null : result.getData();
+ }
+
+ @Override
+ public List listKnowledgeByIds(List knowledgeIds) throws WxErrorException {
+ if (knowledgeIds == null || knowledgeIds.isEmpty()) {
+ return null;
+ }
+ StringJoiner joiner = new StringJoiner(",");
+ for (String knowledgeId : knowledgeIds) {
+ if (StringUtils.isNotBlank(knowledgeId)) {
+ joiner.add(knowledgeId);
+ }
+ }
+ if (joiner.length() == 0) {
+ return null;
+ }
+
+ Map query = new HashMap<>();
+ query.put("ids", joiner.toString());
+ String response = service.executeKnowledgeGet("/api/v1/knowledge/batch", query);
+ return parseKnowledgeInfoList(response);
+ }
+
+ @Override
+ public KnowledgeInfo getKnowledge(String knowledgeId) throws WxErrorException {
+ String response = service.executeKnowledgeGet("/api/v1/knowledge/" + knowledgeId, null);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ public KnowledgeInfo updateKnowledge(String knowledgeId, KnowledgeUpdateRequest request) throws WxErrorException {
+ String response = service.executeKnowledgePut("/api/v1/knowledge/" + knowledgeId, request);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ public KnowledgeInfo updateManualKnowledge(String knowledgeId, KnowledgeManualCreateRequest request) throws WxErrorException {
+ String response = service.executeKnowledgePut("/api/v1/knowledge/manual/" + knowledgeId, request);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ @Override
+ public boolean deleteKnowledge(String knowledgeId) throws WxErrorException {
+ service.executeKnowledgeDelete("/api/v1/knowledge/" + knowledgeId);
+ return true;
+ }
+
+ @Override
+ public boolean updateKnowledgeTags(List knowledgeIds, Long tagId) throws WxErrorException {
+ if (knowledgeIds == null || knowledgeIds.isEmpty() || tagId == null) {
+ return false;
+ }
+
+ Map request = new HashMap<>();
+ request.put("knowledge_ids", knowledgeIds);
+ request.put("tag_id", tagId);
+ String response = service.executeKnowledgePut("/api/v1/knowledge/tags", request);
+ return StringUtils.isNotBlank(response);
+ }
+
+ @Override
+ public List searchKnowledge(String keyword, String knowledgeBaseId, Integer page, Integer pageSize)
+ throws WxErrorException {
+ Map query = new HashMap<>();
+ query.put("keyword", keyword);
+ query.put("knowledge_base_id", knowledgeBaseId);
+ query.put("page", page == null ? null : String.valueOf(page));
+ query.put("page_size", pageSize == null ? null : String.valueOf(pageSize));
+ String response = service.executeKnowledgeGet("/api/v1/knowledge/search", query);
+ return parseKnowledgeInfoList(response);
+ }
+
+ @Override
+ public String moveKnowledge(KnowledgeMoveRequest request) throws WxErrorException {
+ return service.executeKnowledgePost("/api/v1/knowledge/move", request);
+ }
+
+ @Override
+ public KnowledgeMoveProgress getMoveProgress(String taskId) throws WxErrorException {
+ String response = service.executeKnowledgeGet("/api/v1/knowledge/move/progress/" + taskId, null);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeMoveProgress.class);
+ }
+
+ @Override
+ public boolean createKnowledgeBaseTag(String knowledgeBaseId, KnowledgeTagRequest request) throws WxErrorException {
+ String response = service.executeKnowledgePost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/tags", request);
+ return StringUtils.isNotBlank(response);
+ }
+
+ @Override
+ public boolean updateKnowledgeBaseTag(String knowledgeBaseId, String tagId, KnowledgeTagRequest request)
+ throws WxErrorException {
+ String response = service.executeKnowledgePut("/api/v1/knowledge-bases/" + knowledgeBaseId + "/tags/" + tagId, request);
+ return StringUtils.isNotBlank(response);
+ }
+
+ @Override
+ public String postRaw(String path, Object requestBody) throws WxErrorException {
+ return service.executeKnowledgePost(path, requestBody);
+ }
+
+ @Override
+ public String getRaw(String path, Map queryParams) throws WxErrorException {
+ return service.executeKnowledgeGet(path, queryParams);
+ }
+
+ private List parseKnowledgeInfoList(String response) {
+ if (StringUtils.isBlank(response)) {
+ return null;
+ }
+
+ JsonElement element = WxGsonBuilder.create().fromJson(response, JsonElement.class);
+ Type listType = new TypeToken>() { } .getType();
+ if (element != null && element.isJsonObject()) {
+ JsonObject object = element.getAsJsonObject();
+ if (object.has("data")) {
+ return WxGsonBuilder.create().fromJson(object.get("data"), listType);
+ }
+ }
+ return WxGsonBuilder.create().fromJson(element, listType);
+ }
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpClientImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpClientImpl.java
new file mode 100644
index 000000000..e37d60e35
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpClientImpl.java
@@ -0,0 +1,4 @@
+package me.chanjar.weixin.aispeech.api.impl;
+
+public class WxAispeechServiceHttpClientImpl extends WxAispeechServiceHttpComponentsImpl {
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpComponentsImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpComponentsImpl.java
new file mode 100644
index 000000000..ac91d9893
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpComponentsImpl.java
@@ -0,0 +1,4 @@
+package me.chanjar.weixin.aispeech.api.impl;
+
+public class WxAispeechServiceHttpComponentsImpl extends WxAispeechServiceImpl {
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceImpl.java
new file mode 100644
index 000000000..fa04f3623
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceImpl.java
@@ -0,0 +1,237 @@
+package me.chanjar.weixin.aispeech.api.impl;
+
+import com.google.gson.Gson;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.UUID;
+import lombok.Getter;
+import me.chanjar.weixin.aispeech.api.WxAispeechDialogService;
+import me.chanjar.weixin.aispeech.api.WxAispeechKnowledgeService;
+import me.chanjar.weixin.aispeech.api.WxAispeechService;
+import me.chanjar.weixin.aispeech.config.WxAispeechConfigStorage;
+import me.chanjar.weixin.aispeech.util.WxAispeechSignUtil;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.hc.DefaultHttpComponentsClientBuilder;
+import me.chanjar.weixin.common.util.http.hc.HttpComponentsClientBuilder;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.classic.methods.HttpPut;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.net.URIBuilder;
+import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
+
+public class WxAispeechServiceImpl implements WxAispeechService {
+ private static final Gson GSON = new Gson();
+
+ @Getter
+ private final WxAispeechDialogService dialogService = new WxAispeechDialogServiceImpl(this);
+ @Getter
+ private final WxAispeechKnowledgeService knowledgeService = new WxAispeechKnowledgeServiceImpl(this);
+
+ @Getter
+ private WxAispeechConfigStorage configStorage;
+ private CloseableHttpClient httpClient;
+ private HttpHost proxy;
+
+ @Override
+ public void setConfigStorage(WxAispeechConfigStorage configStorage) {
+ this.configStorage = configStorage;
+ this.initHttp();
+ }
+
+ protected void initHttp() {
+ HttpComponentsClientBuilder builder = configStorage.getHttpComponentsClientBuilder();
+ if (builder == null) {
+ builder = DefaultHttpComponentsClientBuilder.get();
+ }
+
+ builder.httpProxyHost(configStorage.getHttpProxyHost())
+ .httpProxyPort(configStorage.getHttpProxyPort())
+ .httpProxyUsername(configStorage.getHttpProxyUsername())
+ .httpProxyPassword(configStorage.getHttpProxyPassword() == null ? null :
+ configStorage.getHttpProxyPassword().toCharArray());
+
+ if (configStorage.getHttpProxyHost() != null && configStorage.getHttpProxyPort() > 0) {
+ this.proxy = new HttpHost(configStorage.getHttpProxyHost(), configStorage.getHttpProxyPort());
+ } else {
+ this.proxy = null;
+ }
+
+ this.httpClient = builder.build();
+ }
+
+ protected String executeDialogPost(String path, Object requestBody, boolean withOpenToken, String appid)
+ throws WxErrorException {
+ String body = toBody(requestBody);
+ String requestId = UUID.randomUUID().toString();
+ long timestamp = System.currentTimeMillis() / 1000;
+ String nonce = randomNonce();
+ String sign = WxAispeechSignUtil.calcDialogSign(configStorage.getToken(), timestamp, nonce, body);
+ String resolvedAppid = StringUtils.defaultIfBlank(appid, configStorage.getAppid());
+
+ HttpPost request = new HttpPost(configStorage.getDialogApiBaseUrl() + path);
+ request.setHeader("request_id", requestId);
+ request.setHeader("timestamp", String.valueOf(timestamp));
+ request.setHeader("nonce", nonce);
+ request.setHeader("sign", sign);
+ request.setHeader("Content-Type", ContentType.APPLICATION_JSON.getMimeType());
+ if (withOpenToken) {
+ if (StringUtils.isBlank(configStorage.getOpenAiToken())) {
+ throw new WxErrorException("X-OPENAI-TOKEN不能为空,请先调用getAccessToken或手动设置");
+ }
+ request.setHeader("X-OPENAI-TOKEN", configStorage.getOpenAiToken());
+ } else {
+ if (StringUtils.isBlank(resolvedAppid)) {
+ throw new WxErrorException("X-APPID不能为空");
+ }
+ request.setHeader("X-APPID", resolvedAppid);
+ }
+ request.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON));
+ return executeRequest(request);
+ }
+
+ protected String executeKnowledgeGet(String path, Map queryParams) throws WxErrorException {
+ try {
+ URIBuilder builder = new URIBuilder(configStorage.getKnowledgeApiBaseUrl() + path);
+ if (queryParams != null) {
+ for (Map.Entry entry : queryParams.entrySet()) {
+ if (entry.getValue() != null) {
+ builder.addParameter(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+ HttpGet request = new HttpGet(builder.build());
+ enrichKnowledgeHeaders(request, "");
+ return executeRequest(request);
+ } catch (Exception e) {
+ throw toWxErrorException(e);
+ }
+ }
+
+ protected String executeKnowledgePost(String path, Object requestBody) throws WxErrorException {
+ String body = toBody(requestBody);
+ HttpPost request = new HttpPost(configStorage.getKnowledgeApiBaseUrl() + path);
+ request.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON));
+ enrichKnowledgeHeaders(request, body);
+ return executeRequest(request);
+ }
+
+ protected String executeKnowledgePut(String path, Object requestBody) throws WxErrorException {
+ String body = toBody(requestBody);
+ HttpPut request = new HttpPut(configStorage.getKnowledgeApiBaseUrl() + path);
+ request.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON));
+ enrichKnowledgeHeaders(request, body);
+ return executeRequest(request);
+ }
+
+ protected String executeKnowledgeMultipartPost(String path, File file, String title, String description, String metadata)
+ throws WxErrorException {
+ HttpPost request = new HttpPost(configStorage.getKnowledgeApiBaseUrl() + path);
+ MultipartEntityBuilder builder = MultipartEntityBuilder.create();
+ builder.addBinaryBody("file", file, ContentType.DEFAULT_BINARY, file.getName());
+ if (StringUtils.isNotBlank(title)) {
+ builder.addTextBody("title", title, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8));
+ }
+ if (StringUtils.isNotBlank(description)) {
+ builder.addTextBody("description", description, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8));
+ }
+ if (StringUtils.isNotBlank(metadata)) {
+ builder.addTextBody("metadata", metadata, ContentType.APPLICATION_JSON);
+ }
+ HttpEntity entity = builder.build();
+ request.setEntity(entity);
+ if (entity.getContentType() != null) {
+ request.setHeader("Content-Type", entity.getContentType());
+ }
+ enrichKnowledgeHeaders(request, "");
+ return executeRequest(request);
+ }
+
+ protected String executeKnowledgeDelete(String path) throws WxErrorException {
+ HttpUriRequestBase request = new HttpUriRequestBase("DELETE", URI.create(configStorage.getKnowledgeApiBaseUrl() + path));
+ enrichKnowledgeHeaders(request, "");
+ return executeRequest(request);
+ }
+
+ private void enrichKnowledgeHeaders(HttpUriRequestBase request, String body) throws WxErrorException {
+ if (StringUtils.isBlank(configStorage.getAppid())) {
+ throw new WxErrorException("知识助理请求需要配置appid");
+ }
+ if (StringUtils.isBlank(configStorage.getSecretKey())) {
+ throw new WxErrorException("知识助理请求需要配置secretKey");
+ }
+
+ String requestId = UUID.randomUUID().toString();
+ long timestamp = System.currentTimeMillis() / 1000;
+ String nonce = randomNonce();
+ String signature = WxAispeechSignUtil.calcKnowledgeSignature(configStorage.getSecretKey(), timestamp, nonce,
+ requestId, body);
+
+ request.setHeader("X-APPID", configStorage.getAppid());
+ request.setHeader("X-Request-ID", requestId);
+ request.setHeader("X-Timestamp", String.valueOf(timestamp));
+ request.setHeader("X-Nonce", nonce);
+ request.setHeader("X-Signature", signature);
+ if (!request.containsHeader("Content-Type")) {
+ request.setHeader("Content-Type", ContentType.APPLICATION_JSON.getMimeType());
+ }
+ }
+
+ private String executeRequest(HttpUriRequestBase request) throws WxErrorException {
+ if (this.proxy != null) {
+ RequestConfig requestConfig = RequestConfig.custom().setProxy(this.proxy).build();
+ request.setConfig(requestConfig);
+ }
+
+ try (CloseableHttpResponse response = httpClient.execute(request)) {
+ int statusCode = response.getCode();
+ HttpEntity entity = response.getEntity();
+ String body = entity == null ? "" : new String(entity.getContent().readAllBytes(), StandardCharsets.UTF_8);
+ if (statusCode >= 200 && statusCode < 300) {
+ return body;
+ }
+
+ throw new WxErrorException(WxError.builder().errorCode(statusCode).errorMsg(body).build());
+ } catch (IOException e) {
+ throw toWxErrorException(e);
+ }
+ }
+
+ protected T fromJson(String json, Class clazz) {
+ return GSON.fromJson(json, clazz);
+ }
+
+ private String toBody(Object requestBody) {
+ if (requestBody == null) {
+ return "{}";
+ }
+ if (requestBody instanceof String) {
+ return (String) requestBody;
+ }
+ return GSON.toJson(requestBody);
+ }
+
+ private WxErrorException toWxErrorException(Exception e) {
+ if (e instanceof WxErrorException) {
+ return (WxErrorException) e;
+ }
+ return new WxErrorException(e);
+ }
+
+ private String randomNonce() {
+ return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
+ }
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AispeechApiResponse.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AispeechApiResponse.java
new file mode 100644
index 000000000..ef04ca351
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AispeechApiResponse.java
@@ -0,0 +1,13 @@
+package me.chanjar.weixin.aispeech.bean.dialog;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+@Data
+public class AispeechApiResponse {
+ private Integer code;
+ private String msg;
+ @SerializedName("request_id")
+ private String requestId;
+ private T data;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AsyncTaskResult.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AsyncTaskResult.java
new file mode 100644
index 000000000..a806fb368
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AsyncTaskResult.java
@@ -0,0 +1,33 @@
+package me.chanjar.weixin.aispeech.bean.dialog;
+
+import com.google.gson.JsonElement;
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class AsyncTaskResult {
+ private Integer state;
+ private String msg;
+ private Integer progress;
+ private Long start;
+ private Long end;
+ private String url;
+ private Integer totalCount;
+ private Integer successCount;
+ private Integer failCount;
+ private JsonElement successSkillInfo;
+ private List successSkillInfoList;
+
+ @Data
+ public static class SkillInfo {
+ private Long id;
+ private String name;
+ private List intents;
+ }
+
+ @Data
+ public static class IntentInfo {
+ private Long id;
+ private String name;
+ }
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/BotIntent.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/BotIntent.java
new file mode 100644
index 000000000..3927461fc
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/BotIntent.java
@@ -0,0 +1,13 @@
+package me.chanjar.weixin.aispeech.bean.dialog;
+
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class BotIntent {
+ private String skill;
+ private String intent;
+ private Boolean disable;
+ private List questions;
+ private List answers;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogQueryRequest.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogQueryRequest.java
new file mode 100644
index 000000000..dd748957f
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogQueryRequest.java
@@ -0,0 +1,19 @@
+package me.chanjar.weixin.aispeech.bean.dialog;
+
+import com.google.gson.annotations.SerializedName;
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class DialogQueryRequest {
+ private String query;
+ private String env;
+ @SerializedName("first_priority_skills")
+ private List firstPrioritySkills;
+ @SerializedName("second_priority_skills")
+ private List secondPrioritySkills;
+ @SerializedName("user_name")
+ private String userName;
+ private String avatar;
+ private String userid;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogResult.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogResult.java
new file mode 100644
index 000000000..587279068
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogResult.java
@@ -0,0 +1,39 @@
+package me.chanjar.weixin.aispeech.bean.dialog;
+
+import com.google.gson.JsonElement;
+import com.google.gson.annotations.SerializedName;
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class DialogResult {
+ private String answer;
+ @SerializedName("answer_type")
+ private String answerType;
+ @SerializedName("skill_name")
+ private String skillName;
+ @SerializedName("intent_name")
+ private String intentName;
+ @SerializedName("msg_id")
+ private String msgId;
+ private List