Skip to Content
IoT 应用开发IronFlock SDK

IronFlock SDK

IronFlock SDK 让您的边缘应用程序能够与 IronFlock 平台交互。它提供三种语言版本——Python、JavaScript 和 Go——功能相同,下文将并列介绍。在已注册的设备上运行时,它会自动处理身份验证,并提供用于发布数据、查询历史记录、跨设备调用远程过程以及更新设备元数据的函数。

SDK包要求
Pythonironflock(PyPI)Python 3.8+
JavaScriptironflock(npm)Node.js 18+ 或现代浏览器
Gogithub.com/RecordEvolution/ironflock-goGo 1.27.2+

安装

pip install ironflock

或将 ironflock 添加到应用的 requirements.txt 中。

快速开始

import asyncio from datetime import datetime, timezone from ironflock import IronFlock async def main(): while True: await ironflock.publish_to_table("sensordata", { "tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5, "humidity": 60 }) await asyncio.sleep(5) ironflock = IronFlock(mainFunc=main) ironflock.run()

在 IronFlock 应用容器内使用时,SDK 会自动从环境中读取连接凭据——无需手动配置。您写入表的每一行都带有 tsp,即该行的时间戳(参见 publishToTable)。

构造函数选项

ironflock = IronFlock( mainFunc=main, # async function to run after connecting serial_number="abc123", # override device serial (optional) reconnect_window=60, # seconds a table operation rides out a platform restart (optional) )
参数描述
mainFunc连接建立后执行的异步函数
serial_number覆盖随发布和表写入一起发送的设备序列号。默认使用环境变量 DEVICE_SERIAL_NUMBER。当设备代理注入了按应用的凭据时,它不会改变连接用于认证的身份
reconnect_window表操作在引发异常之前,等待平台在重启后恢复的秒数。默认为 60;0 表示关闭(参见连接可靠性)

发布数据

publishToTable / publish_to_table

向舰队表发布数据记录。表名必须与应用的 data-template.yml 中定义的表匹配。SDK 会自动将数据路由到正确的项目数据库。

from datetime import datetime, timezone await ironflock.publish_to_table("sensordata", { "tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5, "humidity": 60, "device_id": "sensor-001" })

数据后端从第一个参数读取该行:在 Python 中是一个 dict,在 JavaScript 中是数组的第一个元素,在 Go 中是第一个位置参数(一个 map 或结构体)。额外的关键字参数(在 Go 中为参数中的 ironflock.Kwargs 值)只有在数据模板把某列映射到它们(kwargs.<name>)时才会被读取。

每一行都需要 tsp。 每张表都有一个必填的 tsp 列,即该行的时间戳(例如 ISO 8601 字符串,或在 Go 中为 time.Time,它会以 UTC 的 RFC 3339 发送);除非数据模板将其映射到别处,否则数据后端会从行中读取它。缺少 tsp 的已发布行会被丢弃,唯一的痕迹只是应用 error-logs 表中的一条记录——而已确认的发布看起来是成功的。因存储空间不足而被拒绝的行则会被丢弃,连这条记录都没有。当您需要知道某行是否已被存储时,请使用 appendToTable / append_to_table。

实体表中的部分行。 在具有实体键(maintainLatestFlagFor)的表中,一行只需包含实体键、tsp 和发生变化的列。行中省略的列会保留该实体之前的值;以 None / null / nil 发送的列会被清空。某个字段是否算作省略,取决于您如何构建该行:

  • Python: 值为 None 的键会作为 null 发送并清空该列。要保留旧值,请从 dict 中删除该键。
  • JavaScript: 值为 undefined 的属性会从负载中被丢弃,因此保留旧值。要清空该列,请将其设为 null。
  • Go: 带有 omitempty 或 omitzero 标签的结构体字段在为空时会被省略,因此保留旧值。否则,nil 指针字段与 ironflock.Row 中的 nil 值一样,会作为 null 发送并清空该列。

这一规则适用于全部四种写入方法。在没有实体键的表中,行中省略的列会存储为 NULL。

appendToTable / append_to_table

使用远程过程调用而非发布/订阅方式向舰队表追加数据。当需要确认数据已持久化时使用此方法:被拒绝的行会导致错误。

result = await ironflock.append_to_table("sensordata", { "tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5, "humidity": 60 })

与 publishToTable 一样,该行需要 tsp。数据后端会以 wamp.error.runtime_error 拒绝缺少 tsp 的行,且不给出原因;此时 SDK 的错误会附加 “the data backend refused the row without a reason — a missing tsp is the usual cause; see the error-logs table” 这句提示。带类型的拒绝(例如一体机的磁盘已满)列在数据后端拒绝中。

publishRowsToTable / publish_rows_to_table

在单条消息中发布多行(批量插入)到舰队表。平台会在一次操作中原子地插入整个批次(全部成功或全部失败)。对于高频数据,如果每行都进行一次往返通信开销过大,请使用此方法。与 publishToTable 一样,这是即发即忘(fire-and-forget)的——确认信息仅表示消息已送达路由器,而非确认数据库已完成插入。

await ironflock.publish_rows_to_table("sensordata", [ {"tsp": "2024-01-15T10:30:00.000Z", "temperature": 22.5}, {"tsp": "2024-01-15T10:30:01.000Z", "temperature": 22.7}, ])

第二个参数是一个非空的待插入行对象列表,每一行都带有自己的 tsp——在 Go 中为行的切片([]ironflock.Row、[]map[string]any 或结构体切片)。被数据后端拒绝的批次(例如因为某一行缺少 tsp)会被丢弃,且与 publishToTable 一样,不会向发布方发出任何通知。

appendRowsToTable / append_rows_to_table

通过单次远程过程调用追加多行(批量插入)到舰队表。平台会原子地插入整个批次(全部成功或全部失败):如果任何一行无效,整个批次都将被拒绝,且不会持久化任何数据。当您需要获知插入结果时,应优先使用此方法而非 publishRowsToTable / publish_rows_to_table。每一行都需要自己的 tsp;拒绝情况与 appendToTable 相同,包括针对无原因被拒绝的批次给出的提示。

result = await ironflock.append_rows_to_table("sensordata", [ {"tsp": "2024-01-15T10:30:00.000Z", "temperature": 22.5}, {"tsp": "2024-01-15T10:30:01.000Z", "temperature": 22.7}, ]) # result -> {"success": True, "count": 2}

reportError / report_error

将应用程序错误报告到舰队的 error-logs 表中。这是对 publishToTable / appendToTable 的便捷封装:它会为该行打上 source: "app" 标记、一个严重级别 level 以及一个时间戳,然后像写入任何普通表行一样将其写入。该错误会落入与 fleetdb 系统错误相同的 error-logs 表中(系统错误标记为 source: "system"),因此可以使用 getHistory 查询,使用 subscribeToTable / subscribe_to_table 流式订阅,在看板模板中使用,并通过 transformed.error-logs 实时传递——而不会触发平台的系统错误提示框。

# Fire-and-forget (default): publishes to the error-logs table await ironflock.report_error("Sensor read timed out", level="warn") # Pass an exception to capture its traceback (falls back to the message) try: risky_operation() except Exception as err: await ironflock.report_error(err) # Use the append RPC when you want to await the insert outcome, and show the # operator a friendlier line than the raw error await ironflock.report_error( "Calibration failed: offset 0.42 out of range", append=True, user_message="Please recalibrate the sensor", )

该行为 { tsp, msg, user_message, source: "app", level }:msg 保存技术性文本(traceback 或 stack),user_message 保存看板错误提示框中显示的那一行——即您传入的用户消息;如果没有传入,则为消息本身。

参数:

参数类型描述
errorstr / string 或异常 / Error错误消息,或一个异常对象,其 traceback/stack(或消息)将被记录
levelstr / string,可选严重级别:"error"、"warn"、"info" 或 "debug"。默认为 "error"
appendbool / boolean,可选当为 true 时,使用 append 远程过程调用(返回插入结果)。默认为 false(即发即弃式发布)
tspstr / string,可选ISO-8601 时间戳覆盖值。默认为当前时间
user_message / userMessagestr / string,可选面向操作员的文本,存储为 user_message。默认为所记录的消息

在 Python 中,这些选项以关键字参数形式传入(report_error(error, level=..., append=..., tsp=..., user_message=...));在 JavaScript 中,它们通过一个选项对象传入(reportError(error, { level, append, tsp, userMessage }));在 Go 中通过一个结构体传入(ReportError(ctx, errOrMsg, ironflock.ReportErrorOptions{Level, Append, Tsp, UserMessage})),其中 errOrMsg 是一个 error 或字符串,Level 为 ironflock.LevelError、LevelWarn、LevelInfo、LevelDebug 之一,并且除非设置了 Append,否则返回的 *Result 为 nil。

publish

向您自己的 WAMP 主题发布消息。用于不映射到数据库表的自定义消息传递或事件。路由器为平台保留了一些名称——参见应用可以使用的 URI。

await ironflock.publish("com.myapp.alerts", { "level": "warning", "message": "Temperature threshold exceeded" })

查询历史数据

getHistory

从舰队表或 transform 中检索历史数据:返回匹配的最新行,按 tsp 升序排列。支持过滤、时间范围和分页。

# Simple query data = await ironflock.getHistory("sensordata", {"limit": 100}) # Query with time range and filters data = await ironflock.getHistory("sensordata", { "limit": 500, "offset": 0, "timeRange": ["2026-01-01T00:00:00Z", "2026-03-01T00:00:00Z"], "filterAnd": [ {"column": "temperature", "operator": ">", "value": 20}, {"column": "humidity", "operator": "<=", "value": 80} ] }) # Current value(s) only: the "latest" marker returns the newest row per entity current = await ironflock.getHistory("sensordata", { "limit": 100, "filterAnd": [{"latest": True}] }) # One field of a large json column: the column comes back pruned to its paths readings = await ironflock.getHistory("sensordata", { "limit": 100, "columns": ["json_data"], "columnPaths": ["json_data.sensor.temp"], })

查询参数:

字段类型描述
limitint / number返回的最大行数(1–10,000,必填;transform 为 1–3,000)
offsetint / number分页偏移量
timeRangelist / array[start, end] — ISO 日期时间字符串或 epoch 毫秒数字,二者不可混用;None / null 表示开放端。旧式的 {"start": ..., "end": ...} 对象会被转换为该二元组
filterAndlist / array以 AND 组合的过滤条目:条件、分组,以及/或 latest 标记(见下文)
columnslist / array要返回的列(可选)。tsp、device_key 和 authid 始终包含在内;省略则返回所有列
columnPathslist / array裁剪 json 列(可选):采用与过滤条件相同写法的 JSON 路径("json_data.a.b"、"json_data['key']")。在 columns 中列出的 json 列返回时只包含其各路径的并集;在此处没有路径的列则完整返回。transform 以及早于 fleetdb v1.0.52 的数据后端会忽略该参数

在 Go 中,这些是 ironflock.TableQueryParams 的字段 Limit、Offset、TimeRange、FilterAnd、Columns 和 ColumnPaths。TimeRange 是一个 *ironflock.TimeRange{Start, End}:每个边界可以是 time.Time、ISO 8601 字符串、epoch 毫秒数字,或表示开放端的 nil(ironflock.Between、Since 和 Until 可由 time.Time 值构建它)。FilterAnd 是一个 []ironflock.Filter。没有旧式的对象形式。

过滤运算符: =, !=, <>, >, <, >=, <=, LIKE, ILIKE, NOT LIKE, NOT ILIKE, IN, NOT IN, IS NULL, IS NOT NULL

每个过滤条件是包含 column、operator 和 value 键的对象——在 Go 中,用 ironflock.Where(column, operator, value)、ironflock.IsNull(column) 和 ironflock.IsNotNull(column) 构建。运算符不区分大小写,匹配时忽略前后空格,并以上面列出的形式发送——这是数据后端唯一接受的形式——因此 "like" 会以 "LIKE" 发出,在分组内部也是如此。不在列表中的运算符会原样发送,并附带一条警告。IS NULL 和 IS NOT NULL 不接受 value,并且是查询 NULL 的唯一方式:其他所有运算符都会排除该列为 NULL 的行,与 SQL 完全一致。IN 和 NOT IN 接受一个列表(在 Go 中为切片)。LIKE 区分大小写;要不区分大小写地匹配,请使用 ILIKE。

使用 OR 组合。 filterAnd 的各条目之间以 AND 组合。分组条目用一个运算符("AND" 或 "OR")组合其自身的条目——条件、latest 标记或更深一层的分组:

# Rows that are not soft-deleted: deleted IS NULL OR deleted = false "filterAnd": [ { "combinator": "OR", "filters": [ {"column": "deleted", "operator": "IS NULL"}, {"column": "deleted", "operator": "=", "value": False}, ], } ]

早于分组功能的数据后端(v1.2.0 之前的 fleetdb)会拒绝这种结构,而不是只应用其中一部分,因此它会明确地报错,而不是悄悄返回本应被过滤掉的行。

读取当前值。 filterAnd 中的 {"latest": true} 条目(在 Go 中为 ironflock.Latest())并不是一个过滤条件,而是一个模式开关:数据后端只返回每个实体的最新行,该结果由表通过 maintainLatestFlagFor 声明的实体键在 SQL 中推导得出。没有实体键的表则返回其唯一的最新一行。

其他条件与该标记的组合方式符合预期:作用于实体键列的条件用于缩小返回哪些实体的范围,而所有其他条件和 timeRange 都会应用到得出的最新行上。因此,将 {"latest": true} 与 deleted 过滤条件结合使用,会隐藏已删除的实体,而不会让它们此前的行重新浮现。

IronFlock 的早期版本会存储一个物理的 latest_flag 列。该列已不再存在——旧式的 latest_flag = true 过滤条件仍被接受并按该标记处理,但新代码应使用 {"latest": true}。latest 标记在 getSeriesHistory 中不可用。

时间范围。 数据后端使用 JavaScript 的 Date 读取字符串形式的边界,因此请写成 ISO 8601 日期或日期时间(2026-07-01、2026-07-01T12:30:15.250Z),并用 T 分隔日期和时间。对于 0–99 年,SDK 会拒绝日期与时间之间用空格分隔的写法,因为 Date 会误读它:"0001-01-01 00:00:00"——即 Python 的 str(datetime.min) 给出的值——会被读作 2001 年。不带时区的时间按 UTC 读取。数值边界是 epoch 毫秒,并且必须是有限值:NaN 和 ±Infinity 到达数据后端时会成为开放边界,从而悄悄地取消时间过滤,因此 SDK 会在发送前拒绝它们。在 Go 中,time.Time 边界会以 UTC 的 RFC 3339 发送,而字符串边界必须是 Date 能正确读取的 ISO 8601 形式之一——一个日期,或日期加上一天中的时间,可带可选的 Z 或 UTC 偏移(+02:00 或 +0200);其他字符串,例如基本格式 20260701T123000Z、只有小时的偏移,或当月不存在的日期(2026-02-30),都会在发送任何内容之前以 ironflock.ErrInvalidArgument 失败。

读取 transform。 transform(数据模板中的 SQL 视图,参见转换表)的读取方式与表不同。数据后端只应用 limit(最大 3000,更大的值会被以 URI_INVALID_LIMIT 拒绝)、offset 和 filterAnd。它会忽略 timeRange、columns、columnPaths 和 latest 标记,并丢弃针对 transform 未声明列的过滤条件(transform 没有隐式的 tsp、device_key 或 authid),从而扩大结果范围。其行按视图自身的顺序倒序返回:只有当视图按最新在前排序时,您才会得到最新的行,并且按从旧到新排列。请在 transform 的 SQL 中限定其时间范围。

大结果。 超出数据后端单条消息容量(8 MiB 的 JSON)的结果会分块到达,SDK 会在内存中将其重新组装:您得到的就是一次读取的行,顺序也相同。分块对不上时——缺失或重复了某一块,或者行数与数据后端的汇总不一致——读取会失败,而不是只返回部分行。超过 8 MiB 的单行,或超过数据后端 1 GiB 防护上限的结果,会被以 URI_RESULT_TOO_LARGE 拒绝:请降低 limit、缩小 timeRange、选择更少的 columns,或使用 columnPaths 进行裁剪。

getSeriesHistory / get_series_history

从舰队表中检索降采样的时间序列数据:按时间桶聚合各列(例如每小时平均值)。非常适合跨越长时间范围的图表。适用于表(不适用于 transform),要求数据后端为 fleetdb v1.0.58 及以上版本。

from datetime import datetime, timedelta, timezone from ironflock import SeriesMetric now = datetime.now(timezone.utc) avg = SeriesMetric(ref="temperature", method="AVG") # AVG, SUM, COUNT, MIN, MAX, FIRST, LAST series = await ironflock.get_series_history("sensordata", { "metrics": [avg, {"ref": "humidity", "method": "MAX"}], "bucketMs": 3_600_000, # one-hour buckets; omit to divide the range into `limit` buckets "limit": 24, # the bucket budget, 1–10000 "timeRange": [(now - timedelta(hours=24)).isoformat(), now.isoformat()], # required, with a start "groupBy": ["device_key"], }) for row in series: print(row["tsp"], row["device_key"], row[avg.column]) # avg.column is "AVG:temperature"

查询参数:

字段类型描述
metricslist / array每个时间桶计算的聚合,至少一个:{"ref": ..., "method": ...}。ref 是一个列,或指向 json 列内部的 JSON 路径("json_data.temp");method 为 "AVG"、"SUM"、"COUNT"、"MIN"、"MAX"、"FIRST" 或 "LAST"。每个指标以 "<METHOD>:<ref>" 为名返回,该名称不得超过 63 字节
bucketMsint / number时间桶宽度,以整数毫秒表示,至少为 1000(可选)。省略时,时间范围会被划分为 limit 个桶
limitint / number桶的预算(1–10,000)——而非行数上限
timeRangelist / array[start, end] — ISO 日期时间字符串或 epoch 毫秒数字。必填,且必须有起点;终点为 None / null 表示当前时刻。终点必须晚于起点
groupBylist / array将每个桶按分组拆分为每组一行的列(可选)
filterAndlist / array与 getHistory 相同,以 AND 组合的条件和分组(可选)。此处会拒绝 latest 标记,在分组内部也是如此;要读取当前值请使用 getHistory

行。 每一行是一个桶——使用 groupBy 时,是某一个分组的一个桶。tsp 是该桶的起始时间(epoch 毫秒),每个 groupBy 列以其自身名称返回,每个指标则以 "<METHOD>:<ref>" 为名返回(Python 中为 SeriesMetric.column,JavaScript 中为 seriesColumn(metric),Go 中为 SeriesMetric.Column()),例如 row["AVG:temperature"]。各行按 tsp 升序排列;没有数据行的桶不会出现(不做空缺填充)。AVG 和 SUM 接受数值列,MIN 和 MAX 还接受字符串和时间戳;FIRST 和 LAST 给出桶内最早和最晚一行的值;COUNT 统计非空值的数量(对 tsp 而言即行数)。与 getHistory 一样,大结果会由分块重新组装。

桶与开放的终点。 limit 计数的是桶,而不是行。不指定 bucketMs 时,时间范围会被划分为 limit 个桶。指定后,如果该范围包含的桶多于 limit 个,数据后端会把桶加宽为 bucketMs 的倍数——而开放的终点会一直延伸到数据后端自己的当前时刻,比您的当前时刻稍晚一些。终点开放的最近 24 小时包含 25 个一小时桶,因此 limit: 24 会得到两小时的桶。请像示例那样给出终点,或者多留一个桶的余量:使用 bucketMs 且终点开放时,可能会返回 limit + 1 个桶。

SDK 会在发送前检查数据后端会拒绝的内容:至少一个指标、非空的 ref、已知的方法、不超过 63 字节的列名、一个起点、晚于起点的终点,以及值为不小于 1000 的整数的 bucketMs(在 Go 中,Bucket 至少为 1 秒,且为整数毫秒)。数据后端会以 URI_INVALID_TIME_RANGE、URI_INVALID_METRIC、URI_INVALID_GROUP_BY 或 URI_SERIES_TOO_MANY_GROUPS(超过 50,000 行)拒绝它无法回答的查询——参见数据后端拒绝。

已弃用的形式。 1.9.0 及之前的 SDK 版本把 metrics 作为列名发送,并附带一个顶层的 method({"metrics": ["temperature"], "method": "AVG", ...}),自 fleetdb v1.0.58 起数据后端会拒绝这种形式。该形式仍然可用:SDK 会为每一列发送一个 {ref, method} 指标,发出警告(Python 中为 DeprecationWarning,JavaScript 中为每个进程一次的控制台警告),并把结果列重新命名回列名,因此旧代码仍可读取 row["temperature"]——除非该行已经有同名的列,例如对 tsp 做 COUNT 时的 tsp。请迁移到 {ref, method} 指标,并读取 row["AVG:temperature"]。Go SDK 只有当前的形式:每个指标各有一个方法(SeriesMetric)。

订阅数据

subscribeToTable / subscribe_to_table

订阅舰队表的实时更新。每当数据后端存储一个新行时,处理函数都会被调用,其第一个参数就是所存储的行——值已按数据模板的列类型转换,机密列已被遮蔽。通过批量插入路径(publishRowsToTable / appendRowsToTable)写入的行会逐行依次传递给您的处理函数,因此无论数据以何种方式写入,处理函数的代码都保持不变。

def on_sensor_data(*args, **kwargs): row = args[0] print("New reading:", row["tsp"], row["temperature"]) await ironflock.subscribe_to_table("sensordata", on_sensor_data)

transform 没有属于自己的行:在其调度的每个周期,数据后端都会把整个视图——最多 3000 行,按视图顺序倒序——作为一个事件发布,事件的第一个参数就是行列表。Python 中的 event_rows(args[0])、JavaScript 中的 eventRows(args) 和 Go 中的 ev.Rows() 可以读取这两种形式:表的行得到 [row],transform 的列表得到该列表(空视图为 []),其他任何内容得到 None / undefined / nil。在 Go 中,对于 transform 的事件,ev.Row() 为 nil。

from ironflock import event_rows def on_hourly(*args, **kwargs): for row in event_rows(args[0]) or []: print(row) await ironflock.subscribe_to_table("hourly_averages", on_hourly)

subscribe

订阅您自己的 WAMP 主题以实现自定义实时消息传递。对于表行,请使用 subscribeToTable / subscribe_to_table:路由器会拒绝对原始表写入主题的订阅(参见应用可以使用的 URI)。

def on_alert(*args, **kwargs): print("Alert received:", args, kwargs) await ironflock.subscribe("com.myapp.alerts", on_alert)

机密列

在应用的数据模板中声明为 secret: true 的列会被加密存储。没有任何普通读取路径会返回明文,即使是写入它的那个应用也不例外:

读取路径机密列看起来是什么样
getHistory、subscribeToTable / subscribe_to_table,以及其他一切经由消息路由器的读取占位符 __secret__,以 SECRET_PLACEHOLDER 导出(在 Go 中为 ironflock.SecretPlaceholder)
SQL 访问(FleetDB Access 的 Postgres 登录)存储的密文,ifsec:1:…
revealSecrets / reveal_secrets解密后的明文

NULL 在每条路径上都原样透传为 NULL,因此“已设置但被隐藏”与“从未写入”依然可以区分。

在实体表上,读取-修改-写回是安全的:重新发布仍带有 __secret__ 占位符的行——或干脆省略机密列——都会保留已存储的机密值而不是将其覆盖,而显式的 null 会清除它。确切规则参见数据后端指南中的机密列。

from datetime import datetime, timezone # Written like any other column — the data backend encrypts it on insert await ironflock.publish_to_table("credentials", { "tsp": datetime.now(timezone.utc).isoformat(), "device_id": "d-1", "api_token": "hunter2", }) # A normal read redacts it rows = await ironflock.getHistory("credentials", {"limit": 1}) # [{"tsp": "...", "device_id": "d-1", "api_token": "__secret__"}]

下面这两个函数只能从应用自己的容器中调用。消息路由器对所有浏览器角色以及消费方应用一律拒绝它们,这也是 ConsumedApp 句柄没有对应方法的原因:另一个应用的机密列到达时已被遮蔽,并且始终保持这个状态。

revealSecrets / reveal_secrets

读取你自己某张表中的行,并解密其中的机密列。

rows = await ironflock.reveal_secrets("credentials", { "limit": 1, "filterAnd": [{"column": "device_id", "operator": "=", "value": "d-1"}], }) # [{"tsp": "...", "device_id": "d-1", "api_token": "hunter2"}]

查询参数与 getHistory 相同,只有一处不同:limit 不得超过 100。这个上限的存在是为了阻止单次调用解密整张表,并且数据后端会拒绝更大的值,而不是悄悄地把它截到上限。

对于设备上的每个应用凭据,数据后端每分钟最多响应 30 次明文读取;超出后调用会以 URI_RATE_LIMITED 失败。请只读取一次机密值并保存下来,而不是每次使用时都读取。没有机密列的表会被以 URI_NOT_A_SECRET_COLUMN 拒绝,针对机密列的过滤条件会被以 URI_SECRET_COLUMN 拒绝。

verifySecret / verify_secret

在不泄露机密值的前提下,将一个候选值与机密列进行比对——它是明文读取函数在密码校验场景下的对应物。比较在数据后端内部以恒定时间完成,返回的只有比对结果。

ok = await ironflock.verify_secret( "credentials", "api_token", submitted_token, {"limit": 1, "filterAnd": [{"column": "device_id", "operator": "=", "value": "d-1"}]}, ) if ok: ...

参数:

参数类型描述
tablenamestr / string要比对的表
columnstr / string要比较的机密列
candidatestr / string待检验的明文。在服务端完成比较,绝不会被写入该表
query_params / queryParamsSecretQueryParams参与比对的行。默认为最新的一行;limit 不得超过 100

在 Go 中,这些参数是上下文之后的位置字符串参数,查询是一个 *ironflock.TableQueryParams(nil 表示最新的一行)。

JavaScript 返回 { match, checked },Go 返回一个带有 Match 和 Checked 的 *ironflock.SecretVerifyResult;Python 返回一个普通的 bool。在能拿到 checked 的地方,它值得一用:它把候选值错误(checked > 0)与选择条件根本没有匹配到任何行(checked === 0)区分开来——对用户而言,这是两种非常不同的失败。

由于每一行都用它自己的随机值加密,两行持有相同的机密值时,存储的是不同的密文。这正是对机密列使用 = 过滤无法工作(数据后端会以 URI_SECRET_COLUMN 拒绝针对机密列的任何过滤)、而需要这个函数的原因。

对于设备上的每个应用凭据,数据后端每分钟最多响应 120 次校验;超出后调用会以 URI_RATE_LIMITED 失败。如果每次请求都要校验(例如 API 密钥),请将成功的结果短暂缓存。不是机密列的列会被以 URI_NOT_A_SECRET_COLUMN 拒绝。

跨应用数据访问

在同一个项目中,从你自己的应用内部读取另一个应用的舰队数据。你的应用必须在其自身 data-template.yml 的 consumes: 部分声明提供方应用(或持有下文所述的通配符授权),并且项目用户必须授予访问权限。访问是只读的:你可以查询提供方共享的表和 transform 的历史数据,并实时订阅其行,但不能写入它们。对被消费应用的连接会按应用缓存,并在你的实例停止时自动关闭。

该连接需要设备代理注入的按应用凭据(APP_AUTH_ID、APP_AUTH_SECRET)。旧式的设备凭据——即没有获得按应用凭据的应用(例如运行在较旧设备代理下的应用)所回退使用的序列号——会在另一个应用的 realm 上被平台拒绝(NOT_AUTHORIZED,并附带说明这一点的消息);除非提供方应用运行在同一台设备上,此时它会以该应用自身的完整权限(而非只读)加入其 realm。

如果你的应用持有通配符授权(consumes: [{ app: "*" }]),你可以使用 listConsumableApps / list_consumable_apps 和 connectToAllApps / connect_to_all_apps(见下文)动态地发现并打开提供方。

connectToApp / connect_to_app

打开到另一个应用数据后端的只读连接,并返回一个句柄。该句柄提供 getHistory / get_history、subscribeToTable / subscribe_to_table 和 getSeriesHistory / get_series_history(仅限表)——与你在自己的表上使用的查询和订阅完全相同,参数和结果也相同——此外还提供 close 以及共享的 tables / transforms 目录。

# Open a read-only handle on another app's data backend weather = await ironflock.connect_to_app("weather-app") # Inspect what the provider shares print([t["tablename"] for t in weather.tables]) # Query history and subscribe, just like your own tables rows = await weather.get_history("forecasts", {"limit": 100}) def on_forecast(*args, **kwargs): print("New forecast:", args) await weather.subscribe_to_table("forecasts", on_forecast)

参数:

参数类型描述
app_name / appNamestr / string提供方应用的名称,即你在 consumes: 部分中声明的名称
stagestr / string,可选提供方的阶段(stage):"dev" 或 "prod"。默认为你自己应用的阶段
on_error / onErrorcallable,可选当已打开的连接被最终拒绝时(例如授权随后被撤销——拒绝何时算作最终,见下文),会以一个 CrossAppAccessError 调用它

如果访问被拒绝或使用不当,将引发(Python)/抛出(JavaScript)/返回(Go,一个用 errors.As 检查的 *ironflock.CrossAppAccessError)一个带有 code 字段(在 Go 中为 Code,并有 ironflock.CodeNoGrant 之类的常量)的 CrossAppAccessError:NO_GRANT、PROVIDER_NOT_INSTALLED、UNKNOWN_APP、PRIVATE_TABLE、SECRET_COLUMN(查询对提供方标记为机密的列进行了过滤——在分组内部也算——或选择了该列)或 NOT_AUTHORIZED(授权已被撤销或当时无法验证——或者你的应用出示的是旧式设备凭据,此时消息中会说明这一点)。

在 connectToApp / connect_to_app 等待期间发生的拒绝会使调用立即失败。句柄一旦打开,只有当拒绝持续存在时才算作最终——自上次连接成功以来累计 3 次或更多拒绝(路由器因认证原因关闭会话算一次,每次被拒绝的重连也各算一次),且第一次拒绝发生在至少 60 秒之前——因为在平台短暂无法验证访问权限时,它也会以同样的方式拒绝。在此之前,句柄会持续重连,并在某次重连成功时恢复。一旦拒绝成为最终结果,就会调用 on_error / onError / OnError,并将该句柄逐出缓存:下一次 connectToApp / connect_to_app / ConnectToApp 会打开一个全新的连接。此后,该句柄的读取和订阅都会立即以 on_error / onError 收到的那个错误失败——在 Go 中,则以一个包装了 wamp.ErrStopped 的错误失败。

在 Python 中,stage 和 on_error 是关键字参数;在 JavaScript 中,它们通过一个选项对象传入(connectToApp(appName, { stage, onError }));在 Go 中通过一个结构体传入(ConnectToApp(ctx, appName, ironflock.ConnectToAppOptions{Stage, OnError})),其中 OnError 是一个 func(*ironflock.CrossAppAccessError)。在 Go 中,ctx 只限定调用方的等待:针对同一应用的并发调用共享同一次尝试,Close(ctx) 用于关闭句柄。

listConsumableApps / list_consumable_apps

列出项目中每一个非私有的提供方——这是为持有通配符消费授权(在你的 data-template.yml 中声明 consumes: [{ app: "*" }],并由项目用户授予)的应用提供的发现原语。它只执行一次调用,且不打开任何连接:将返回的目录渲染到一个选择器中,然后对你想要的提供方调用 connectToApp / connect_to_app——或使用 connectToAllApps / connect_to_all_apps 一次性打开所有提供方。

注意: 在你应用的 data-template.yml 中声明该授权,并为 * 加上引号——不带引号的 * 是一个 YAML 别名,无法解析:

consumes: - app: "*"
providers = await ironflock.list_consumable_apps() for p in providers: print(p["app"], list(p["stages"].keys())) # e.g. "weather-app" ["dev", "prod"]

每个条目描述一个提供方:

字段类型描述
appstr / string提供方应用的名称
provider_app_keyint / number提供方的应用密钥
stagesdict / object按阶段(stage)划分的目录 { dev?, prod? };只有当提供方针对该阶段有数据后端时,该阶段才会出现。每个目录包含它所共享的非私有 tables 和 transforms

在 Go 中,每个条目是一个 ironflock.ConsumedAppInfo,带有 App、ProviderAppKey 和 Stages.Dev / Stages.Prod(一个带有 Tables 和 Transforms 的 *StageCatalog,缺失的阶段为 nil);Catalog(stage) 按名称返回其中一个。

如果你的应用没有持有通配符授权,将引发(Python)/抛出(JavaScript)/返回(Go)一个带有 code: NO_GRANT 的 CrossAppAccessError。

connectToAllApps / connect_to_all_apps

在一次调用中打开到项目中每一个非私有提供方的只读句柄(仅限通配符消费方)。它通过 listConsumableApps / list_consumable_apps 枚举提供方并逐个打开,跳过任何针对所请求阶段没有数据后端的提供方。每个句柄都以与 connectToApp / connect_to_app 相同的键进行缓存,因此之后调用 connectToApp(name) 会返回那个已经预热好的句柄。返回的这些句柄会在你的实例停止时一起关闭。

apps = await ironflock.connect_to_all_apps( on_error=lambda err: print("Provider skipped:", err) ) for app in apps: rows = await app.get_history(app.tables[0]["tablename"], {"limit": 10}) print(app.app, rows)

参数:

参数类型描述
stagestr / string,可选提供方的阶段(stage):"dev" 或 "prod"。默认为你自己应用的阶段
continue_on_error / continueOnErrorbool / boolean,可选当为 true 时(默认值),打开失败的提供方会被报告给 on_error / onError 并从结果中省略。当为 false 时,第一个失败会被引发/抛出
on_error / onErrorcallable,可选对每个无法打开的提供方调用它(当 continue_on_error / continueOnError 为 true 时),以及在一个已经打开的连接随后被最终拒绝(例如授权被撤销)时,以一个 CrossAppAccessError 调用它

返回成功打开的提供方句柄(与 connectToApp / connect_to_app 的句柄类型相同)。如果你的应用没有持有通配符授权,将引发(Python)/抛出(JavaScript)/返回(Go)一个带有 code: NO_GRANT 的 CrossAppAccessError。

在 Python 中,stage、on_error 和 continue_on_error 是关键字参数;在 JavaScript 中,它们通过一个选项对象传入(connectToAllApps({ stage, onError, continueOnError }));在 Go 中通过一个结构体传入(ConnectToAllApps(ctx, ironflock.ConnectToAllAppsOptions{Stage, OnError, StopOnError})),其中 OnError 是一个 func(error),StopOnError: true 对应于 continueOnError: false。

托管文件存储

每个应用的数据后端在表之外还会获得一块私有的对象存储,通过 files 属性访问(在 Go 中为 ifl.Files(),一个可以在 Start 之前获取的 *filestore.FileStore)。可以用它来存放图片、PDF、相机帧、固件二进制块——任何不适合放进表行里的内容。无需任何配置:即使应用的数据模板中没有 files: 段,它仍会获得一个名为 default 的命名空间(声明了自有命名空间的模板会替换它——参见命名空间)。

本节是客户端参考。关于平台一侧——声明命名空间、配额和保留,在应用之间共享文件,按应用的 S3 凭据以及本地部署行为——参见文件存储。

其核心思路在于:存储一个对象时会直接返回一个永久 URL,你可以把它原样写入表的某一列,这样仪表板组件无需任何额外工作就能把它渲染出来:

from datetime import datetime, timezone # Store an object and get a permanent URL back in the same call info = await ironflock.files.put("part-1.jpg", jpeg_bytes, content_type="image/jpeg") # The URL is safe to store in a table column — a dashboard widget can then # render <img src="{{photo_url}}"> without any extra round trip await ironflock.publish_to_table("inspections", { "tsp": datetime.now(timezone.utc).isoformat(), "part_id": "1", "photo_url": info.url, }) # Read it back data = await ironflock.files.get("part-1.jpg") # Every object in the folder 2026/, in its subfolders too (pages are fetched for you) async for obj in ironflock.files.iter(prefix="2026/"): print(obj.key, obj.size)

该 URL 永不过期,但它不是公开链接:只有在此数据后端上持有 READ 权限、且已通过身份验证的请求方才能读取它,鉴权代理会在每一次请求时重新校验这一点。因此把它存入数据库是安全的。

命名空间

命名空间是一个带有策略的键前缀——保留期、共享规则、允许的内容类型。它并不是一个独立的存储桶;一个应用的所有命名空间都位于该应用唯一的那块存储区域之内。只有当一组对象需要不同的规则时才去声明它;否则就留在 default 中,并用类似 2026/03/part-1.jpg 这样的键路径来组织你的对象。

在 data-template.yml 中声明命名空间。一旦声明了任何命名空间,就会替换隐式的 default:请同时列出一个名为 default 的命名空间,否则所有未指定命名空间的调用都会以 NO_SUCH_NAMESPACE 失败。

files: # Storage budget the app suggests for itself. The project user can change it, # and their setting is the one that gets enforced. quotaBytes: 5368709120 namespaces: # Keep default: declaring namespaces replaces the implicit one. - name: default - name: frames description: Raw camera frames, one JPEG per inspected part. contentTypes: ["image/jpeg"] maxObjectBytes: 20971520 retention: { deleteAfter: 30 days }

请注意,该预算是为整个应用声明一次的,而不是按命名空间声明的。命名空间只是应用那块唯一存储区域内部的一个键前缀,因此并不存在什么东西可以让按前缀划分的预算去强制执行。maxObjectBytes 则确实是按命名空间生效的——它限制的是单个对象的大小,而不是总量:未设置时为 100 MiB,最大 5 GiB(超过则为 OBJECT_TOO_LARGE)。

下面的每个方法都接受命名空间作为可选参数,默认为 default;空命名空间("")同样表示 default。

存储与读取对象

# Bytes in, bytes out info = await ironflock.files.put("reports/march.pdf", pdf_bytes, content_type="application/pdf") data = await ironflock.files.get("reports/march.pdf") # Or straight from/to a local file — these stream on the large-object path, # so a multi-gigabyte file never has to fit in memory await ironflock.files.put_file("firmware/v2.bin", "/data/build/v2.bin") await ironflock.files.get_to_file("firmware/v2.bin", "/tmp/v2.bin")
方法描述
put(key, data, …)存储一个对象(Python 中为 bytes,JavaScript 中为 Uint8Array,Go 中为 []byte)。返回该对象的元数据,其中包含它的 url
get(key, namespace?)返回该对象的内容。超过内联上限的对象会直接从对象存储获取;在无法直连时,则经由路由器分段读取(参见大对象)
put_file(key, path, …) / get_to_file(key, path, …)Python 和 Go(PutFile / GetToFile;Go 还可以用 PutReader 从 io.Reader 流式读取,用 GetTo 流式写入 io.Writer)。从本地文件存储,或写入到本地文件。在大对象路径上以流式方式传输
delete(key, namespace?)删除一个对象
copy(key, to, …)复制一个对象,也可复制到另一个命名空间
move(key, to, …)先复制再删除。非原子操作——服务端没有 move 这个动作,因此一旦删除失败,两份副本都会留存

JavaScript 没有提供文件路径辅助方法,因为该包为 Node 和浏览器发布的是同一个构建产物——请自行使用 fs 读写本地文件。

在 Python 中,put_file 和 get_to_file 的文件操作在事件循环之外执行。get_to_file 会创建缺失的父目录,并在拿到对象的第一批字节后就地写入文件:被拒绝的读取(对象不存在、未授权)不会触碰该路径,而中途失败的读取会保留在此之前已写入的内容。FIFO 会随着对象流入而写入,而打开 FIFO 会等待读取方,因此一个没有人读取的 FIFO 会阻塞该调用(而不是事件循环);可以用 asyncio.wait_for 放弃它。put_file 会先把 FIFO 读到结尾再进行存储。

在 Go 中,GetToFile 同样会创建缺失的父目录,但它会先下载到目标旁边的一个临时文件中,完成后再将其重命名到位:失败或被取消的下载绝不会留下不完整的文件,并会保留原有的文件。符号链接会被跟随(写入的是它们指向的文件,链接本身保持不变);替换后的文件会在进程权限允许的范围内获得旧文件的所有者、组和权限位。FIFO 或设备会随着对象流入而就地写入。对于无法在其旁边创建新文件的目标,例如位于只读或伪文件系统上的目标,请对您自己打开的文件使用 GetTo。

put 接受: content_type / contentType(MIME 类型;命名空间可能会限制允许哪些类型)和 namespace。在 Python 中它们是关键字参数;在 JavaScript 中它们放在一个选项对象里;在 Go 中它们是选项(filestore.ContentType(…)、filestore.Namespace(…))。

列举与查看

方法描述
list(…)文件服务列举结果的一页,默认为文件夹式:objects 包含键在前缀之后不再含 / 的对象,prefixes 包含其下的文件夹。delimiter 设为 "" 时为扁平列举——列出前缀下的每一个对象,包括子文件夹中的对象。返回 objects、prefixes、is_truncated / isTruncated / IsTruncated,以及一个 cursor——把它传回即可获取下一页
iter(…) / iterate(…)遍历某个前缀下每一个对象(包括子文件夹中的对象)的异步迭代器,自动翻页。在 Python 中名为 iter,在 JavaScript 中名为 iterate,在 Go 中名为 Iter(一个用于 range 循环的 iter.Seq2[*ObjectInfo, error])
stat(key, namespace?)获取单个对象的元数据,而不传输其内容
exists(key, namespace?)某个对象是否存在
namespaces()本应用可以使用的命名空间
usage(…)本应用占用了多少存储空间——见下文
catalog()命名空间,以及服务器下发的各项限制和配额。只获取一次并缓存:catalog(refresh=True) / catalog(true) / RefreshCatalog(ctx) 会重新获取

文件夹。 以 / 结尾的前缀表示一个文件夹:iter(prefix="2026/") / iterate({ prefix: "2026/" }) / Iter(ctx, filestore.Prefix("2026/")) 会产出 2026/a.txt 和 2026/sub/c.txt,但不会产出 2026-notes.txt。list 无法按名称打开文件夹,因为文件服务会拒绝以 / 结尾的列举前缀(FleetFiles v0.2 返回 INTERNAL):以前缀 2026 调用 list,会在对象中返回 2026-notes.txt,并在前缀中返回文件夹 2026/。该服务以 Unicode 规范化形式 C 存储键,因此文件夹前缀也请使用 NFC;遍历时如果遇到位于该前缀另一种规范化形式之下的键,会直接失败,而不是跳过所有键。

三个 SDK 都用相同的字段来描述对象,只是各自遵循本语言的命名风格:namespace、key、size、etag、content_type / contentType、last_modified / lastModified、checksum_sha256 / checksumSha256 和 url——在 Go 中为 filestore.ObjectInfo 的字段 Namespace、Key、Size、ETag、ContentType、LastModified、ChecksumSHA256 和 URL。

存储用量与配额

usage 由对象存储在一次调用中直接给出答案,因此这些总量是精确的,而不是由 SDK 自行累加出来的:

u = await ironflock.files.usage() print(u.size_bytes, u.object_count, u.quota_bytes, u.free_bytes) # Break the total down per namespace (costs one listing per namespace) detailed = await ironflock.files.usage(detail=True) print(detailed.per_namespace) # {"default": 1048576, "frames": 73400320}

在 Go 中,这些字段是 filestore.StorageUsage 的 SizeBytes、ObjectCount、QuotaBytes、FreeBytes 和 PerNamespace。

字段含义
size_bytes / sizeBytes当前已存储的字节数
object_count / objectCount已存储的对象数量
quota_bytes / quotaBytes实际强制执行的预算。0 表示不限量
free_bytes / freeBytes剩余的字节数。-1 表示不限量——在这里报告 0 会被读作“已满”
per_namespace / perNamespace按命名空间划分的字节数。只有在你请求详细分项时才会出现

按命名空间划分的分项默认是关闭的,因为对象存储无法直接回答这个问题:它是按存储区域来记账的,而命名空间只是一个前缀,所以 SDK 必须逐个列举每个命名空间才能把大小累加起来。需要的时候再去请求它,不要放在热路径上。

这里会出现两种不同的配额,把它们区分开是值得的。catalog() 会同时报告这两者:

字段含义
quota_bytes / quotaBytes实际被强制执行的值,读取自对象存储——也就是项目用户的设置
suggested_quota_bytes / suggestedQuotaBytes应用的数据模板所请求的值。如果它没有请求任何值,则为 0

在 Go 中,Catalog(ctx) 返回一个带有 QuotaBytes 和 SuggestedQuotaBytes 的 *filestore.Catalog。

只要用户调高或调低了应用的预算,这两者就会不同;这也正是为什么强制执行的值是从对象存储读取的,而不是取自模板——重新部署应用绝不能悄无声息地重置用户的选择。UI 可以把两者都显示出来(“应用建议 X,你设置的是 Y”)。强制执行时始终使用前者。用户可以随时更改预算,而 catalog() 报告的是其上次获取时的值:请从 usage() 读取当前值。

共享对象

链接有两种,二者的区别很重要:

方法有效期谁能读取它
url(key, …)永久只有在此数据后端上持有 READ 权限、且已通过身份验证的请求方——每次请求都会重新校验。可以安全地存入表的列中
share_url / shareUrl / ShareURL会过期(默认 15 分钟,上限由服务器强制限定)任何持有该链接的人。 使用它时不会再对授权做任何校验

share_url / shareUrl 是一种 bearer 凭据,即持有链接者即可访问:把它交给需要临时访问的人,不要把它存进数据库。凡是仪表板要渲染的内容,一律使用 url。

在部署环境没有 HTTP 边缘入口时(例如一台仅有纯 HTTP 的一体机设备),url 会返回 None / undefined / ""(Go)——这就是应当回退到 get 的信号。把对象的 etag 作为 version 参数传入(在 Go 中为 filestore.Version(etag)),可以让浏览器以不可变(immutable)的方式缓存响应。

在 Python 和 Go 中,cloud_url(key, …) / CloudURL(ctx, key, …) 返回经由一体机云端文件隧道的永久 URL(https://i<instanceKey>-files.<cloud-edge>):只要一体机在线,就可以从互联网访问,授权方式与 url 相同。在云托管的部署中(此时 url 本身已可从互联网访问)、在没有 HTTP 边缘入口时,以及在数据后端的云共享被关闭期间,它返回 None(在 Go 中为 "")。用户可以随时切换云共享,因此当缓存的目录早于 30 秒时,cloud_url / CloudURL 会重新读取目录。

upload_url / uploadUrl / UploadURL 会签发一个会过期的 URL,它接受直接上传,并返回 url、method、headers 和 expires_in / expiresIn(在 Go 中为一个带有 URL、Method、Headers 和 ExpiresIn 的 *filestore.UploadTarget)。请按原样发送这些请求头,这样对象就会以签发时指定的内容类型存储。命名空间的内容类型和大小上限、声明的 size 以及配额都是在签发 URL 时检查的,而不是在使用时:该 URL 仅针对主机签名,因此任何持有它的人在其过期之前都可以向它对应的键写入任意内容。与 share_url / shareUrl 一样,它也是一种 bearer 凭据——只不过是用于写入的。

大对象

SDK 会根据对象大小自动选择传输方式——没有任何需要配置的地方:

对象大小传输方式
不超过内联上限(目前为 6 MiB)经由消息路由器的一次调用
更大绕过路由器,通过 HTTPS 直接传输到对象存储
更大,且在没有直连路径时读取经由消息路由器分段传输

确切的上限由服务器在运行时给出,即 catalog() 中的 inline_max_bytes / inlineMaxBytes / InlineMaxBytes,因此无需发布新的 SDK 版本就能调高它。

直连路径既需要一个能够预签名 URL 的部署环境,也需要设备能够访问对象存储主机,而不只是路由器。当其中任一条件不满足时——目录表明该部署环境无法预签名(本地一体机默认如此)、签发读取 URL 时返回 NOT_SUPPORTED,或者在收到任何字节之前就无法访问对象存储——get(以及 Python 的 get_to_file、Go 的 GetTo 和 GetToFile)会改为经由路由器分段读取对象(FleetFiles v0.1.33 及以上),每段最多为内联上限。每一段都是一次独立的读取:在这种读取过程中对象若被替换,读取会以 INTERNAL(“object changed while it was read”)失败。写入没有这样的回退方式。

对于写入,仍有两个硬上限,二者都会报告 TOO_LARGE,并在原因说明中指出你触碰的是哪一个:

  • 5 GiB——对象存储的单次上传上限。分片上传尚未实现。
  • 在没有直连端点时的内联上限——无法预签名的部署环境(一体机默认如此)根本无法存储更大的对象。重试或改用更小的分块都无济于事,错误消息中也会这样写明。

直连路径上现场常见的两类故障各有专属的错误码,而不会看起来像是授权问题:PRESIGN_UNREACHABLE(代理只放行通往路由器的流量——读取会回退为分段读取,写入则会失败)和 CLOCK_SKEW(对象存储会拒绝时间偏差超过 15 分钟的请求——请检查设备上的 NTP)。

文件存储错误

文件服务或直连传输的失败都会引发(Python)/抛出(JavaScript)/返回(Go)一个 FileStoreError,它携带一个稳定的 code 和一段人类可读的 reason。请依据 code 分支判断,绝不要依据 reason。 本地文件错误、被取消的调用,以及下文映射之外的路由器层 WAMP 错误,都会按原样引发。

from ironflock.filestore import FileStoreError try: await ironflock.files.put("huge.bin", payload) except FileStoreError as e: if e.code == "QUOTA_EXCEEDED": print("Filestore is full:", e.reason) else: raise
错误码含义
NOT_AUTHORIZED调用方无权执行此操作
NO_SUCH_NAMESPACE该命名空间未在数据模板中声明
NO_SUCH_OBJECT该键不存在
TOO_LARGE超出单次调用的传输上限
OBJECT_TOO_LARGE超出该命名空间自身的 maxObjectBytes
QUOTA_EXCEEDED文件存储已满
CONTENT_TYPE_NOT_ALLOWED该命名空间限制了 contentTypes
INVALID_KEY格式错误的键或前缀(见下文;FleetFiles v0.2 仍将其报告为 INTERNAL)
INVALID_RANGE超出对象范围的字节区间;SDK 从不会请求这样的区间
NOT_SUPPORTED后端无法完成此操作
NOT_AVAILABLE此部署环境没有文件服务——或者暂时还没有:平台重启之后,或文件服务正在注册其过程期间。该调用未在任何地方执行,可以重复调用
PRESIGN_UNREACHABLE无法直接访问对象存储(是不是有代理?)
CLOCK_SKEW设备时钟偏差过大
INTERNAL其他所有情况——也包括在分段读取期间发生变化的对象

较新的服务器可能会引入本次 SDK 发布尚不认识的错误码。它们会作为 code 原样透传,而不会被归并,因此请把无法识别的值当作一般性失败来处理。

文件调用会等待连接(Python 和 Go 中最多 10 秒,JavaScript 中最多 6 秒),并且不会重试:与表操作不同,它们挺不过平台重启,因此在重启期间,调用可能会以 NOT_AVAILABLE 失败,直到文件服务重新完成注册。这样的调用未在任何地方执行,因此你的应用可以重复它。

键。 键是否合法由文件服务裁定;SDK 会按原样发送键和前缀。格式错误的键或前缀——为空;以 / 开头或结尾(iter / iterate 的文件夹前缀除外);含有空段、. 段或 .. 段,或以 . 结尾、以空格开头或结尾的段;含有 \ : * ? " < > | ^ ~ 之一、控制字符或 Unicode 格式字符;超过 900 字节、20 段或每段 255 字节;或位于保留的 _sys/ 前缀之下——会被能够对键错误进行分类的文件服务以 INVALID_KEY 拒绝,而 FleetFiles v0.2 则以 INTERNAL 拒绝。请注意时间戳:由于含有 :,2026-10-09T12:00:00Z 不是一个有效的键段。

在 Python 中,FileStoreError 从 ironflock.filestore 导入;在 JavaScript 中,它由包的根导出(import { FileStoreError } from "ironflock");在 Go 中,它是 *filestore.Error(也可写作 ironflock.FileStoreError),错误码以常量形式提供,例如 filestore.CodeQuotaExceeded。

跨设备通信

registerDeviceFunction / register_device_function

注册一个过程:运行同一应用(处于同一阶段)的其他设备可以调用它,仪表板组件的操作也可以调用它。SDK 会为您构建完整的 URI <SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<topic>,其中 STAGE 为 DEV 或 PROD。路由器不接受应用的任何其他注册,并且只接受单一注册(参见应用可以使用的 URI)。每次重连后,该注册都会被恢复。

def add(a, b): return a + b await ironflock.register_device_function("com.myapp.add", add)

register() 是 register_device_function() 的别名。

callDeviceFunction / call_device_function

调用同一应用的另一台设备注册的过程。SDK 使用目标设备的密钥自动组装完整的 WAMP 主题:<SWARM_KEY>.<device_key>.<APP_KEY>.<STAGE>.<topic>,即 registerDeviceFunction / register_device_function 在目标设备上注册的同一个 URI。

result = await ironflock.call_device_function( 42, # target device key "com.myapp.add", # procedure name args=[3, 5] # arguments ) print(result) # 8

call

使用完整的 WAMP URI 调用远程过程。应用无法注册形如 some.full.wamp.topic 的名称,因此这样的名称只能到达平台服务在应用数据 realm 上注册的过程,例如数据后端和文件存储的过程。要调用您的应用的另一台设备注册的函数,请使用 callDeviceFunction / call_device_function。

result = await ironflock.call("some.full.wamp.topic", args=[42])

应用可以使用的 URI

您的应用容器会以路由器角色 app 加入它自己的数据 realm,即 realm-<SWARM_KEY>-<APP_KEY>-<dev|prod>。路由器会根据该角色的允许列表检查其中的每一次发布、订阅、调用和注册。SDK 自身的方法都在允许范围之内;只有当您把自己的 URI 传给 publish、subscribe 或 call,或直接使用 connection(在 Go 中为 Connection())时,这些规则才有意义。被拒绝的操作会以 wamp.error.not_authorized 失败(在 Go 中,ironflock.WampURI(err) 会返回它)。

您自己 realm 上的 URI允许的操作使用者
您自己的名称,例如 com.myapp.status发布、订阅(也可使用前缀订阅)、调用——不可注册publish、subscribe、call
<SWARM_KEY>.<APP_KEY>.<table>、bulk.<SWARM_KEY>.<APP_KEY>.<table>发布——不可订阅publishToTable / publish_to_table、reportError / report_error、publishRowsToTable / publish_rows_to_table
transformed.<table>、transformed.bulk.<table>仅订阅subscribeToTable / subscribe_to_table
databackend.errors.…、files.events…仅订阅数据后端的错误事件和文件事件
<SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<name>注册(单一注册)、调用、发布——不可订阅registerDeviceFunction / register_device_function、callDeviceFunction / call_device_function
append.…、appendBulk.…、history.transformed.…、secret.reveal.…、secret.verify.…、files.read.…、files.write.…调用(由 SDK 调用)表、历史、机密和文件相关方法
sys.appaccess.list、sys.appaccess.resolve调用listConsumableApps / list_consumable_apps、connectToApp / connect_to_app、connectToAllApps / connect_to_all_apps
wamp.session.get调用JavaScript SDK 的心跳

经过身份检查的名称。 以数字开头的名称——表写入主题和设备函数 URI——还要通过平台的身份检查,该检查会把它们绑定到您 realm 的项目密钥(SWARM_KEY)。只有针对您自己设备的函数 URI,并且其项目密钥、应用密钥和阶段都与您的 realm 一致时,注册才会被接受;前缀注册或通配符注册只有在固定了这四个段时才会被接受。发布和调用只需要您 realm 的项目密钥:发布到带有其他应用密钥的表主题的行会被接受,但不会到达任何数据后端,因而丢失。请让 SDK 来构建这些 URI。

保留名称。 除上面两个跨应用过程之外,路由器会拒绝 sys.;除 wamp.session.get 之外,会拒绝 wamp.。表中的其他名称属于平台:不要让您自己的名称以数字、bulk.、transformed.、databackend.errors.、files.、append.、appendBulk.、history. 或 secret. 开头。其他所有首段都是您应用自己的主题空间。

没有人订阅原始表主题。 这些主题携带的是原样发送的行,包括机密列的明文——数据后端只在存储该行时才对其加密。subscribeToTable / subscribe_to_table 改为从 transformed.<table> 读取已存储的行,这些行已完成类型转换,机密列也已被遮蔽。

其他应用的数据是只读的。 在提供方的 realm 上,您的应用(路由器角色 app_reader)除了心跳 wamp.session.get 之外,只能订阅 transformed.… 和 files.events…,并调用 history.transformed.… 和 files.read.…——不能发布、不能注册、不能写入,也不能使用机密过程。

设备元数据

setDeviceLocation / set_device_location

请求平台更新设备的 GPS 位置,IronFlock 地图会实时显示该位置。

目前不可用: 在应用的数据 realm 上还没有任何平台服务响应此调用,因此它会以 wamp.error.no_such_procedure 失败。在它可用之前,请将位置保存在您自己的表中。

await ironflock.set_device_location(long=8.6821, lat=50.1109)
参数范围
long-180 到 180
lat-90 到 90

位置历史不会被存储。要跟踪位置随时间的变化,请创建专用表并使用 publish_to_table / publishToTable。

getRemoteAccessUrlForPort

返回在应用的 port-template.yml 中声明的端口在其隧道激活后可访问的 URL。

url = ironflock.getRemoteAccessUrlForPort(8080) # "https://<device_key>-<app_name>-8080.app.ironflock.com" vnc = ironflock.getRemoteAccessUrlForPort(5900, protocol="tcp") # "tcp://app.ironflock.com:<the tunnel's remote port>"
参数类型描述
portint / number端口,即在 port-template.yml 中声明的端口
protocolstr / string,可选端口的协议:"http"(默认)、"https"、"tcp" 或 "udp",不区分大小写;首尾的空格会被忽略,空值即为 "http"
remote_port_environment / remotePortEnvironmentstr / string,可选仅限 tcp/udp:保存远程端口的变量,用以替代 REMOTE_PORT_FOR_<port>——即模板中该端口自己的 remote_port_environment,0.19.8 之前的设备代理需要它

http 和 https 端口通过隧道标签 <device_key>-<app name, lower case>-<port>(https 端口带有平台的 secure- 前缀)在设备代理注入的隧道域名(TUNNEL_DOMAIN)上提供,并且始终是 https:// URL。在属于本地一体机的设备上(代理会注入 INSTANCE_KEY),则会改为返回经云端转发的路由 https://i<INSTANCE_KEY>-<label>.<CLOUD_TUNNEL_DOMAIN>。

tcp 或 udp URL 携带隧道分配的公共端口,设备代理会将其公布为 REMOTE_PORT_FOR_<port>(代理 0.19.8 及以上):<protocol>://<TUNNEL_DOMAIN>:<remote port>。在一体机的设备上,只要代理公布了面向互联网的端口 REMOTE_PORT_FOR_<port>_CLOUD,就会优先使用它;在它出现之前,以及代理将其移除之后(云端转发被关闭),返回的是一体机本地的 URL。端口为 0 表示尚未分配。

这些值是实时从 /data/env 读取的,因此请再次调用该方法,而不要缓存结果。有些情况是该 URL 无法满足的:

  • 设备代理只为生产(PROD)安装的端口建立隧道。在开发(DEV)容器中,http(s) URL 是本设备上同一应用的 PROD 安装的 URL,而 tcp/udp 则没有 URL;请改为通过设备的局域网地址(DEVICE_LAN_IP、DEVICE_PORT_FOR_<port>)访问 DEV 应用。
  • 在以无域名方式安装的一体机(纯模式)的边缘设备上,代理会将 TUNNEL_DOMAIN 设为 localhost,因此在那里组合出的 URL 指向 localhost:这样的一体机在其主机端口上提供应用端口,而隧道 URL 无法表达这一点。
  • 未设置或为空的隧道域名默认为 app.ironflock.com。平台目前不会设置 CLOUD_TUNNEL_DOMAIN:除非应用在其环境设置中自行设置该变量,否则一体机的 URL 使用 app.ironflock.com。
  • 在以 root 以外的用户身份运行的容器中,这些值是容器启动时的值(参见环境变量)。

当无法组合出 URL 时,该方法返回 None / null(在 Go 中为 "" 和 false):缺少设备密钥或应用名称、某个 tcp/udp 端口尚未公布远程端口(或为 0)、协议未知,或者标签不是有效的 DNS 标签(超过 63 个字符,或应用名称中含有点号)。浏览器中没有环境变量:http(s) URL 会根据 deviceKey 和 appName 选项在 app.ironflock.com 上组合而成。

连接属性与生命周期

属性类型描述
is_connectedbool平台连接是否处于活动状态
connectionCrossbarConnection底层连接实例(高级用法)
方法描述
run()启动连接并运行 mainFunc(阻塞式)
await start()异步启动连接
await stop()停止连接以及被消费应用的连接,并取消正在运行的任务。这些连接会离开各自的 realm,之后不再重连;start() 会重新建立连接。仍在等待的 start() 或 connect_to_app 会失败(参见连接可靠性)
await run_async()异步启动并保持连接

连接可靠性

SDK 会自行重连。连接断开后,它会持续重试直到路由器恢复,然后恢复所有订阅和所有已注册的设备函数——重连后您无需重新订阅。如果其中某一项无法恢复,其余各项仍会照常恢复,而失败的那一项会在会话持续期间不断重试——首次在 1 秒后,之后间隔逐次翻倍,最长 30 秒,每次等待时间有最多 15% 的随机浮动——并且在每次重连后再次重试:路由器之前接受过它,因此这次拒绝只是暂时的,例如平台对设备函数的身份检查短暂地以失败关闭(fail closed)。第一次失败会以警告形式记录,恢复成功后也会记录一条日志。

表操作——写入、历史/序列/机密读取,以及被消费应用的历史读取——同样能挺过平台重启,例如路由器或平台更新。它们会等待连接恢复,最长等待 reconnect_window(Python,单位为秒)/ reconnectWindowMs(JavaScript)/ WithReconnectWindow(Go,一个 time.Duration),默认 60 秒,并在数据后端尚未提供其过程时重试:即路由器返回 wamp.error.no_such_procedure,或者数据后端以 “client has no handler for registration” 拒绝了一个抢在其注册之前到达的调用。这样的调用从未到达任何处理函数,因此重试绝不会把一行写入两次;任何其他错误都会按原样引发。文件调用不会重试(参见文件存储错误)。如果请求已经发出而其会话关闭(路由器关闭会话、链路断开或调用 stop()),该请求会立即失败,而不是继续等待回应,并且不会重试:它可能已经到达了处理函数。在 Go 中,这样的请求——以及在其等待时限内没有找到连接的任何操作——会以一个包装了 wamp.ErrNotConnected 的错误失败(Stop 之后为 wamp.ErrStopped)。Go 的其他操作(Publish、Subscribe、Call、设备函数、跨应用发现)最多等待 10 秒以获得连接,并且不会重试。

一种特殊情况是路由器可达,但应用的 realm 尚不存在。在并发安装的竞争条件下,应用容器会在数据后端为其创建 realm 之前启动,因此最初的加入请求会被 wamp.error.no_such_realm 拒绝;SDK 每隔几秒重试一次,realm 一出现应用就会启动。如果一分钟后 realm 仍然不存在,那么它很可能永远不会出现(应用已被删除,或该阶段没有数据后端),此时 SDK 会放慢到每两分钟尝试一次,而不是永远每秒敲打路由器。一旦 realm 出现,或路由器本身离线后恢复,就会立即恢复快速节奏。

stop() 会结束所有对连接的等待。仍在等待首次加入 realm 的 start()(例如 realm 尚不存在时)会以 Connection to realm <realm> stopped before it opened 失败。仍在打开提供方连接的 connect_to_app / connectToApp 会以 Connection to app '<app>' (<stage>) aborted by stop() 失败;如果连接最终还是加入了 realm,则会关闭该连接,并以 Connection to app '<app>' (<stage>) closed by stop() 失败。与 stop() 竞争的拒绝仍保留其 CrossAppAccessError。stop() 返回时,已没有任何连接仍在建立中;路由器若不回应离开请求,连接会在 5 秒后被切断。

在 Go 中,Stop(ctx) 同样会结束这些等待,并给出包装了 wamp.ErrStopped 的错误:仍在等待的 Start 会返回它,仍在打开中的 ConnectToApp 会以 ironflock: connection to app '<app>' (<stage>) aborted by Stop 失败(如果连接最终还是加入了,则为 closed by Stop)。Stop 会在 ctx 的时限内等待离开完成,然后关闭套接字;Run 为它留出 10 秒。

更棘手的情况是连接悄无声息地失效:空闲时 NAT 或代理在两端都不知情的情况下切断了它,或者负载均衡器后面的路由器重启了。没有任何信息表明连接已经丢失。只负责发布数据的应用会在下一次写入失败时发现,而只负责订阅的应用从不写入,因此若无额外机制,它会一直处于假死状态,直到容器重启。

三个 SDK 都能检测到这种情况。检测默认启用,无需任何配置。

Python SDK 使用 WebSocket 保活:链路空闲时每 30 秒向路由器发送一次 ping,并期望在 10 秒内收到 pong。若未收到,连接会被丢弃并重新建立。收到的数据同样算作存活证明,因此繁忙的连接从不会被探测——只有链路确实安静下来时才会发出 ping。

通过 connectToApp / connect_to_app 打开的跨应用连接通常只用于订阅,而这正是该机制所保护的场景。它们会被自动覆盖,并且在被路由器拒绝后也会重连,直到拒绝持续存在为止(参见 connectToApp)。

错误处理

每个 SDK 方法在失败时都会明确报错:遇到无效参数、连接丢失或平台拒绝时,它会引发异常(Python)、拒绝(JavaScript)或返回错误(Go),并给出一条包含操作名称、主题和原因的消息。没有任何错误会被悄悄吞掉,因此请将您希望能够继续运行的调用包裹在 try 块中——在 Go 中,请检查返回的错误。

try: rows = await ironflock.getHistory("sensordata", {"limit": 100}) except ValueError as e: # Invalid parameters — e.g. limit out of range, or a malformed filter print(f"Bad query: {e}") except RuntimeError as e: # Not connected, table not in the data-template, or the platform rejected the call print(f"Query failed: {e}")

在 JavaScript 中,来自平台的失败是 WampError 实例——它是一个普通的 Error 子类,并额外在 error 中携带 WAMP 错误 URI,在 args / kwargs 中携带错误负载。其他所有情况(参数错误、未连接)都是普通的 Error。

在 Go 中,失败的操作会返回一个 *ironflock.OperationError,其消息会指出操作和原因,并包装该原因;errors.Is 和 errors.As 可以穿透它。来自路由器或被调用方的拒绝是一个带有 URI、Args 和 Kwargs 的 *ironflock.WampError(*wamp.Error);ironflock.WampURI(err) 返回该 URI,没有时返回 ""。其他哨兵错误有 ironflock.ErrMissingConfig(缺少操作所需的身份信息)、ironflock.ErrAlreadyStarted 和 wamp.ErrStopped(Stop 之后)。

升级说明: 较早版本的 Python 和 JavaScript SDK 在调用失败时会记录一条日志并返回 None / null。现在它们会改为引发错误,因此形如 if result is None: 的代码已无法再检测到失败——请改用 try / except(或 try / catch)。

数据后端拒绝

被数据后端拒绝的操作会以下列 WAMP 错误 URI 之一失败,其第一个参数为原因。三个 SDK 都将它们导出为常量;Go 以 Go 的风格为其命名(ironflock.URIRateLimited、ironflock.URIStorageFull、……)。在 Python 中,IronFlock 的方法会引发一个 RuntimeError,其 cause 为 autobahn 的 ApplicationError(被消费应用的读取会直接引发 ApplicationError 本身),而 wamp_error_uri(err) 可以从这两者中读取 URI。在 JavaScript 中,错误是一个 WampError,其 error 即为该 URI。在 Go 中,它是一个包裹着 *ironflock.WampError 的 *ironflock.OperationError,由 ironflock.WampURI(err) 读取 URI。SDK 不会重试其中任何一种。被数据后端拒绝的已发布行会被丢弃且不报错——只有 append 方法会报告它。

from datetime import datetime, timezone from ironflock import URI_STORAGE_FULL, wamp_error_uri row = {"tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5} try: await ironflock.append_to_table("sensordata", row) except RuntimeError as err: if wamp_error_uri(err) == URI_STORAGE_FULL: pending.append(row) # the appliance's disk is nearly full: append the row later else: raise
常量被拒绝的情况
URI_RATE_LIMITED对于设备上的每个应用凭据,每分钟超过 30 次的 revealSecrets / reveal_secrets 调用,或超过 120 次的 verifySecret / verify_secret 调用
URI_RESULT_TOO_LARGE某次读取中单行超过 8 MiB 的消息预算,或结果超过 1 GiB 的防护上限(其他大结果会分块到达,由 SDK 重新组装)。其关键字参数会指出最大的几列:请降低 limit、缩小 timeRange、选择更少的 columns,或使用 columnPaths 进行裁剪
URI_INVALID_LIMIT以超过 3000 的 limit 读取 transform
URI_INVALID_TIME_RANGE、URI_INVALID_METRIC、URI_INVALID_GROUP_BY、URI_SERIES_TOO_MANY_GROUPS表无法回答的序列查询:没有起点,或终点不晚于起点;表无法聚合的指标(列不存在、机密列、该列类型不接受的方法、对 tsp 使用 COUNT 以外的方法);无法用于分组的 groupBy 列;超过 50,000 行
URI_SECRET_COLUMN、URI_NOT_A_SECRET_COLUMN针对机密列的过滤;对非机密列调用 verifySecret / verify_secret,或对没有机密列的表调用 revealSecrets / reveal_secrets
URI_STORAGE_FULL在一体机磁盘即将写满时进行追加(可用空间低于约 3 GiB;恢复到 4 GiB 起会再次接受)——这是暂时的:请稍后再追加这些行
URI_STORAGE_OVERUSAGE超出账户存储配额的追加;短时间内重试也无济于事
URI_ENTITY_KEY_CONFLICT某一行的实体键(maintainLatestFlagFor)和 tsp 与批次中或已存储的另一行相同:请为每一行分配各自的 tsp
URI_SECRET_SENTINEL_UNRESOLVABLE、URI_SECRET_CIPHERTEXT_REJECTED保留机密先前值(__secret__)的写入,却没有可用于查找该值的实体键;写入已经加密过的值

数据后端不给出原因而拒绝的追加——即它对缺少 tsp 的行所作的回应 wamp.error.runtime_error——不是带类型的拒绝:SDK 的错误消息会指向缺失的 tsp,以及应用的 error-logs 表,数据后端会在那里记录原因。

浏览器使用(仅 JavaScript)

JavaScript SDK 可在现代浏览器中使用。由于浏览器没有环境变量,需要通过构造函数传入所有配置:

import { IronFlock } from "ironflock"; const ironflock = new IronFlock({ serialNumber: "device-serial-from-server", deviceKey: "12", appName: "MyWebApp", swarmKey: 10, appKey: 20, env: "PROD", }); await ironflock.start(); await ironflock.publishToTable("sensordata", [{ tsp: new Date().toISOString(), temperature: 22 }]);

使用 IronFlock.fromServer() 从后端获取配置,而不是硬编码凭据:

const ironflock = await IronFlock.fromServer("/api/ironflock-config"); await ironflock.start();

后端接口应返回包含连接选项(serialNumber、deviceKey、appName、swarmKey、appKey、env)的 JSON 对象。

注册 AI Agent 函数

SDK 可以注册供 AI Agent 调用的函数。注册一个过程并在 ai-template.yml 中引用其主题:

def get_sensor_reading(sensor_id): """Returns the latest reading from a sensor.""" reading = read_from_hardware(sensor_id) return { "sensor_id": sensor_id, "temperature": reading.temp, "humidity": reading.hum, "timestamp": reading.ts } await ironflock.register_device_function("sensors.get_latest", get_sensor_reading)

当用户提出需要实时传感器数据的问题时,AI Agent 可以调用此函数。

要将注册的 WAMP 主题与 AI 代理连接,请在应用的 .ironflock/ai-template.yml 中引用它:

sensor_agent: tool_description: | 当用户询问传感器读数、设备实时数据或当前环境条件时, 请委托给此代理。 system_prompt: | 你是传感器数据专家。使用 get_current 获取任意传感器的最新读数。 在回答中始终包含单位。 main: true max_context_tokens: 30000 max_iterations: 5 tools: get_current: description: 返回传感器的最新读数。 topic: sensors.get_latest parameters: sensor_id: type: string description: 要查询的传感器标识符。 required: true

topic 的值(sensors.get_latest)必须与边缘代码中传递给 register_device_function / registerDeviceFunction / RegisterDeviceFunction 的名称一致。IronFlock 会自动将调用路由到注册了该函数的设备。

有关 ai-template.yml 的完整参考,请参阅定义代理与工具。

环境变量

以下变量由 IronFlock 运行时在应用容器内自动设置:

变量描述
DEVICE_NAME设备显示名称
DEVICE_SERIAL_NUMBER唯一且不可变的设备标识符
DEVICE_KEY用于认证的设备密钥
SWARM_KEY项目标识符
APP_KEY应用标识符
APP_NAME应用名称
ENV环境:DEV 或 PROD
APP_AUTH_ID、APP_AUTH_SECRET应用自己的凭据,SDK 会将其出示给路由器。跨应用访问需要它;没有它时(较旧的设备代理),SDK 会回退为使用设备序列号
import os device_name = os.environ.get("DEVICE_NAME") serial = os.environ.get("DEVICE_SERIAL_NUMBER") project_key = os.environ.get("SWARM_KEY")

远程访问相关变量(TUNNEL_DOMAIN、DEVICE_LAN_IP、DEVICE_PORT_FOR_<port>、REMOTE_PORT_FOR_<port>、……)列在配置端口中。

设备代理还会把这些值镜像到 /data/env,每个值一个 <NAME>.txt 文件,并在应用运行期间保持该镜像为最新;SDK 会优先读取它,因此在运行时发生变化的值(例如远程访问端口)也能到达正在运行的应用。按应用凭据会在每次尝试连接时重新读取,因此代理在应用运行期间轮换的凭据会在下一次重连时出示。代理将 /data/env 设为仅 root 可读:在以其他用户身份运行的容器中,SDK 看到的是容器启动时的值,并会记录一次警告。

Last updated on