Dify SSE 流式调用实战:执行进度、运行 ID 与断线核实

通过 API 调用工作流时,短文档可以等待一份完整 JSON;文件多了、模型调用慢了,使用者就容易面对长时间没有反馈的等待。

SSE 能在执行过程中持续传回事件。本篇承接第十篇的文件上传接口,增加流式事件解析、运行 ID 保存和断线后的只读查询,让程序既能观察执行过程,也能区分"收到消息""连接断开"和"任务完成"。

正文提供独立 Python 客户端、Mac 与 Windows 运行步骤、演示文件及本地构造的协议示例。客户端同时支持 run 和 query,不依赖上一篇的 Python 文件。

1. 本次流程与适用环境

text 复制代码
run:本地 TXT → 上传文件 → POST streaming → 逐帧读取事件
                                      ├─尽早保存 task_id / workflow_run_id
                                      ├─记录节点事件
                                      └─完成事件 → 校验状态与 outputs → 保存报告

断线或未完成:保留已有日志与运行 ID
query:人工使用已知 workflow_run_id → GET 运行详情 → 核实状态与输出
项目 本文约定
Dify 社区版 1.17.1,自部署
客户端 Python 3.11 或以上版本,requests
系统 macOS 终端 / Windows PowerShell
应用 已发布 Workflow
文件输入 inputs.documents,File List,本地 TXT,1~3 份
最终输出 report:String;data:Object
业务记录 每份文件有序号、文件名及 success / skipped / failed 状态

资料核对日期:2026-10-22。工作流业务配置沿用第九篇的文件清单与失败保位设计;接口输入与输出沿用第十篇。本篇关注外部程序如何读取流式结果,不重新搭建业务节点。

SSE 提供持续反馈,但仍受网络、代理、客户端和服务端配置影响,不保证连接永远不断,也不使一次新的 POST 自动变成上次运行的续接。官方响应模式说明

2. 请求体改为 streaming,读取方式也要改变

文件上传仍是 POST /v1/files/upload,取得根级 id 后组装自定义文件列表。关键运行入参是:

json 复制代码
{
  "inputs": {
    "documents": [
      {
        "type": "document",
        "transfer_method": "local_file",
        "upload_file_id": "00000000-0000-4000-8000-000000000001"
      }
    ]
  },
  "response_mode": "streaming",
  "user": "csdn-demo-user-001"
}

这里的 UUID 是占位示例,程序会填入实际上传返回的 ID。上传与运行使用同一应用 API Key、同一个稳定 user,输入名必须与 Workflow 的 documents 一致。

客户端发送请求时设置 stream=True,先检查 HTTP 状态和响应 Content-Type 是否为 text/event-stream,再按事件读取;不能对整条 SSE 响应调用 response.json()。官方流式调用说明、Requests 流式读取

注意两个参数的职责:response_mode 告诉 Dify 返回 SSE,requests 的 stream=True 告诉客户端逐步读取响应。只改其中一个,不等于完成了整条流式调用逻辑。

3. 看懂 SSE 的行、帧和 JSON 事件

固定版本常见线格式:

text 复制代码
event: ping

data: {"event":"workflow_started","task_id":"...","workflow_run_id":"...","data":{...}}

data: {"event":"node_finished","task_id":"...","workflow_run_id":"...","data":{...}}

上面的省略号只用于解释结构,不是可以解析或提交的完整 JSON。

三个要点:

  1. SSE 以空行分隔事件帧,不以任意一次网络读取作为事件边界。
  2. 普通业务帧的 data: 内容是 JSON,事件种类看 JSON 中的 event 字段。
  3. event: ping 是没有 JSON 数据的心跳帧,不要拿它做 JSON 解析,也不要把它当作工作流开始或完成。

Dify 1.17.1 初始订阅会先发送心跳,默认心跳间隔约 10 秒;仍应以收到 workflow_started 等实际事件判断执行进展。固定版本心跳与流读取、消息生成器默认配置

