English | 中文
https://github.com/user-attachments/assets/9258e5f1-4d00-4c76-8637-8fff220c14dc
Ask4Me 是一个自建的小服务:你通过 API 发起一个“请求”,服务端把交互链接推送到你的通知渠道(Server酱 / Apprise)。你在手机或浏览器里点开链接,按按钮或输入文字提交后,发起请求的那条 HTTP 长连接会拿到最终结果(JSON 或 SSE)。
从概念上说,这是一种 Human-in-the-Loop:这可能是你能自建的最简单的 Human-in-the-Loop 方案之一。
请提供 mcd 或 jsonforms.schema 其中之一。不加任何交互控件虽然也能收到通知,但用户没有任何可提交的部分(没按钮/没输入框),因此也就拿不到有意义的数据。
curl -sS --max-time 120 \
-X POST 'http://localhost:8080/v1/ask' \
-H 'Authorization: Bearer change-me' \
-H 'Content-Type: application/json' \
-d '{"title":"Ask4Me Demo","body":"请点一个按钮回复。","mcd":":::buttons\n- [OK](ok)\n- [Later](later)\n:::"}'
你会收到一条带交互链接的通知。点开链接并点一下按钮后,这条 curl 请求会返回:
{
"request_id": "req_xxx",
"last_event_type": "user.submitted",
"data": { "action": "ok", "text": "" },
"last_event_id": "evt_xxx"
}
本仓库包含:
ask4me)ask4me-sdk(目录:sdk-js/)ask4me-cli(目录:packages/cli/)ask4me-server(目录:packages/server/,用于下载/启动二进制)推荐从 GitHub Releases 下载对应平台的二进制(由 GoReleaser 产出),或使用 ask4me-server 自动下载并启动。
复制一份示例配置:
cp .env.example .env
至少需要:
ASK4ME_BASE_URL:外部可访问的 base URL,用于拼接交互链接(会发到通知里)ASK4ME_API_KEY:API 鉴权 keynotify.failed 结束):ASK4ME_SERVERCHAN_SENDKEYASK4ME_APPRISE_URLSASK4ME_TERMINAL_CACHE_SECONDS:终态结果的内存缓存时间(秒)。当客户端 nonStream/SSE 长连接中途断开时,可在这段时间内用同一个 request_id 再请求一次拿到终态结果;也用于 SSE 订阅者退出后的短期回查。推荐使用 ask4me-server 自动下载/启动:
npm install -g ask4me-server
ask4me-server --config ./.env
或直接运行已下载/已编译的 ask4me:
./ask4me -config ./.env
启动后默认监听 :8080(可用 ASK4ME_LISTEN_ADDR 修改)。
每次发布 Release 时会自动构建并推送到 Docker Hub 和 GHCR:
# Docker Hub
docker pull easychen/ask4me:latest
# GitHub Container Registry
docker pull ghcr.io/easychen/ask4me:latest
使用 env 文件启动:
docker run -d \
--name ask4me \
-p 8080:8080 \
-v $(pwd)/ask4me.db:/data/ask4me.db \
--env-file .env \
easychen/ask4me:latest
或直接传入环境变量:
docker run -d \
--name ask4me \
-p 8080:8080 \
-v $(pwd)/ask4me.db:/data/ask4me.db \
-e ASK4ME_BASE_URL=https://your-domain.com \
-e ASK4ME_API_KEY=your-key \
-e ASK4ME_SERVERCHAN_SENDKEY=your-sendkey \
easychen/ask4me:latest
SQLite 数据库存放在容器内的 /data/ask4me.db,挂载宿主机目录可在重启后保留数据。
nonStream 是默认模式:不带 stream=true 时,/v1/ask 会一直阻塞,直到用户在网页端提交或请求过期,然后一次性返回 JSON。
curl -sS --max-time 120 \
-X POST 'http://localhost:8080/v1/ask' \
-H 'Authorization: Bearer change-me' \
-H 'Content-Type: application/json' \
-d '{"title":"Ask4Me Demo","body":"请点一个按钮回复。","mcd":":::buttons\n- [OK](ok)\n- [Later](later)\n:::"}'
说明:
mcd 或 jsonforms.schema。如果两者都没提供,服务端会降级成一个默认的 OK 按钮。--max-time 120 是一个示例值:避免你在终端里“无限等”。如果 120 秒内你还没点开通知并提交,curl 会超时退出;你可以用 request_id 重新发起“续接等待”(见下文)。serverchan_action_links=true,并确保 ASK4ME_BASE_URL 为 https(Action Link 仅支持 https 链接替换为 sccallback://)。返回示例(终态后返回):
{
"request_id": "req_xxx",
"last_event_type": "user.submitted",
"data": { "action": "ok", "text": "" },
"last_event_id": "evt_xxx"
}
启用 Server酱 3 Action Link 的示例(POST):
curl -sS --max-time 120 \
-X POST 'http://localhost:8080/v1/ask' \
-H 'Authorization: Bearer change-me' \
-H 'Content-Type: application/json' \
-d '{"title":"Ask4Me Demo","body":"请点一个按钮回复。","serverchan_action_links":true,"mcd":":::buttons\n- [OK](ok)\n- [Later](later)\n:::"}'
jsonforms 仅支持 POST JSON(不支持通过 GET query 参数传入)。当提供 jsonforms.schema 时,交互页会渲染表单并忽略 mcd。
curl -sS --max-time 120 \
-X POST 'http://localhost:8080/v1/ask' \
-H 'Authorization: Bearer change-me' \
-H 'Content-Type: application/json' \
-d '{
"title": "JSON Forms Demo",
"body": "请填写表单。",
"jsonforms": {
"schema": {
"type": "object",
"properties": {
"name": { "type": "string", "minLength": 1 },
"age": { "type": "integer", "minimum": 0 }
},
"required": ["name"]
},
"uischema": {
"type": "VerticalLayout",
"elements": [
{ "type": "Control", "scope": "#/properties/name" },
{ "type": "Control", "scope": "#/properties/age" }
]
},
"data": { "age": 18 },
"submit_label": "提交"
},
"expires_in_seconds": 600
}'
当用户提交表单,终态事件 user.submitted 的 data 类似:
{
"action": "",
"text": "",
"payload": { "name": "Alice", "age": 18 }
}
last_event_type 可能的终态值:
user.submitted:用户提交成功(按钮或输入)request.expired:到期未提交notify.failed:通知发送失败(通常是没配置通知渠道或渠道异常)除了 JSON Forms 内置的 Control / Group / VerticalLayout / Label 等元素,Ask4Me 额外内置了三个自定义渲染器,全部通过 options 字段触发,不引入非标准的 type,所以 UI schema 仍然是合法的 JSON Forms。未命中触发条件时,行为会回落到 vanilla 默认渲染器。
1. 可折叠分组(Collapsible Group) —— 把若干表单项打包到一个折叠区,默认收起。适合"高级设置"或想让长表单更紧凑的场景。
{
"type": "Group",
"label": "高级设置",
"options": { "collapsible": true, "defaultOpen": false, "maxHeight": 320 },
"elements": [
{ "type": "Control", "scope": "#/properties/foo" },
{ "type": "Control", "scope": "#/properties/bar" }
]
}
collapsible: true → 用原生 `渲染,label作为标题
-defaultOpen(默认false) → 初始是否展开
-maxHeight`(数字或 CSS 字符串) → 内容区最大高度,超出自动滚动
2. 静态长文本说明(Long Label) —— 不绑定数据字段,仅展示一段长说明 / 须知 / 协议。可以加 maxHeight 滚动,也可以用 collapsible 默认收起。
// 仅滚动
{
"type": "Label",
"text": "<这里是一段很长的说明>",
"options": { "maxHeight": 240 }
}
// 默认收起,点击展开
{
"type": "Label",
"text": "<这里是一段很长的说明>",
"options": {
"collapsible": true,
"defaultOpen": false,
"summary": "查看完整说明",
"maxHeight": 280
}
}
summary(可选) → 折叠时始终可见的标题文字。不填则回落为 "详情"。markdown: true → 把 text 当 Markdown 渲染(GFM:标题 / 列表 / 表格 / 代码块 / 链接 / 图片 / 引用)。结果经 DOMPurify 净化,<script>、事件属性、javascript: 协议都会被剥离。该选项单独存在也能触发本渲染器(无需 collapsible 或 maxHeight)。3. 绑定数据的只读长文本(Read-only Block) —— 把一个字符串字段渲染为不可编辑的滚动块(例如服务端生成的长描述需要回显给用户审阅)。通过 readonlyBlock: true 显式开启。
{
"type": "Control",
"scope": "#/properties/description",
"label": "系统说明",
"options": {
"readonlyBlock": true,
"collapsible": true,
"defaultOpen": false,
"maxHeight": 200
}
}
readonlyBlock: true 时不会触发,照旧走默认 Control 渲染器。label;可用 options.summary 覆盖。markdown: true → 把绑定字段的字符串当 Markdown 渲染(解析与净化同 Long Label)。Markdown 示例 —— 把发布说明(含图片 / 列表 / 链接)作为 Markdown 展示:
{
"type": "Label",
"text": "## 发布 v1.4.0\n\n- 新增 **可折叠** 表单分组\n- 修复 [issue 42](https://example.com/issues/42)\n\n",
"options": { "markdown": true, "maxHeight": 280 }
}
组合示例 —— 顶部放一段折叠的长须知,下面是普通的提交表单:
curl -sS --max-time 120 \
-X POST 'http://localhost:8080/v1/ask' \
-H 'Authorization: Bearer change-me' \
-H 'Content-Type: application/json' \
-d '{
"title": "确认发布",
"jsonforms": {
"schema": {
"type": "object",
"properties": {
"approve": { "type": "boolean" },
"comment": { "type": "string" }
}
},
"uischema": {
"type": "VerticalLayout",
"elements": [
{
"type": "Label",
"text": "本次操作将把 v1.4.0 发布到生产环境。\n\n变更日志: ...(很长)...",
"options": { "collapsible": true, "summary": "查看变更日志", "maxHeight": 240 }
},
{ "type": "Control", "scope": "#/properties/approve", "label": "我已确认并同意发布" },
{ "type": "Control", "scope": "#/properties/comment", "label": "备注" }
]
},
"submit_label": "确认发布"
}
}'
如果你所在环境不方便设置 Authorization: Bearer ... 头,可以用 GET 并在 URL 上带 key:
curl -sS --max-time 120 -G 'http://localhost:8080/v1/ask' \
--data-urlencode 'key=change-me' \
--data-urlencode 'title=Ask4Me Demo' \
--data-urlencode 'body=请点一个按钮回复。' \
--data-urlencode $'mcd=:::buttons\n- [OK](ok)\n- [Later](later)\n:::'
安全提示:URL 参数可能被代理/日志记录;能用 POST + Authorization 时优先用 POST。
当你用 --max-time 40 这类客户端超时后,可以用 request_id 续接等待:
curl -sS --max-time 40 -G 'http://localhost:8080/v1/ask' \
--data-urlencode 'key=change-me' \
--data-urlencode 'request_id=req_xxx'
你可以自行生成一个 request_id(例如在任务队列里预先分配),然后让 server 复用这个 ID 创建请求。这样做有两个好处:
request_id 重新请求并取回终态结果request_id 当作外部系统里的幂等键/关联键使用规则:
request_id 必须以 req_ 开头,且只包含小写字母、数字与下划线示例(GET + 预生成 request_id):
curl -sS --max-time 40 -G 'http://localhost:8080/v1/ask' \
--data-urlencode 'key=change-me' \
--data-urlencode 'request_id=req_myjob_20260131_0001' \
--data-urlencode 'title=Ask4Me Demo' \
--data-urlencode 'body=请回复确认。' \
--data-urlencode $'mcd=:::buttons\n- [OK](ok)\n- [Later](later)\n:::'
下面用 GET 方式演示“逐步加参数”,并用 --data-urlencode 避免手写 URL encode。注意:GET 模式下可交互主要依赖 mcd(JSON Forms 仅支持 POST)。
curl -sS --max-time 40 -G 'http://localhost:8080/v1/ask' \
--data-urlencode 'key=change-me' \
--data-urlencode 'title=Ask4Me Demo'
curl -sS --max-time 40 -G 'http://localhost:8080/v1/ask' \
--data-urlencode 'key=change-me' \
--data-urlencode 'title=Ask4Me Demo' \
--data-urlencode 'body=这是一条测试消息,请点一个按钮或输入一段话。'
curl -sS --max-time 40 -G 'http://localhost:8080/v1/ask' \
--data-urlencode 'key=change-me' \
--data-urlencode 'title=Ask4Me Demo' \
--data-urlencode 'body=请在 10 分钟内回复。' \
--data-urlencode 'expires_in_seconds=600'
curl -sS --max-time 40 -G 'http://localhost:8080/v1/ask' \
--data-urlencode 'key=change-me' \
--data-urlencode 'title=Ask4Me Demo' \
--data-urlencode 'body=请选择一个动作,或输入一段话。' \
--data-urlencode 'expires_in_seconds=600' \
--data-urlencode $'mcd=:::buttons\n- [OK](ok)\n- [Later](later)\n:::\n\n:::input name="note" label="补充说明" submit="提交"\n:::'
MCD 是“交互控件描述”。server 会把 mcd 存到数据库,并在交互页 /r/<request_id>/?k=<token> 里解析它,渲染按钮和输入框。
当前实现是“按行解析”,只识别两类结构,其它内容会被忽略(不会渲染成 Markdown)。
语法:
:::buttons
- [<label>](<value>)
- [<label2>](<value2>)
:::
规则:
label:按钮显示文本value:按钮提交值,最终会出现在终态结果的 data.action:::示例:
:::buttons
- [OK](ok)
- [Later](later)
:::
当用户点击 OK,终态事件 user.submitted 的 data 类似:
{ "action": "ok", "text": "" }
语法:
:::input name="<name>" label="<label>" submit="<submit>"
:::
规则:
label:输入框上方的提示文本submit:提交按钮文本name:当前版本会被解析并保存,但提交时服务端仍固定使用 data.text 作为结果字段(name 预留给后续扩展,不会改变返回 JSON 的字段名)示例:
:::input name="note" label="补充说明" submit="提交"
:::
当用户提交输入,终态事件 user.submitted 的 data 类似:
{ "action": "", "text": "用户输入的内容" }
你可以同时提供按钮与输入框:用户点按钮或输入文本都能完成一次提交;提交后页面会显示 “Submitted.”。
当你需要实时拿到 request.created(包含 interaction_url)以及后续事件流时,使用 SSE:
curl -N -sS \
-X POST 'http://localhost:8080/v1/ask?stream=true' \
-H 'Authorization: Bearer change-me' \
-H 'Content-Type: application/json' \
-d '{"title":"Ask4Me","body":"Please respond.","mcd":":::buttons\n- [OK](ok)\n:::"}'
SSE 输出格式:
data: <Event JSON>\n\ndata: [DONE]\n\nX-Ask4Me-Request-IdSDK 目前默认使用 SSE 模式(会自动加 stream=true),适合在程序里实时消费事件。
安装:
npm i ask4me-sdk
使用示例:
import { ask } from "ask4me-sdk";
const endpoint = "http://localhost:8080/v1/ask";
const apiKey = "change-me";
const { requestId, result } = await ask({
endpoint,
apiKey,
payload: {
title: "Ask4Me Demo",
body: "请随便点一个按钮或回一段话。",
mcd:
":::buttons\n" +
"- [OK](ok)\n" +
"- [Later](later)\n" +
":::\n\n" +
":::input name=\"note\" label=\"补充说明\" submit=\"提交\"\n" +
":::",
expires_in_seconds: 600
},
onEvent: (ev) => {
process.stdout.write(`${JSON.stringify(ev)}\n`);
}
});
console.log("request_id:", requestId);
console.log("final:", result);
安装:
npm i -g ask4me-cli
调用示例:
ask4me-cli -h http://localhost:8080 -k change-me --title 'Ask4Me' --body 'Please respond.'
本仓库提供了 GoReleaser 配置(.goreleaser.yaml)。如果你只想手动交叉编译,也可以用:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o dist/ask4me-linux-amd64 .
用于自动下载/启动 Go server 二进制,并协助生成配置文件。
安装:
npm i -g ask4me-server
启动(默认写入配置到 ~/.ask4me/.env):
ask4me-server
指定配置路径:
ask4me-server --config ./.env
后台运行:
ask4me-server -d
欢迎提交 Issue / PR。开发与贡献约定请见 CONTRIBUTING.md。
如发现安全问题,请按 SECURITY.md 的方式私下报告。
$ claude mcp add ask4me \
-- python -m otcore.mcp_server <graph>