Sensor
Sensor 是 Agent 的感官中枢:把外部世界的事件(消息、RSS 更新、闹钟到点……)推送给 Agent,并可将 Agent 的回复发回外部渠道。每个 Sensor 都是一个独立的常驻 Python 子进程。 A sensor is the agent's sensory system: it pushes outside events (messages, feed updates, alarms) into the agent and can carry replies back out. Each sensor is its own long-running Python subprocess.
-
由主进程
SensorManager用uv run <name>.py拉起,依赖通过 PEP 723 内联元数据声明(uv 自动建环境)。 TheSensorManagerstarts it withuv run <name>.py; dependencies are declared as PEP 723 inline metadata and uv builds the environment. - 与主进程通过 stdin/stdout 的单行 JSON-RPC(ACP 方言)双向通信:stdout 只能输出协议 JSON,调试日志必须走 stderr。 It speaks one-line JSON-RPC over stdin/stdout (ACP dialect). stdout carries protocol JSON only; debug output must go to stderr.
- 子进程崩溃会被守护线程拉起;同名 Sensor 已在运行时,会被先整树击杀再重启,避免双实例抢占外部连接。 A watchdog restarts it if it dies. If the same sensor is already running, the old process tree is killed first so two instances never fight over the same external connection.
目录与命名约定Layout and naming
sensors/<sensor-name>/ # 市场仓库中的单元
├── <sensor-name>.py # 单文件脚本,名称 = 文件夹名
└── config.json # 市场/配置清单
| 字段Field | 说明Notes |
|---|---|
name |
安装标识,与文件夹名、代码文件名一致 Install id, identical to the folder and code file names |
description / description-zh |
英文 / 中文一句话描述One-line description in each language |
enabled |
是否启用;registry 占位默认 false,安装后由用户启用
Whether it runs. Registry placeholders default to false; the user enables it after installing.
|
env |
环境变量键值对({ "FEISHU_APP_SECRET": "" })。代码里
os.environ.get("X") 读到的每个键都必须在 env 中声明,留空占位,实际值由用户在前端填写。
Environment keys as placeholders, e.g. { "FEISHU_APP_SECRET": "" }.
Every key read with os.environ.get("X") must appear here. The
real values are filled in from the front end. Never hard-code a secret.
|
tool_detail |
是否向该 Sensor 推送工具调用细节,默认 false(只推回复正文)
Whether tool-call detail is forwarded, default false (reply text only)
|
协议方法Protocol methods
| 方向Direction | 方法Method | 用途Purpose |
|---|---|---|
| 出向(Sensor → 网关)Outbound | initialize |
握手第一步(id=1)Handshake step 1 (id=1) |
session/new |
握手第二步(id=2),响应回传 sessionId
Handshake step 2 (id=2); the reply carries sessionId
|
|
session/prompt |
注入外部事件到当前活跃会话(fire-and-forget)Inject an outside event into the active session (fire and forget) | |
_purrcat/launch_task |
触发后台任务图谱,时钟类专用Start a background graph; intended for clock-like sensors | |
_purrcat/upload_file |
上传文件给 Agent(base64,单文件 ≤ 20MB)Upload a file to the agent (base64, 20MB per file) | |
| 入向(网关 → Sensor)Inbound | session/update |
Agent 侧更新,update.sessionUpdate 区分词汇:agent_message_chunk(文本回复,消息级粒度)、agent_thought_chunk、tool_call、tool_call_update、usage_update、session_info_update。
Agent-side updates; update.sessionUpdate is the vocabulary:
agent_message_chunk (reply text, one event per message),
agent_thought_chunk, tool_call, tool_call_update,
usage_update, session_info_update.
|
_purrcat/file |
Agent 消息提及本地文件时网关自动追加(base64) Appended by the gateway when the agent's message mentions a local file | |
_purrcat/turn_end |
轮次终结标记(params.stopReason),不消费则忽略End-of-turn marker (params.stopReason); ignore it if you don't need it |
stdio Sensor 固定当前活跃会话——网关在 session/new 时无条件注入 follow
标记。你无需、也不应实现任何会话切换逻辑。
A stdio sensor is pinned to the active session: the gateway injects a follow
marker on session/new. Never implement session switching yourself.
开发铁律Hard rules
-
stdout 只能输出协议 JSON(一行一条)。启动时
sys.stdout = sys.stderr,把真实 stdout 另存引用,调试print走 stderr。 stdout carries protocol JSON only, one object per line. Reassignsys.stdout = sys.stderrat start-up, keep the real stdout in a reference, and send debug output to stderr. - 严禁退出主进程。连接崩溃要无限重连(带 sleep 缓冲);守护线程只救意外退出。 Never exit. Reconnect in a loop with a sleep backoff; the watchdog is only there for the truly unexpected.
- 凭证一律从环境变量读取,禁止硬编码密钥。 Read credentials from the environment. Never hard-code them.
- 有游标 / 登录态的必须持久化到脚本同目录的 state 文件——Sensor 会被热重启,不落盘就丢消息、丢登录态。 Persist cursors and login state to a file next to the script. Sensors restart hot; anything held in memory is lost.
- 一切外部网络调用放后台线程,主线程只跑 stdin 读取循环。 Put all network calls on a background thread. The main thread only reads stdin.
-
鉴权求助必须写清凭证名与获取入口(控制台网址 + 步骤);缺凭证时严禁静默退出或空转,应立刻用
session/prompt向 Agent 求助。 Ask for credentials properly: name them and say exactly where to get them. When credentials are missing, never exit silently — immediately ask the agent oversession/prompt.
最小骨架Minimal skeleton
协议体检(合并硬性判据)覆盖:协议纯净、永不退出、契约自述、依赖可装、握手时序、鉴权自服务、sessionId 回填、故障可见。沙盒根目录自带真网关夹具 evals/gateway_probe.py,凭证留空来逼出鉴权路径。
The merge checklist covers protocol purity, never exiting, a truthful manifest, installable
dependencies, handshake order, self-service credentials, sessionId echo, and visible failures. The
sandbox root ships a real gateway fixture at evals/gateway_probe.py that runs with
empty credentials to force the auth path.
# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///
import sys, json, os, threading
# stdout 让给协议,日志走 stderr
_REAL_STDOUT = sys.stdout
sys.stdout = sys.stderr
SENSOR_NAME = "my-sensor"
MY_TOKEN = os.environ.get("MY_TOKEN", "") # 必须在 config.json 的 env 里声明
_SID = ""
_SID_READY = threading.Event()
_ID = 2 # 1/2 留给握手
def send(method, params, rid=None):
global _ID
if rid is None:
_ID += 1
rid = _ID
_REAL_STDOUT.write(json.dumps(
{"jsonrpc": "2.0", "id": rid, "method": method, "params": params},
ensure_ascii=False) + "\n")
_REAL_STDOUT.flush()
def connect():
send("initialize", {"clientInfo": {"name": SENSOR_NAME}}, rid=1)
send("session/new", {"clientInfo": {"name": SENSOR_NAME}}, rid=2)
def prompt(text):
_SID_READY.wait(timeout=30)
if _SID:
send("session/prompt", {"sessionId": _SID,
"prompt": [{"type": "text", "text": text}]})
def on_line(line):
global _SID
try:
msg = json.loads(line)
except Exception:
return
if msg.get("id") == 2 and "result" in msg: # 回填 sessionId
_SID = (msg["result"] or {}).get("sessionId", "")
_SID_READY.set()
return
upd = (msg.get("params") or {}).get("update") or {}
if upd.get("sessionUpdate") == "agent_message_chunk":
text = (upd.get("content") or {}).get("text", "")
# TODO: 把 Agent 回复转发到外部渠道(放后台线程,别阻塞 stdin)
def external_loop(): # 后台线程:收外部事件
if not MY_TOKEN:
prompt("【my-sensor 需要配置】缺少凭证 MY_TOKEN。请到 <控制台网址> 按步骤创建后,"
"在前端配置中心 → Sensor 设置里填写并启用。配置后我将自动开始监听。")
return
while True:
# TODO: 长轮询 / WebSocket 收外部消息
pass
connect()
threading.Thread(target=external_loop, daemon=True).start()
print("[my-sensor] 已启动")
for line in sys.stdin: # 主线程只跑 stdin 读取
if line.strip():
on_line(line)
安装与启用Install & enable
-
安装:市场接口
/api/tools/market/sensors/install,把config.json与<name>.py落到~/.purrcat/sensor/,并合并进~/.purrcat/activate_sensor.json。 Install via/api/tools/market/sensors/install, which writesconfig.jsonand<name>.pyinto~/.purrcat/sensor/and merges an entry into~/.purrcat/activate_sensor.json. -
启用/禁用与填
env:写activate_sensor.json对应条目的enabled/env。 Enable, disable, and fill inenvby editing the matching entry inactivate_sensor.json. -
重载:
POST /api/config/sensor/reload(停掉全部旧进程,按配置重新拉起启用项)。 Reload withPOST /api/config/sensor/reload: every old process is stopped and the enabled entries are started again.