通用 SSE 还允许同一帧有多行 data:,应以换行拼接后再解析;文本使用 UTF-8。读取块可能切在 CRLF 换行或中文字符中间,解析器需要处理这些边界。本例实现逐行与分帧逻辑,不把它称为完整的浏览器 EventSource 实现。SSE 标准

4. 哪些事件要处理,哪些信息不能误读

事件 客户端处理
workflow_started 尽早保存 task_id、workflow_run_id
node_started 记录节点开始事件,展示安全的 node_id
node_finished 记录节点状态,继续等待工作流层面的结果
text_chunk 可记入日志;本例不把它拼成最终报告
workflow_finished 保存完成事件,再校验运行状态和 outputs
error 保存已有信息,结束本次读取并报错
workflow_paused 记录暂停,明确结果尚未完成

4.1 两个 ID 的用途

标识 用途
task_id 执行中的任务控制,例如停止任务
workflow_run_id 识别某一次运行,用于查询运行详情
workflow_id 工作流版本标识,不是本次运行 ID

普通工作流与节点事件带有顶层 task_id、workflow_run_id。节点事件里的 data.id 是节点执行 ID,不能拿它查询整条工作流。

error 是一个例外:固定版本可以没有 task_id,且开始之前的错误可能让 workflow_run_id 为 null。因此,客户端只在收到有效 ID 时保存;没有 ID 的 error 仍必须进入错误处理,不能因为"双 ID 不齐"而忽略错误。固定版本事件模型、工作流事件转换器

4.2 节点失败不等于立即结束整个客户端

第九篇的 Continue on Error 和 LLM 异常分支允许记录单项失败后继续处理其他文件。收到某个 node_finished 的失败状态时,不能立即丢掉后续事件;最后可能仍会得到 partial-succeeded 和完整的逐份报告。

同样,text_chunk 不保证出现。没有文字片段不代表没有执行,最终报告以已校验的完成事件或详情查询的 outputs 为准。固定版本节点与输出处理

4.3 不把节点数换算成通用百分比

分支、迭代和重试会改变节点的实际执行次数。某个 node_id 可以多次出现,事件中的 index 也不是完成百分比。本例只展示收到的事件和节点标识,不生成一个缺少可靠分母的进度百分比。

5. 准备文件与依赖

5.1 api-notes-a.txt

text 复制代码
星舟团队账号资料安排(API 演示资料 A)
记录日期:2026-10-22
以下人员和安排均为虚构。

王宁负责在 2026-10-27 前整理账号申请说明,补充服务台入口和必填信息。
说明必须保留"1 个工作日内反馈进度",不能改成承诺在 1 个工作日内完成开通。
团队还决定补充常见问题清单,但没有指定负责人和截止时间。

5.2 api-notes-b.txt

text 复制代码
星舟团队反馈核对安排(API 演示资料 B)
记录日期:2026-10-22
以下人员和安排均为虚构。

陈澄负责在 2026-10-28 前核对 8 条试用意见,保留原文并标注重复记录。
8 条意见是待核对的记录数量,不代表已经确认 8 个独立缺陷。
有人提出自动分类的建议,但本次没有形成实施决定;正式上线日期仍未确定。

分别保存为 UTF-8 TXT。第一次只用 A 文件,确认流程后再同时上传两份。

依赖文件 dify-api-requirements.txt:

text 复制代码
requests>=2.32,<3

macOS,在资料所在目录执行:

bash 复制代码
python3 -m venv .venv
.venv/bin/python -m pip install -r dify-api-requirements.txt

Windows PowerShell:

powershell 复制代码
py -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r dify-api-requirements.txt

如果 Windows 没有 py 命令,用本机已安装的 Python 3 启动命令创建环境。后面直接调用虚拟环境里的解释器,不需要激活环境或修改执行策略。

6. 完整独立客户端

保存为 dify-workflow-sse-client.py:

