Qwen Code Java SDK 深度指南:基于 qwen serve daemon 传输的可靠 Java 11 编程代理客户端
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
Qwen Code Java SDK 是面向 Qwen Code 终端编程代理的官方 Java 客户端库,其核心亮点在于 0.1.0-alpha 版本引入的 daemon 传输层:通过 REST 突变(mutation)加可续传 SSE(Server-Sent Events)与qwen serve守护进程通信,以"失败即关闭"(fail closed)的强可靠语义保证不会把截断的生成结果当作成功返回。本文基于仓库内 QWEN.md 与 java-daemon-sdk-alpha.md 设计文档,结合 qwencode 模块源码 与测试代码,完整讲解该 SDK 的版本约束、架构分工、构建安装、核心 API、传输契约、可靠性语义与已知 alpha 限制,帮助你在自己的 Java 11+ 应用中稳定、正确地接入 Qwen Code 守护进程。
一、版本与 Java 版本约束
QWEN.md 明确了该 SDK 的版本发布形态:
- 该 Maven 包发布为
com.alibaba:qwencode-sdk:0.1.0-alpha,要求Java 11 或更高版本; - Java 8 用户必须继续使用
0.0.3-alpha,因为 0.1.0-alpha 将整个 artifact 的最低 Java 版本从 8 提升到了 11。
这一约束同样写入了 RELEASE.md 与 README.md。需要注意的是,0.1.0-alpha提升最低 Java 版本影响的是整个 artifact,而非仅新增的 daemon API——旧的 stdio API 虽然保持源码兼容(source-compatible),但同样运行在 Java 11 之上。从 pom.xml 可以看到编译器配置为maven.compiler.release=11,构建与发布还要求 Maven >= 3.9.2。
二、双 API 架构:daemon 传输与 legacy stdio 传输
SDK 在同一 artifact 内包含两套彼此隔离的实现:
| API 包 | 定位 | 传输方式 | 资源模型 |
|---|---|---|---|
com.alibaba.qwen.code.daemon | 推荐 API(0.1.0-alpha 新增) | 通过 REST 突变 + 可续传 SSE 与qwen serve通信 | 自持有界(bounded)的 HTTP、prompt、维护与定时器线程池 |
com.alibaba.qwen.code.cli | 实验性 legacy API(保持源码兼容) | 通过子进程(child process)与 Qwen Code CLI 交互 | 基于QwenCodeCli、Session、ProcessTransport |
QWEN.md 特别强调:daemon 包在实现上有意独立于 legacy 的进程传输(process transport)、DTO、会话模型与全局执行器(global executor)。也就是说,daemon API 没有复用QwenCodeCli那套全局线程池(其默认配置为 30 核心 / 100 最大线程),而是为每个DaemonClient实例自建一套受控资源,这一点从 DaemonClient.java 的构造逻辑可以清楚看到——它按maximumConcurrentPrompts派生 worker、maintenance、future、http、stream-close 等多组独立线程池,并全部使用 daemon 线程工厂创建。
推荐 API 的核心特性
- 与
qwen serve通过 REST 和 SSE 通信; - 默认创建thread 作用域(thread-scoped)的独立会话;
- 在没有可靠 prompt 终结事件(terminal)时失败即关闭,绝不把部分输出当作成功;
- 当守护进程通告
client_heartbeat能力时,使用周期性心跳保持会话存活。
依赖清单
README.md 列出了完整依赖关系:
- 日志:
org.slf4j:slf4j-api(应用自行选择 SLF4J provider,Logback 仅为测试依赖); - 工具类:
org.apache.commons:commons-lang3; - JSON:Fastjson2 用于编码、Jackson Core 用于严格解码;
- 测试:JUnit 5(
org.junit.jupiter:junit-jupiter)。
三、安装与构建
Maven 依赖
在pom.xml中加入:
<dependency> <groupId>com.alibaba</groupId> <artifactId>qwencode-sdk</artifactId> <version>0.1.0-alpha</version> </dependency>Gradle 依赖
implementation 'com.alibaba:qwencode-sdk:0.1.0-alpha'Maven 构建与校验
QWEN.md 给出的标准命令:
mvn test # 运行单元测试 mvn checkstyle:check # 代码风格检查(checkstyle.xml) mvn package # 打包 JAR从 pom.xml 可以看出,构建链还集成了 JaCoCo 覆盖率、source/javadoc 附属包、GPG 签名以及 Sonatype Central 发布插件,产物会声明Automatic-Module-Name: qwencode.sdk。
针对真实守护进程的 E2E 测试
README.md 提供了从仓库源码运行真实 daemon 集成测试的步骤(需先构建 workspaces 与根 CLI bundle):
npm run build npm run bundle npx tsx scripts/run-java-daemon-sdk-e2e.ts注意:npm run build本身不会刷新dist/cli.js,E2E 测试启动该 bundle,缺失时会报出明确的前置错误。对应测试实现见 DaemonServeE2ETest.java。
四、快速上手:DaemonClient+DaemonSessionClient
最小示例:promptText
README 给出了最简洁的用法——先启动qwen serve,然后创建独立的 thread 作用域会话,promptText只在收到匹配的turn_complete后返回;不完整的数据流会抛PromptOutcomeIndeterminateException,而不是把部分文本当成功返回:
import com.alibaba.qwen.code.daemon.DaemonClient; import com.alibaba.qwen.code.daemon.DaemonSessionClient; import com.alibaba.qwen.code.daemon.PromptTextResult; import java.net.URI; try (DaemonClient daemon = DaemonClient.builder() .baseUri(URI.create("http://127.0.0.1:4170")) .build(); DaemonSessionClient session = daemon.createSession()) { PromptTextResult result = session.promptText("Explain this repository"); System.out.println(result.getText()); }预置会话 ID
需要在创建前分配会话身份的调用方,可以传入 RFC UUID v1-v5 形式的 ID。SDK 会在发起突变前检查session_id_override能力,若守护进程返回的 ID 与请求不一致,会报告为SessionCreationOutcomeUnknownException:
CreateSessionRequest request = CreateSessionRequest.builder() .sessionId("550E8400-E29B-41D4-A716-446655440000") .build(); try (DaemonSessionClient session = daemon.createSession(request)) { System.out.println(session.getSession().getSessionId()); }守护进程会把 ID 规范化为小写并创建一个新的 thread 会话——这不是幂等的 attach 操作;当创建结果不明确时,应当用已知 ID 去恢复,而不是重试创建。
带认证的连接
如果qwen serve要求认证,在DaemonClientbuilder 上追加.bearerToken(...)即可。SDK 在 REST 与 SSE 请求上都会携带 Bearer 头,且绝不会把它放进 URL:
DaemonClient daemon = DaemonClient.builder() .baseUri(URI.create("http://127.0.0.1:4170")) .bearerToken(System.getenv("QWEN_SERVER_TOKEN")) .build();五、DaemonClientBuilder 配置项(源码级默认值)
以下默认值可直接从 DaemonClient.java 的Builder字段核实:
| 配置项 | 默认值 | 说明 |
|---|---|---|
baseUri(URI) | http://127.0.0.1:4170 | daemon 地址,必须为不含凭据、query、fragment 的绝对 HTTP(S) 源 |
bearerToken(String) | 无 | 附加Authorization: Bearer ...头 |
connectTimeout(Duration) | 10 秒 | HttpClient 建连超时 |
requestTimeout(Duration) | 30 秒 | 每个有限 JSON/错误体的请求超时(收到响应头不结束该超时) |
promptObservationTimeout(Duration) | 30 分钟 | 本地 prompt 观察(SSE 读取)总预算 |
sseIdleTimeout(Duration) | 45 秒 | SSE 空闲看门狗:无活动则主动关闭流 |
heartbeatInterval(Duration) | 1 分钟 | 自动心跳间隔;设为Duration.ZERO可关闭 |
maximumReconnectAttempts(int) | 8 | SSE GET 的最大重连次数(指数退避 + 全抖动) |
maximumSseFrameBytes(int) | 16 MB(16384 KB) | 单帧字节上限,最低 1024 |
maximumConcurrentPrompts(int) | 32 | 每个客户端最大并发 prompt 数 |
maximumConcurrentPrompts是资源预算的核心:worker 池、future 发布池、stream-close 池的容量都由它派生(例如 stream-close 池容量为maximumConcurrentPrompts * 2,对应"每个 prompt 槽位允许一个正在排干的清理任务")。当能力耗尽时,后续startPrompt会抛DaemonClientCapacityException,而不是无界增长线程或排队任务。
六、会话与 Prompt 请求配置
CreateSessionRequest
见 CreateSessionRequest.java:
workspaceCwd(String):会话工作目录;approvalMode(DaemonApprovalMode)/rawApprovalMode(String):审批模式(wire 值);sessionScope(String):thread(默认)或single,二选一;sessionId(String):可选的自定义会话 ID(RFC UUID 风格)。
PromptRequest
见 PromptRequest.java:
text(String)便捷工厂:构造单个文本块;builder().addText(...)/addContent(Map):构造多内容块 prompt;deadline(Duration):请求守护进程侧的绝对截止时间。取值范围为 1 到 2,147,483,647 毫秒(对齐 daemon 的 Node 定时器范围)。只有在守护进程通告prompt_absolute_deadline能力时才会被接受,否则在发送前直接失败,避免服务端静默忽略;observationTimeout(Duration):仅约束本地 SSE 观察时间,不会发出任何取消突变,与deadline相互独立。
七、传输契约与 Wire Flow
单次 prompt 的线上流程
设计文档 java-daemon-sdk-alpha.md 给出了明确的五步流程:
- 发送一次不重试的
POST /session/:id/prompt; - 要求返回
202,并校验响应体中的{promptId, lastEventId, eventEpoch?}(admission watermark,准入水位); - 以水位作为
Last-Event-ID打开GET /session/:id/events;当 daemon 提供 epoch 时附带X-Qwen-Event-Epoch头; - 只重放并观察与该 prompt 相关的事件,同时将会话级失败帧(如
client_evicted、session_died、state_resync_required)视为致命; - 仅在匹配的
turn_complete或turn_error时停止。
这种按 prompt 订阅的方式天然覆盖了在202响应到达客户端之前就已发出的事件,无需未知 prompt 缓存,也不需要长驻的会话泵(session pump)。
HTTP 传输细节
- 使用 JDK
HttpClient,强制HTTP/1.1,从不跟随重定向; - 每个请求都发送 JSON 或 event-stream 的
Accept头、配置了 Bearer 认证时的认证头,以及创建会话后 daemon 下发的X-Qwen-Client-Id; - SSE 额外发送
Accept-Encoding: identity、Cache-Control: no-cache与Last-Event-ID;可用时携带X-Qwen-Event-Epoch光标; - 有限 JSON 与错误体由有界订阅者消费,并通过
sendAsync与请求截止时间赛跑;SSE 非成功响应体的预算取请求预算与 prompt 观察预算的较小值。
SSE 解析与重连
DaemonSessionClient.java 展示了严格的帧校验逻辑:
- 支持 LF/CRLF 换行、注释、多行
data:;UTF-8 严格解码; - 校验帧、事件名、信封版本、数字 ID、SSE/envelope ID 一致性;
- ID 小于等于已提交光标的事件视为重复,不投递;下一个数字事件必须恰好是
cursor + 1,否则判定 ID 缺口并失败关闭; - 无 ID 的合成事件仅接受守护进程文档化的控制帧(
client_evicted、slow_client_warning、stream_error、state_resync_required、replay_complete),且不推进光标;无 ID 的内容或终结事件直接失败关闭; - 只重连 SSE GET,采用有界指数全抖动退避(上限 5 秒)、流断开后的 SSE
retry指令、以及可重试 HTTP 响应上的Retry-After(上限 5 秒,可解析 RFC 1123 时间);突变请求绝不自动重试。
事件纪元(Event Epoch)
SDK 会从 prompt 准入结果种子化 epoch,从校验通过的 SSE 响应头学习新 epoch(用于兼容),在响应省略头时保留已知值,并且在 prompt 观察期间检测到 epoch 变化时失败关闭——这是 #7458 重启安全事件光标纪元的核心契约。
八、可靠性与失败关闭语义
终结事件的唯一权威性
只有匹配的turn_complete和turn_error是终结事件;队列(queue)与prompt_cancelled事件仅是建议性(advisory)的。本地超时会停止观察,但不会自动取消 daemon 端的 turn。当取消、截止、拆除与 agent 结算并发竞争时,daemon 的 exactly-once 闩锁(latch)会发布第一个正式终结事件并压制后续候选——因此 SDK 始终以收到的终结事件为准,绝不根据自己发出的最后一个控制突变来推断结果。
结果不明确的异常分类
RELEASE.md 与设计文档共同定义了完整的异常面:
| 场景 | 异常 |
|---|---|
| 发送后未收到有效 202、或返回 HTTP 408/5xx | PromptAdmissionUnknownException(绝不重发 prompt) |
| 会话创建结果不明确 | SessionCreationOutcomeUnknownException |
| 取消结果不明确 | MutationOutcomeUnknownException |
| detach 结果不明确 | DetachOutcomeUnknownException |
| 无可靠终结事件、观察失败、超时、重连耗尽 | PromptOutcomeIndeterminateException(携带可用的部分文本) |
turn_error终结(promptText场景) | PromptTurnException |
| 文本超出 UTF-8 字节上限 | PromptContentLimitException |
权限(permission)、取消、心跳、detach、删除等突变都采用同样保守的分类——因为中间响应并不能证明 daemon 拒绝了该突变。每个突变每次方法调用最多尝试一次。
promptText()只收集 assistant 文本,强制 UTF-8 字节上限(默认 4 MB,见DaemonSessionClient.DEFAULT_MAXIMUM_TEXT_BYTES),并且仅在匹配的turn_complete时返回PromptTextResult。
幂等关闭与销毁
close()本地幂等,停止本地观察,至多尝试一次 detach;丢失的 detach 响应不重试;destroySession()是唯一会发出DELETE /session/:id的 API,可在 detach 之后调用;- 结果不明确的完成是会话的终结边界(outcome boundary)而非可复用边界:一旦准入结果未知或已准入 prompt 以不明确方式结束,该
DaemonSessionClient会永久拒绝后续 prompt,即使本地流清理成功也必须关闭或销毁会话。
九、能力协商与自动心跳
创建前的能力校验
DaemonClient.java 展示了会话创建前的硬性校验:
- 必须先读取
GET /capabilities(version 必须为 1); - 要求 daemon 通告
rest传输与session_scope_override,否则拒绝创建——防止旧 daemon 静默忽略请求的 thread 作用域而把客户端挂到共享会话上; - 请求自定义 session ID 时,额外要求
session_id_override能力。
DaemonCapabilities(见 DaemonCapabilities.java)暴露getVersion()、getMode()、getFeatures()、getTransports()、getWorkspaceCwd()、getQwenCodeVersion()与supports(feature)。
自动心跳
当 daemon 通告client_heartbeat时,会话保持打开期间 SDK 会按配置间隔(默认 1 分钟)发送一次新的心跳突变,直至 detach 或 destroy。心跳具有正常的有限请求截止时间且不重试;将heartbeatInterval设为Duration.ZERO可关闭自动保活。从源码可见,若心跳返回 404/405 会被视为能力不再支持而停止(DaemonSessionClient.java)。
十、流式回调:startPrompt与PromptObserver
需要按序获取文本、思考、工具、用量、权限与原始事件时,使用startPrompt配合PromptObserver。观察者接口(PromptObserver.java)提供六类默认方法:
onText(String, DaemonEvent)onThought(String, DaemonEvent)onTool(Map<String,Object>, DaemonEvent)onUsage(Map<String,Object>, DaemonEvent)onPermission(PermissionRequest, DaemonEvent)onEvent(DaemonEvent)
回调执行语义(设计文档明确):
- 回调在客户端自有 daemon 线程上串行执行;事件光标只在所有适用回调成功返回后才推进;
- 回调必须快速返回,不得等待同一个
PromptCall,不得在回调中关闭或销毁同一会话; - 从回调中响应权限请求是被支持的;当 daemon 报告该请求已解决或不再挂起时,响应方法返回
false。
startPrompt立即返回PromptCall,其acceptanceFuture()(daemon 已接受该 prompt)与completionFuture()(turn 已可靠终结)相互独立,调用方可区分"daemon 接受了 prompt"与"本轮可靠结束"两个阶段。取消未来视图不会取消 daemon 端的 prompt——需要会话级取消请调用cancelActivePrompt(),并且仍要等待匹配的终结事件;协作式取消以stopReason=cancelled的turn_complete结束,取消过程中 agent 或 provider 失败则可能产生turn_error。
十一、已知 Alpha 限制
QWEN.md 与 RELEASE.md 一致列出以下边界,接入时务必知晓:
- 不承诺跨 daemon 重启的 exactly-once 执行,不支持自动 epoch 恢复、snapshot/resync、持久化光标或真正按 prompt ID 定向取消;
- 创建时选择模型(creation-time model selection)故意不暴露:当前 daemon 只通过 create 响应之前发出的 SSE 事件报告
modelServiceId被拒,而按 prompt 订阅从后续准入水位开始,无法证明模型已生效; - 模棱两可的创建可能遗留未知会话:daemon 可能保留一个 ID 从未到达调用方的会话,SDK 不重试创建也无法 detach,恢复边界是 daemon 侧的生命周期回收(reaping);
- 确认式取消握手没有"仅确认"超时:这是刻意的——加入仅确认超时会让迟到的会话级取消可能到达后继 prompt,破坏 FIFO 取消排干栅栏。因此,一个无限期忽略其
AbortSignal的 provider/工具/自定义集成可能让取消结果未知、会话不可用,直到更强的运行时隔离出现; - 配套 daemon 版本要求:0.1.0-alpha 的生命周期保证依赖与 SDK 同一源码修订发布的 qwen-code 构建,其必须包含按客户端 detach 台账(#7386)、按 epoch 终结保证(#7400)与重启安全事件光标纪元(#7458),以及本次发布的确认式准入取消 + FIFO 取消排干栅栏。仅 #7400 一个提交是不够的。
十二、验证与测试体系
设计文档与 daemon 测试目录 展示了完整验证矩阵:
- 单元测试(
DaemonSessionClientTest、HttpSupportTest、JsonSupportTest、SseReaderTest)使用进程内 HTTP 服务器注入:SSE 分片、慢速单行投递、重放、重复、缺口、冲突的 prompt ID、不透明未来事件数据、水位重放、断开、压缩响应、停滞的有限响应体、事件纪元传播与不匹配、resync、观察者失败、缺失终结事件、模糊突变响应等; - 生命周期测试覆盖:单本地 prompt 准入、准入/关闭串行化、截止终结后会话复用、取消完成、拆除终结排序、有界文本、自动心跳、幂等关闭、detach 客户端身份、detach-once、显式 destroy;
- CI在 Linux 上以 Java 11/17/21 编译测试,macOS/Windows 覆盖 Java 21 smoke;Linux CI 与受保护发布流程针对真实
qwen serve进程(临时 workspace + model stub)跑 E2E,即DaemonServeE2ETest。
十三、继续深入阅读
- 模块总览与 API 示例:README.md、QWEN.md
- 版本历史与发布契约:RELEASE.md
- 实现设计文档:java-daemon-sdk-alpha.md
- 核心实现:DaemonClient.java、DaemonSessionClient.java、PromptRequest.java
- 构建配置:pom.xml
适用前提提醒:本文所有行为均基于当前仓库0.1.0-alpha的 daemon 传输实现;使用前请确认你的 Java 运行时为 11+、daemon 为与 SDK 同源码修订的 qwen-code 构建,并将 Java 8 项目锁定在0.0.3-alpha。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考