工作流
工作流是 Kaloscope 实现自动化的基础。无论是搜资源、站点登录、自动追更、生成 NFO,还是定时抓取,背后其实都在跑工作流。
每个工作流由若干节点和连接它们的边组成。执行时,引擎会从入口节点出发,沿着连线依次执行各节点,直到走到出口节点或流程结束。每次执行都会新建一个相互隔离的上下文,节点之间就靠这个上下文来共享和传递数据。

生命周期
除了在画布上手动拖拽节点,你也可以让 AI 助手或外部 MCP 客户端帮忙起草、修改、校验和试运行工作流。不过不管怎么改,保存到草稿后都必须由管理员手动点击「发布」,新版本才会正式启用。
| 状态 | 说明 |
|---|---|
| 草稿 | 新建或编辑后尚未发布的状态,不会参与任何实际任务 |
| 已发布 | 正式生效的版本,可以被索引器、下载计划或定时任务绑定并触发 |
| 有更新 | 已发布后又做了修改;在重新发布之前,系统仍旧沿用上一个已发布版本 |
编辑器结构
- 节点面板(左侧):按类型分组展示可用节点,直接拖进画布就能用
- 画布(中央):放置节点、拉线连接句柄、调整布局
- 小地图(右下):浏览整个流程的缩略图,方便在复杂流程里快速定位和缩放
- 工具栏(右上):提供适应视图、撤销、重做、保存、发布等常用操作