python 复制代码
"""Dify 1.17.1: run 1-3 TXT files over SSE, or query a known run ID; needs requests."""
import argparse
import codecs
import getpass
import json
import os
from pathlib import Path
import re
import sys

try:
    import requests
except ImportError:
    print("stage=dependency error_class=ModuleNotFoundError", file=sys.stderr)
    raise SystemExit(1)

TIMEOUT = (10, 70)


class IncompleteRun(Exception):
    pass


def safe_token(value):
    return isinstance(value, str) and re.fullmatch(r"[A-Za-z0-9_-]{1,128}", value) is not None


def save_json(path, value):
    path.write_text(json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False) + "\n", encoding="utf-8")


def check_http(response):
    if not 200 <= response.status_code < 300:
        raise requests.HTTPError()


def validate_data(data, paths=None):
    if not isinstance(data, dict):
        raise ValueError()
    names = ("record_count", "success_count", "skipped_count", "failed_count")
    if any(type(data.get(name)) is not int or data[name] < 0 for name in names):
        raise ValueError()
    records = data.get("records")
    if not isinstance(records, list) or not 1 <= len(records) <= 3:
        raise ValueError()
    if data["record_count"] != len(records) or (paths is not None and len(records) != len(paths)):
        raise ValueError()
    if sum(data[name] for name in names[1:]) != data["record_count"]:
        raise ValueError()
    counts = {"success": 0, "skipped": 0, "failed": 0}
    for index, row in enumerate(records, start=1):
        if not isinstance(row, dict) or row.get("status") not in counts:
            raise ValueError()
        if type(row.get("file_index")) is not int or row["file_index"] != index:
            raise ValueError()
        name = row.get("filename")
        if not isinstance(name, str) or not name.strip() or re.search(r"[/\\\x00-\x1f]", name):
            raise ValueError()
        if paths is not None and name != paths[index - 1].name:
            raise ValueError()
        counts[row["status"]] += 1
    if any(data[status + "_count"] != count for status, count in counts.items()):
        raise ValueError()


def utf8_lines(response):
    decoder = codecs.getincrementaldecoder("utf-8")(errors="strict")
    line, after_cr = [], False
    for chunk in response.iter_content(chunk_size=1):
        for char in decoder.decode(chunk):
            if after_cr and char == "\n":
                after_cr = False
                continue
            after_cr = char == "\r"
            if char in "\r\n":
                yield "".join(line)
                line.clear()
            else:
                line.append(char)
    decoder.decode(b"", final=True)
    # Do not turn an unterminated last line into an extra newline.


def sse_objects(response):
    data_lines = []
    for line in utf8_lines(response):
        if not line:
            if data_lines:
                value = json.loads("\n".join(data_lines))
                if not isinstance(value, dict):
                    raise ValueError()
                yield value
            data_lines = []
        elif not line.startswith(":"):
            field, _, value = line.partition(":")
            if field == "data":
                data_lines.append(value[1:] if value.startswith(" ") else value)
    # An unterminated frame at EOF is not a completed SSE event.


def remember_ids(payload, identifiers, destination):
    sources = [payload]
    if payload.get("event") == "workflow_paused" and isinstance(payload.get("data"), dict):
        sources.append({"workflow_run_id": payload["data"].get("workflow_run_id")})
    updated = dict(identifiers)
    for source in sources:
        for name in ("workflow_run_id", "task_id"):
            value = source.get(name)
            if value is None:
                continue
            if not safe_token(value) or (name in updated and updated[name] != value):
                raise ValueError()
            updated[name] = value
    if updated != identifiers:
        save_json(destination / "run-ids.json", updated)
        identifiers.update(updated)


def read_stream(response, destination):
    check_http(response)
    content_type = response.headers.get("Content-Type", "").split(";", 1)[0].strip().lower()
    if content_type != "text/event-stream":
        raise ValueError()
    response.encoding = "utf-8"
    identifiers = {}
    with (destination / "events.jsonl").open("w", encoding="utf-8") as log:
        for payload in sse_objects(response):
            log.write(json.dumps(payload, ensure_ascii=False, allow_nan=False) + "\n")
            log.flush()
            remember_ids(payload, identifiers, destination)
            event = payload.get("event")
            if not safe_token(event):
                raise ValueError()
            data = payload.get("data")
            node_id = data.get("node_id") if isinstance(data, dict) else None
            print("event=" + event + (" node_id=" + node_id if safe_token(node_id) else ""))
            if event == "error":
                raise RuntimeError()
            if event == "workflow_paused":
                print("workflow_status=paused result=incomplete")
                raise IncompleteRun()
            if event == "workflow_finished":
                save_json(destination / "workflow-finished.json", payload)
                return data
    raise IncompleteRun()


def main(argv=None):
    parser = argparse.ArgumentParser(description=__doc__)
    commands = parser.add_subparsers(dest="mode", required=True)
    run = commands.add_parser("run", help="Upload TXT files and start one streaming run")
    run.add_argument("files", nargs="+")
    run.add_argument("--output-dir", default="sse-result")
    query = commands.add_parser("query", help="Read details of a known run; does not resume SSE")
    query.add_argument("--run-id", required=True)
    query.add_argument("--output-dir", default="query-result")
    args = parser.parse_args(argv)
    stage = "preflight"
    try:
        paths = [Path(value) for value in args.files] if args.mode == "run" else None
        if paths is not None:
            if not 1 <= len(paths) <= 3 or any(not p.is_file() or p.suffix.lower() != ".txt" for p in paths):
                raise ValueError()
        elif not safe_token(args.run_id):
            raise ValueError()
        stage = "output_dir"
        destination = Path(args.output_dir)
        destination.mkdir(parents=True, exist_ok=False)
        stage = "configuration"
        base = os.getenv("DIFY_BASE_URL", "http://localhost/v1").rstrip("/")
        user = os.getenv("DIFY_USER", "csdn-demo-user-001").strip()
        key = os.getenv("DIFY_API_KEY", "").strip() or getpass.getpass("Dify API Key: ").strip()
        if not key or not base.startswith(("http://", "https://")) or (paths is not None and not user):
            raise ValueError()
        with requests.Session() as session:
            session.headers.update({"Authorization": "Bearer " + key})
            if args.mode == "query":
                stage = "query"
                with session.get(base + "/workflows/run/" + args.run_id,
                                 timeout=TIMEOUT, allow_redirects=False) as response:
                    result = response.json()
                    save_json(destination / "run-detail.json", result)
                    check_http(response)
                if isinstance(result, dict) and result.get("id") not in (None, args.run_id):
                    raise ValueError()
            else:
                documents = []
                for index, path in enumerate(paths, start=1):
                    stage = "upload_" + str(index)
                    with path.open("rb") as handle:
                        response = session.post(base + "/files/upload", data={"user": user},
                                                files={"file": (path.name, handle, "text/plain")},
                                                timeout=TIMEOUT, allow_redirects=False)
                    check_http(response)
                    uploaded = response.json()
                    if not isinstance(uploaded, dict) or not isinstance(uploaded.get("id"), str) or not uploaded["id"].strip():
                        raise ValueError()
                    documents.append({"type": "document", "transfer_method": "local_file", "upload_file_id": uploaded["id"]})
                stage = "stream_read"
                with session.post(base + "/workflows/run", stream=True,
                                  json={"inputs": {"documents": documents}, "response_mode": "streaming", "user": user},
                                  timeout=TIMEOUT, allow_redirects=False) as response:
                    result = read_stream(response, destination)
        stage = "workflow_status"
        if not isinstance(result, dict):
            raise ValueError()
        status = result.get("status")
        if status in {"running", "paused"}:
            print("workflow_status=" + status + " result=incomplete")
            raise IncompleteRun()
        if status not in {"succeeded", "partial-succeeded"}:
            raise RuntimeError()
        stage = "validate_outputs"
        outputs = result.get("outputs")
        if not isinstance(outputs, dict) or not isinstance(outputs.get("report"), str):
            raise ValueError()
        data = outputs.get("data")
        validate_data(data, paths)
        stage = "save_outputs"
        save_json(destination / "data.json", data)
        (destination / "report.md").write_text(outputs["report"], encoding="utf-8")
        print("workflow_status=" + status)
        print(f"record_count={data['record_count']} success={data['success_count']} "
              f"skipped={data['skipped_count']} failed={data['failed_count']}")
        return 0
    except Exception as error:
        print(f"stage={stage} error_class={type(error).__name__}", file=sys.stderr)
        return 2 if isinstance(error, IncompleteRun) else 1