工作流节点
节点分为「开始」「结束」「通用」和「控制」四组。编辑不同类别的工作流时,左侧只会显示这个分类下可用的节点。
开始 / 结束
同一工作流里只能搭配使用同一类别的开始和结束节点。
| 节点名称 | 分类 | 方向 | 说明 |
|---|---|---|---|
| 搜索 API | 网络搜索 | 开始 | 接收关键词和分页参数,配置展示样式和全局搜索选项 |
| 搜索 API | 网络搜索 | 结束 | 将搜索结果格式化为标准 JSON 返回 |
| 认证 API | 网络搜索 | 开始 | 配置登录方式和 Cookie 参数 |
| 认证 API | 网络搜索 | 结束 | 输出认证结果,成功后自动持久化 Cookie |
| 详情 API | 网络搜索 | 开始 | 配置媒体类型和视频封装格式 |
| 详情 API | 网络搜索 | 结束 | 将详情数据格式化为标准 JSON 返回 |
| 首页 API | 网络搜索 | 开始 | 配置首页看板数据的来源和展示样式 |
| 首页 API | 网络搜索 | 结束 | 将首页看板数据格式化为分组 JSON 列表返回 |
| 开始 | 资源入库 | 开始 | 接收入库参数,判断对应 NFO 文件已存在时中止流程 |
| 电影 | 资源入库 | 结束 | 生成电影元数据 NFO 文件 |
| 电视剧 | 资源入库 | 结束 | 生成电视剧元数据 NFO 文件 |
| 剧集 | 资源入库 | 结束 | 生成剧集元数据 NFO 文件 |
| 小说 | 资源入库 | 结束 | 返回小说元数据候选,自动入库时保存为外置 OPF |
| 漫画 | 资源入库 | 结束 | 返回漫画元数据候选,自动入库时保存为 ComicInfo |
| 开始 | 定时调度 | 开始 | 接收启动参数,渲染后合并到上下文 |
| 结束 | 定时调度 | 结束 | 标记流程结束,可选立即取消其它节点 |
通用
| 节点名称 | 说明 |
|---|---|
| HTTP 请求 | 发送 HTTP 请求,支持选择客户端和超时时间,请求头和请求体支持 Jinja2 模板 |
| 保存文本 | 将 Jinja2 模板渲染结果写入文件,路径必须位于某个媒体库目录下,支持不同的写入模式 |
| 设置变量 | 将 Jinja2 模板渲染出的 JSON 键值对合并到当前上下文,供后续节点引用 |
| 脚本 | 运行 Python 或 JavaScript 代码,通过约定的 execute 函数与引擎交互 |
| 缓存数据 | 持久保存当前工作流的本地变量,支持有效期,在下次执行时载入 |
| AI | 调用已配置模型,输出文本或经过 JSON Schema 校验的对象,见 AI 节点 |
HTTP 请求设置
「HTTP 请求」节点默认使用 httpx 发请求,如果遇到对 TLS 指纹或反爬较严的站点,也可切换为 curl_cffi(会模拟 Chrome 浏览器指纹)。超时时间以秒为单位,默认 30,最小 1,按接口响应速度调整就行。
请求地址、请求头和请求体都支持模板;拿到响应后,可通过「格式化」字段转成 JSON 对象并合入当前上下文。两种客户端均按网络设置里的规则匹配代理;不过要注意,httpx 支持通过 X-Hishel-* 请求头配置 HTTP 缓存,curl_cffi 不走这套缓存逻辑,而且 User-Agent 等浏览器指纹相关请求头也会由客户端自行生成。
缓存数据
「缓存数据」节点用来把数据存成当前工作流私有的本地变量,下次运行同一工作流时自动载入,很适合用来记分页游标、接口临时令牌或最近处理到的位置。
| 字段 | 说明 |
|---|---|
| 键 | 缓存变量名,支持模板,最长 64 个字符 |
| 有效期(秒) | 0 表示永不过期;过期值在后续执行时清理,不再载入 |
| 值 | 支持 Jinja2 模板,渲染后的文本将持久保存 |
同一个工作流里写入相同的键会覆盖旧值,不同工作流之间的缓存互相独立。另外,写入缓存不会改变这一次运行中的上下文;如果当前流程后面马上要用新值,记得另外加一个「设置变量」节点。
控制
| 节点名称 | 说明 |
|---|---|
| 条件 | 对表达式求值,结果为真走「条件成立」分支,否则走「条件不成立」分支 |
| 循环 | 遍历可迭代对象的每个元素,每次迭代时将当前元素以指定变量名注入到上下文 |
| 合并 | 等待来自多个上游分支的输入达到指定数量后再继续,支持配置最大等待数量和超时时间 |
条件和循环节点会直接保留单个模板表达式的数据类型。比如 {{ results }} 可直接作为列表拿去循环,{{ count > 0 }} 可直接用来做布尔判断,{{ 3 }} 则会循环三次。注意如果只写 results 而不加双花括号,引擎会把它当作普通文本而不是变量;需要设默认值时可以写成 {{ results | default([]) }}。
执行上下文
每次触发流程时,引擎都会新建一个独立的上下文,节点通过模板字段从里面读取数据。上下文初始包含以下四部分内容:
| 来源 | 说明 |
|---|---|
| 系统变量 | 当前 Kaloscope 版本等内置变量,例如 ks_version |
| 全局变量 | 在「全局变量」里统一维护的键值对;加密变量会在注入上下文前自动解密 |
| 本地变量 | 作用域限定在当前工作流的持久变量,由「缓存数据」节点写入;未过期的值会在下次执行时载入 |
| 启动参数 | 触发流程时传入的参数,会直接合并到上下文;在定时调度「开始」节点里还可通过 params 引用原始参数 |
运行期间,上下文里还会自动出现几个以 $ 开头的特殊变量,用来传递数据或查看循环状态:
| 变量名 | 说明 |
|---|---|
$retval | 出口节点返回给调用方的值 |
$loop | 当前循环迭代的变量对象 |
$index | 当前循环迭代的下标 |
循环节点会把当前遍历到的元素,按节点里填的变量名注入上下文。比如变量名填为 item 时,循环体内就能用 {{ item }} 取到当前元素;遍历字典时会逐个返回 (key, value) 键值对,遍历整数时则按 range(n) 遍历。
模板字段
绝大多数节点字段都支持 Jinja2 模板语法,除了直接读取上下文和全局变量,还能搭配过滤器(filter)清洗和转换数据。
{{ title }}
{{ results[0].url }}
{{ content | xpath_first('//title/text()') }}Kaloscope 扩展的过滤器覆盖了 JSONPath、XPath、正则提取、时间格式化、字符串、URL 和路径处理等多种操作,大多数索引和刮削场景不用写脚本就能搞定。
自定义过滤器
在 Jinja2 里,过滤器用来把前一个表达式的结果传给处理函数,常见写法是 {{ value | filter_name(arg1, arg2) }}。除了 Jinja2 内置过滤器,Kaloscope 还补充了下面这些常用过滤器:
| 名称 | 用途 | 示例 |
|---|---|---|
| 字符串 | ||
trim | 去除首尾字符,递归处理列表/字典。参数:chars(可选,默认空白) | {{ title | trim }} |
ltrim | 去除左侧字符,递归处理列表/字典。参数:chars(可选,默认空白) | {{ title | ltrim('[') }} |
rtrim | 去除右侧字符,递归处理列表/字典。参数:chars(可选,默认空白) | {{ title | rtrim(']') }} |
json_escape | 按 JSON 规则转义字符串 | {{ keyword | json_escape }} |
prefix | 添加前缀。参数:prefix(前缀字符串);strict(可选,默认 true 表示值或前缀为空时返回空字符串,传 false 始终拼接) | {{ id | prefix('/detail/') }} |
suffix | 添加后缀。参数:suffix(后缀字符串);strict(可选,默认 true 表示值或后缀为空时返回空字符串,传 false 始终拼接) | {{ filename | suffix('.nfo') }} |
size | 将字节数格式化为可读大小 | {{ total_size | size }} |
| JSONPath | ||
jsonpath_first | JSONPath 查询,返回第一个匹配值。参数:expr(JSONPath 表达式) | {{ data | jsonpath_first('$.items[*].title') }} |
jsonpath_all | JSONPath 查询,返回全部匹配列表。参数:expr(JSONPath 表达式) | {{ data | jsonpath_all('$.items[*].title') }} |
jsonpath | JSONPath 查询。参数:expr(JSONPath 表达式);limit(可选,默认 'auto',可传 'first'/'all'/整数) | {{ data | jsonpath('$.items[*].title', 'all') }} |
| XPath | ||
xpath_first | XPath 查询,返回第一个匹配值。参数:expr(XPath 表达式) | {{ html | xpath_first('//title/text()') }} |
xpath_all | XPath 查询,返回全部匹配列表。参数:expr(XPath 表达式) | {{ html | xpath_all('//a/@href') }} |
xpath | XPath 查询。参数:expr(XPath 表达式);limit(可选,默认 'auto',可传 'first'/'all'/整数) | {{ html | xpath('//img/@src', 'first') }} |
| 正则 | ||
regex_first | 正则匹配,返回第一个捕获组。参数:expr(正则表达式) | {{ text | regex_first('id=(\d+)') }} |
regex_all | 正则匹配,返回所有捕获组列表。参数:expr(正则表达式) | {{ text | regex_all('href="([^"]+)"') }} |
regex | 正则匹配。参数:expr(正则表达式);limit(可选,默认 'auto',可传 'first'/'all'/整数) | {{ text | regex('id=(\d+)') }} |
| 时间与日期 | ||
strftime | 格式化时间。参数:format(可选,默认 '%Y-%m-%d %H:%M:%S');tz(可选,时区偏移小时数) | {{ ts | strftime('%Y-%m-%d', 8) }} |
year | 提取年份。参数:tz(可选,时区偏移小时数) | {{ publish_time | year(8) }} |
duration | 转为可读时长(MM:SS/HH:MM:SS)。参数:unit(可选,默认 'milliseconds',可传 'seconds'/'minutes') | {{ seconds | duration('seconds') }} |
| 编码、URL 与路径 | ||
b64decode | Base64 解码,返回文本字符串 | {{ token | b64decode }} |
b64encode | Base64 编码,返回编码后的字符串 | {{ password | b64encode }} |
quote | URL 百分号编码 | {{ keyword | quote }} |
query_param | 追加查询参数;如果同名参数已存在则保持原 URL 不变 | {{ url | query_param('lang=zh-CN') }} |
parent_path | 取父级目录。参数:levels(层数,默认 1);resolve(先转绝对路径,默认 false) | {{ path | parent_path(2, true) }} |
| 中文转换 | ||
s2t | 简体转繁体 | {{ title | s2t }} |
t2s | 繁体转简体 | {{ title | t2s }} |
特殊参数说明
jsonpath、xpath 和 regex 这三个过滤器的 limit 参数默认是 'auto':无匹配时返回 None,单个匹配时直接返回该值,多个匹配时返回列表。
自定义测试
在 Jinja2 里,测试(test)用来判断值是否满足某个条件,常见写法是 {% if value is test_name %}。除了 Jinja2 内置测试,Kaloscope 还额外提供了两个判断本地文件系统的测试:
| 名称 | 用途 | 示例 |
|---|---|---|
file | 判断路径是否是已存在的文件 | {% if nfo_path is file %}...{% endif %} |
dir | 判断路径是否是已存在的目录 | {% if lib_dir is dir %}...{% endif %} |