if __name__ == "__main__":
    raise SystemExit(main())

这份代码有两个入口:

  • run:校验全部文件、上传、启动一次流式执行,并在完成后保存报告。
  • query:对已知运行 ID 发一次 GET,只查询原运行,不重新上传或发起 POST。

程序要求结果目录此前不存在。API Key 从 DIFY_API_KEY 读取,未设置则在自己的终端隐藏输入;终端不输出服务端原始错误正文,只显示事件、阶段和错误类。原始事件仍保存在本地日志中。

客户端检查数量、状态计数、记录顺序和文件名等接口契约,不证明摘要事实和任务判断正确,也不因为成功返回报告就宣称所有文件都 success。

utf8_lines 使用增量 UTF-8 解码,并把跨读取块的 CRLF 作为一个换行处理;sse_objects 再按真实空行派发事件。示例选用很小的读取块便于及时处理少量执行事件,这不是高吞吐采集方案;代理层缓冲也不能靠客户端缩小读取块消除。

7. 启动一次流式执行

7.1 macOS

bash 复制代码
export DIFY_BASE_URL="http://localhost/v1"
export DIFY_USER="csdn-demo-user-001"
.venv/bin/python dify-workflow-sse-client.py run api-notes-a.txt --output-dir sse-result-001

7.2 Windows PowerShell

powershell 复制代码
$env:DIFY_BASE_URL = "http://localhost/v1"
$env:DIFY_USER = "csdn-demo-user-001"
.\.venv\Scripts\python.exe dify-workflow-sse-client.py run api-notes-a.txt --output-dir sse-result-001

将 Base URL 改成自己的 API 地址。API Key 使用目标 Workflow 应用的密钥,不是模型供应商密钥。客户端在另一台电脑时,不要继续用 localhost 指向自己的电脑。官方鉴权与基础地址说明

需要两份文件时,保持同样的配置并使用新的结果目录:

macOS:

bash 复制代码
.venv/bin/python dify-workflow-sse-client.py run api-notes-a.txt api-notes-b.txt --output-dir sse-result-002

Windows PowerShell:

powershell 复制代码
.\.venv\Scripts\python.exe dify-workflow-sse-client.py run api-notes-a.txt api-notes-b.txt --output-dir sse-result-002

路径有空格时,将每个完整路径分别放进引号中。文件名和序号按这次提交顺序核对;同名文件仍按序号区分。

终端中可能依次看到:

text 复制代码
event=workflow_started
event=node_started node_id=某个实际节点ID
event=node_finished node_id=某个实际节点ID
event=workflow_finished
workflow_status=succeeded
record_count=1 success=1 skipped=0 failed=0

这是格式示意,不是实测日志;事件数量、顺序中的具体节点和最终计数要以自己的实际运行为准。

8. 日志和结果分别在什么地方

完成一次可交付运行后,结果目录可能包含:

text 复制代码
sse-result-001/
  run-ids.json
  events.jsonl
  workflow-finished.json
  report.md
  data.json
文件 什么时候产生,如何使用
run-ids.json 收到有效运行标识时尽早保存;先检查是否存在 workflow_run_id
events.jsonl 每个解析出的 JSON 事件一行,逐条写入并 flush
workflow-finished.json 收到 workflow_finished 后保存完整事件
report.md 完成状态与输出检查通过后保存的报告
data.json 对应业务记录与计数

心跳和注释不作为业务 JSON 写入 events.jsonl。若在开始事件前报错,可能没有运行 ID 文件;若流中途断开,也可能只有日志和 ID,没有正常报告。

这类日志可以包含输入、模型输出、节点错误等业务资料,不是只有事件名的空日志。给文章补截图时应使用演示材料,并检查日志中是否含需要遮盖的内容。

9. 只有完成事件也还需要读状态

对 workflow_finished 事件对象,业务取值是:

text 复制代码
event.data.status
event.data.outputs.report
event.data.outputs.data

JSON 的顶层 event 字段此时等于 workflow_finished。不能把该帧当成阻塞接口的整份响应,或再往下找一个额外的工作流包装层。

客户端接受 succeeded、partial-succeeded 后继续检查输出;失败或停止状态不会生成正常交付报告。准确的部分成功拼写带连字符,仍需要看每份记录的 success、skipped、failed 计数。固定版本完成事件转换

普通 Workflow 没有需要等待的 [DONE] 标志。这里以 workflow_finished 处理终态,以 error 处理错误;workflow_paused 是未完成状态,EOF 也不是成功证据。固定版本终止条件

9.1 本地构造的 SSE 序列

下面是裁剪字段后的教学样例,展示两份文件的部分成功结果;不是完整的真实执行轨迹。UUID、时间、节点错误和报告均为本地构造,摘要从简。实际客户端会保留服务端发送的完整 JSON 事件。

text 复制代码
: local constructed demonstration, not a Dify trace

event: ping

data: {"event":"workflow_started","task_id":"00000000-0000-4000-8000-000000001120","workflow_run_id":"00000000-0000-4000-8000-000000001110","data":{"id":"00000000-0000-4000-8000-000000001110","workflow_id":"00000000-0000-4000-8000-000000001140","created_at":0}}

data: {"event":"node_started","task_id":"00000000-0000-4000-8000-000000001120","workflow_run_id":"00000000-0000-4000-8000-000000001110","data":{"id":"00000000-0000-4000-8000-000000001130","node_id":"demo-extract","node_type":"document-extractor","title":"演示提取节点","index":4,"created_at":0}}

data: {"event":"node_finished","task_id":"00000000-0000-4000-8000-000000001120","workflow_run_id":"00000000-0000-4000-8000-000000001110","data":{"id":"00000000-0000-4000-8000-000000001130","node_id":"demo-extract","node_type":"document-extractor","title":"演示提取节点","index":4,"status":"failed","error":"DEMO_ONLY: locally constructed failure","created_at":0,"finished_at":1}}

data: {"event":"workflow_finished","task_id":"00000000-0000-4000-8000-000000001120","workflow_run_id":"00000000-0000-4000-8000-000000001110","data":{"id":"00000000-0000-4000-8000-000000001110","workflow_id":"00000000-0000-4000-8000-000000001140","status":"partial-succeeded","outputs":{"report":"# SSE 本地模拟报告\n\n共 2 份,成功 1 份,失败 1 份。此文本不是 Dify 或模型实际回复。\n","data":{"record_count":2,"success_count":1,"skipped_count":0,"failed_count":1,"records":[{"file_index":1,"filename":"api-notes-a.txt","status":"success","stage":"complete","summary":"本地构造摘要(非模型实际回复):王宁负责在 2026-10-27 前整理账号申请说明。","error_type":"","message":""},{"file_index":2,"filename":"api-notes-b.txt","status":"failed","stage":"iteration","summary":"","error_type":"IterationItemFailed","message":"本地模拟未返回记录,具体错误仍需查看对应运行日志"}]}},"created_at":0,"finished_at":2}}

最后一个 data 行较长,是同一帧完整 JSON,不要按显示换行随意拆成多个 JSON 对象。若同一帧存在多行 data,解析器会先按 SSE 规则合并它们。

10. 断线后查询原运行,而不是重新提交

10.1 已经保存了 workflow_run_id

打开原目录的 run-ids.json,取出真实 workflow_run_id。下面用终端变量示意替换过程,不包含可直接使用的真实运行 ID。

macOS:

bash 复制代码
RUN_ID="把这里替换为已保存的真实workflow_run_id"
.venv/bin/python dify-workflow-sse-client.py query --run-id "$RUN_ID" --output-dir query-result-001

Windows PowerShell:

powershell 复制代码
$runId = "把这里替换为已保存的真实workflow_run_id"
.\.venv\Scripts\python.exe dify-workflow-sse-client.py query --run-id $runId --output-dir query-result-001

query 使用相同 Dify 服务地址和同一应用 API Key,发出:

http 复制代码
GET /v1/workflows/run/<workflow_run_id>
Authorization: Bearer <同一应用 API Key>

该详情接口不需要请求体,也不要求带 user。结果保存为 run-detail.json;若运行仍是 running 或 paused,程序说明未完成,不写正常报告。本例没有自动轮询;稍后再次核实时使用新的结果目录。固定版本详情接口

10.2 查询详情和完成事件的层级不同

来源 状态 报告
workflow_finished 事件 event.data.status event.data.outputs.report
GET 运行详情 response.status response.outputs.report

查询详情返回对象本身,没有外层 data 包装。官方运行详情说明

下面是另一份本地构造、保留关键字段的详情示例,用来说明取值层级,不是对前述模拟流发起真实查询得到的结果:

json 复制代码
{
  "id": "00000000-0000-4000-8000-000000001110",
  "workflow_id": "00000000-0000-4000-8000-000000001140",
  "status": "partial-succeeded",
  "outputs": {
    "report": "# SSE 本地模拟报告\n\n共 2 份,成功 1 份,失败 1 份。此文本不是 Dify 或模型实际回复。\n",
    "data": {
      "record_count": 2,
      "success_count": 1,
      "skipped_count": 0,
      "failed_count": 1,
      "records": [
        {
          "file_index": 1,
          "filename": "api-notes-a.txt",
          "status": "success",
          "stage": "complete",
          "summary": "本地构造摘要(非模型实际回复):王宁负责在 2026-10-27 前整理账号申请说明。",
          "error_type": "",
          "message": ""
        },
        {
          "file_index": 2,
          "filename": "api-notes-b.txt",
          "status": "failed",
          "stage": "iteration",
          "summary": "",
          "error_type": "IterationItemFailed",
          "message": "本地模拟未返回记录,具体错误仍需查看对应运行日志"
        }
      ]
    }
  },
  "created_at": 0,
  "finished_at": 2
}

query 没有原始命令行文件列表,因此只检查返回记录的内部结构、序号、名称和计数一致性;它不能独立证明这些名称就是自己原来提交的文件。要结合运行 ID、原目录日志和原始提交记录核对。

10.3 没有拿到运行 ID

先检查 events.jsonl 和 Dify 运行日志,确认错误发生在哪个阶段。只有 ping、或在开始前就收到 error 时,可能尚无可查询的运行 ID。不要编造一个 ID,也不要把"本地没有 ID"理解成服务端必定没有执行。

再次 POST 会启动新运行,不是查询旧任务。需要重试时,应先核实已有执行和业务影响,再决定是否重新提交。

11. 详情查询与 SSE 续接是两种操作

Dify 1.17.1 已支持独立的 SSE 续接入口:

http 复制代码
GET /v1/workflow/<workflow_run_id>/events?user=<原user>

这里的 workflow 是单数;与详情查询的 /workflows/run/{id} 不同。续接需要原 user,还有状态快照与暂停相关选项。该版本没有逐事件游标或 Last-Event-ID 续传参数,不能据此宣称断线期间的每段文字都能完整补回。固定版本续接路由、官方续接接口说明

本文客户端没有实现 SSE 续接,query 只做一次只读详情查询。这样先把"保存标识、确认原运行、读取最终结果"跑清楚,再按实际业务决定是否需要事件续接和去重。

12. 超时、暂停和退出码如何理解

timeout=(10, 70) 是客户端连接与读取等待策略,不是 70 秒整条工作流总时限。读取超时应为心跳和正常事件留出空间;代理缓冲或网络中断仍可能影响实际接收。Requests 超时说明

本例处理结果:

退出码 含义
0 取得允许的完成状态,输出通过本例检查并完成保存;不代表每份文件都成功
2 EOF 缺少完成事件,或已知状态为 running / paused 等未完成情况
1 其他异常,包括网络读取错误、error 事件、失败终态或输出不合契约

命令行参数解析错误也可能由 argparse 返回 2,应同时看终端提示,不能只凭退出码诊断网络状态。

程序不会在断线后自动重发 POST、自动查询或自动重连。客户端结束等待不等于服务端已停止;query 也不会恢复暂停中的人工输入流程。

13. 在自己的实例验证这些场景

下面是实际搭建后应完成的检查,属于待实测清单:

场景 检查点
一份正常 TXT 收到开始事件后 ID 已保存;终态与报告记录匹配
两份文件,其中一份业务失败 节点错误后仍收集最终事件,逐份结果不遗漏
首帧只有 ping 不报 JSON 错误,也不显示任务已完成
流读取中断 保留已有 ID 与日志,无自动重发 POST;用 query 核实旧运行
只有 EOF、没有完成事件 不生成正常完成报告
开始前 error、运行 ID 为 null 正确报错,不要求虚构 task_id 或运行 ID
运行暂停 标记未完成,不把暂停当作成功
结果目录已存在 网络请求前拒绝,避免覆盖旧报告
查询仍在运行 保存详情,提示未完成,不读取未就绪输出

正文代码已进行 19 项本地检查,覆盖协议分帧、标识保存、状态处理、只读查询和输出校验;其中包含真实 Requests 2.32.5 的内存响应测试,验证 CRLF 换行及中文字符跨读取块的处理,不发送网络请求。另已核对官方文档与固定版本源码。

没有连接真实 Dify、没有发起实际文件上传或模型调用,也没有在 Windows 执行命令。示例流与详情 JSON 均为本地构造,不能作为真实服务的运行证据。正式接入前需要在自己的实例完成上表的端到端验证。

完成这一层后,可以再把调用逻辑放进自己的后端,统一保存任务状态、控制用户权限,并向业务页面提供结果查询。

相关推荐
一级新生5 小时前
Dify 接入大模型:本地还是云端?实战与选型
人工智能·大模型·dify·ollama
一级新生1 天前
Ollama 是什么、干什么用、怎么用
人工智能·dify·ollama·本地大模型
一级新生1 天前
Dify 日常使用手册(小白版)
人工智能·docker·云原生·eureka·dify·ai工具
情 九2 天前
Dify HTTP 请求节点实战:发送 JSON、解析响应与处理异常
网络协议·http·json·dify
yuanlaile10 天前
大模型应用层开发实战:从 Prompt 工程到 Dify、私有 RAG、Agent 完整落地总结
prompt·agent·dify·大模型应用开发·llm 应用·prompt 工程
SatVision炼金士15 天前
DIFY本地部署开源模型配置ollama,提示修改成功却没有加载模型问题解决
开源·dify·本地部署·ollama
更深兼春远17 天前
第3篇:给 Dify 接上大模型——云端 API 与本地 Ollama 两种方式
大模型·dify·ollama·deepseek·apikey
XLYcmy17 天前
PDF 论文处理器 — 改进方向与建议文档
python·pdf·llm·agent·dify·rag·harness
XLYcmy18 天前
PDF 论文处理器 — 优势与功能文档
大数据·python·网络安全·pdf·dify·rag·漏洞检测