应用场景 / JevModel
Slack 消息分类:用 Events API 和 Jev 给频道消息分流
用一个小型 Slack 应用给求助或反馈频道的消息分类:接收 message.channels 事件、校验签名、3 秒内确认,再向 Jev 提一个固定 Choice 和一个紧急程度 Noul,然后加表情、回帖或转发。包含重试、去重、隐私,以及何时用无代码工具就够。
最近更新:
速读:先确认,再分类
- 订阅 message.channels 机器人事件(权限 channels:history)的 Slack 应用,会在机器人已加入的公开频道里每收到一条消息就收到一次 HTTP POST。用签名密钥校验 X-Slack-Signature 头,然后在 3 秒内返回 HTTP 200;超时会被 Slack 视为失败,并最多重试 3 次。
- 确认之后,只把消息文本发到 JevModel 的 /api/v1/systemone,配一个你定义的 2–12 个标签的 Choice(例如 bug、access、how_to、other)和一个“现在是否被阻塞?”的 Noul。用返回的概率决定加表情、在线程里回复还是转发,不确定的交给人。
- 按 event_id 去重,忽略机器人消息和编辑事件,不要因为一个标签就删除消息。如果已经在用 Zapier、Make、n8n、Power Automate 或 Pipedream,它们的 Slack 触发器无需自建服务器也能调用同一个 Jev API。
Slack 消息分类实际上指什么?
大多数团队至少有一个杂乱请求堆积的频道:#help、#it-support、#customer-feedback、#sales-questions。分类指的是读取每条新消息,从一张简短固定的列表里贴上一个标签,让消息不必有人整天盯着就能到达对的人。标签只有触发了可见动作才有用:负责人按表情过滤的表情回应、追问缺失信息的线程回复、转发到团队频道的副本,或你自己系统里的一张工单。第二个独立问题往往和类别同样重要:现在有人被阻塞吗?把这两个判断分开。一条消息可能是低优先级的操作疑问,也可能是紧急的权限问题;把紧急程度混进类别列表,两边答案都会变差。本文只讨论机器人已加入的公开频道。私信和私有频道需要不同权限和更充分的隐私理由,不在本文范围内。
用 Jev 进行客服分流产品与客服文本的意图分类:规则、LLM 与类型化 APIChoice、Score 与 NoulSlack 应用如何接收频道消息?
用 Events API。在应用设置里开启 Event Subscriptions,选择投递方式:Slack 向其 POST 的 HTTP Request URL,或 Socket Mode(由你的进程打开 WebSocket,不需要公网地址)。在机器人事件下订阅 message.channels;它需要 channels:history 权限,而且只会收到机器人已加入的公开频道消息,所以要在每个需要分类的频道里用 /invite 邀请它。每个事件以 JSON 送达:外层信封(type 为 event_callback、team_id、全局唯一的 event_id)和内层事件(type 为 message,含 channel、user、text、ts,channel_type 为 channel)。如果只想处理 @ 机器人的消息,改订 app_mention。走 HTTP 投递时,Slack 会给每个请求签名:用签名密钥对字符串 v0:{X-Slack-Request-Timestamp}:{原始请求体} 计算 HMAC-SHA256 十六进制摘要,前面加 v0=,用常量时间比较与 X-Slack-Signature 对比,并拒绝与本机时钟相差超过五分钟的时间戳,以防重放。Bolt(JavaScript、Python、Java)会自动完成这一步。新 Request URL 收到的第一个请求是 url_verification 挑战,原样返回 challenge 值即可。
JevModel API:请求与响应运行记录与隐私为什么应用必须在 3 秒内回应 Slack?
Slack 的 Events API 文档要求在三秒内返回 HTTP 2xx;否则本次投递记为失败,Slack 会重试三次:几乎立即一次、一分钟后一次、五分钟后一次,并带 x-slack-retry-num 头(1、2、3)和 x-slack-retry-reason(如 http_timeout)。如果 60 分钟内超过 95% 的投递失败,Slack 可能临时停用你的事件订阅(每小时少于 1,000 个事件的应用不会被自动停用)。分类调用是对另一个服务的网络请求,所以不要让 Slack 等它。先确认,再从队列或后台任务中分类,这也是 Slack 自己的建议。另外三条规则能避免重复和死循环。第一,记录每个处理过的 event_id 并丢弃重复项;重试带着同一个 event_id,而 JevModel 的 API 没有幂等键,不去重的话同一条消息会被分类并计费两次。第二,忽略带 bot_id 或 subtype 的消息(编辑、入群、你自己机器人的回复),否则机器人会给自己的线程回复分类。第三,为用量做预算:Slack 对每个应用、每个工作区每 60 分钟最多投递 30,000 个事件,而每条被分类的消息都是一次普通 Jev 请求,从你的 Token 余额扣除。
JevModel 价格与 Jev 免费额度JevModel API:请求与响应对比:无代码 Slack 触发器 vs 自建 Slack 应用 vs 导出 CSV
三条路都能发送同一个 Jev 问题,区别在于由谁维护管道。下表对比工作流形态,不是准确率,也没有任何一行是基准测试。
Zapier 文本分类:AI by Zapier Classify vs Vertex Classify Text vs HTTP → Jevn8n Text Classifier 节点:内置分类 vs 在工作流中调用 JevJev API 批量文本分类:CSV 与 JSONL| 维度 | 无代码工具(Zapier、Make、n8n…) | 自建 Slack 应用 → Events API → Jev | 频道导出 → Console → Batch |
|---|---|---|---|
| 托管 | 用工具自带的 Slack 触发器;无需服务器 | 你自己的接口或 Socket Mode 进程 | 只需浏览器 |
| 时效 | 取决于工具的轮询或 Webhook 方案 | 3 秒确认之后近实时 | 事后分析 |
| 重试与去重 | 由工具处理;查看其运行记录 | 你负责:保存 event_id,跳过 bot_id 和 subtype | 不适用 |
| Jev 输出 | 同样的 Choice / Noul 答案与概率 | 同上,并完全掌控表情与回复 | 导出文件里的逐行答案(未购买前每批 50 行,购买后 1,000 行) |
代码示例:校验、确认、分类、加表情(示意)
示意代码是一个普通的 Node 18+ HTTP 处理程序,方便看清每一步;签名校验和确认也可以交给 Bolt。给机器人加 channels:history 和 reactions:write 权限,订阅 message.channels,并把机器人邀请进频道。处理程序先对原始请求体校验签名,回应 url_verification,立刻返回 200,然后才调用 Jev。请求遵循 JevModel 文档化的契约:用已验证邮箱账号的 Bearer 密钥 POST /api/v1/systemone,发送 state 和具名问题,这里是一个判断请求类型的 Choice 和一个判断紧急程度的 Noul;成功响应 code 为 0 并带 data.answers。没有 model 字段,因为本站始终通过 OpenRouter 调用 typesafe/jev-1.13;JevModel 与开发 Jev 的 TypeSafe AI 相互独立。低概率答案会加 eyes 表情提醒人工查看,非零 code 则不动这条消息。内存里的 Set 只用于示意;多进程运行时请用共享存储,如果平台在响应发出后会终止任务,请用真正的队列。
JevModel API:请求与响应Choice、Score 与 NoulJev 概率、置信度与人工复核阈值// Illustrative only. Node 18+. Env: SLACK_SIGNING_SECRET, SLACK_BOT_TOKEN, JEVMODEL_API_KEY.
// Bot scopes: channels:history (for message.channels) and reactions:write.
import crypto from "node:crypto";
import http from "node:http";
const handled = new Set(); // event_id values; use a shared store in production
function fromSlack(req, raw) {
const ts = Number(req.headers["x-slack-request-timestamp"]);
if (!ts || Math.abs(Date.now() / 1000 - ts) > 300) return false; // replay window
const mine = "v0=" + crypto
.createHmac("sha256", process.env.SLACK_SIGNING_SECRET)
.update("v0:" + ts + ":" + raw)
.digest("hex");
const theirs = String(req.headers["x-slack-signature"] || "");
return mine.length === theirs.length &&
crypto.timingSafeEqual(Buffer.from(mine), Buffer.from(theirs));
}
http.createServer((req, res) => {
let raw = "";
req.on("data", (chunk) => (raw += chunk));
req.on("end", () => {
if (!fromSlack(req, raw)) return res.writeHead(401).end();
const body = JSON.parse(raw);
if (body.type === "url_verification") return res.end(body.challenge);
res.writeHead(200).end(); // acknowledge within 3 seconds, work afterwards
const e = body.event;
if (!e || e.type !== "message" || e.subtype || e.bot_id) return; // skip edits, joins, bots
if (handled.has(body.event_id)) return; // Slack retries reuse the event_id
handled.add(body.event_id);
classify(e).catch(console.error);
});
}).listen(3000);
async function classify(e) {
const res = await fetch("https://jevmodel.app/api/v1/systemone", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.JEVMODEL_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
state: { channel: "help", text: e.text }, // text only, no user profile
questions: {
kind: {
type: "choice",
instructions: "What kind of request is this help-channel message?",
criteria: {
bug: "Something is broken, slow, or returns an error.",
access: "Asking for an account, a permission, or access to a tool.",
how_to: "Asking how to do something that already works.",
other: "Chat, thanks, or anything that fits none of the labels above.",
},
},
urgent: {
type: "noul",
instructions: "Is someone blocked right now by what this message describes?",
criteria: {
true: "A person or customer cannot work now, or a service is down.",
false: "It can wait for normal working hours.",
},
},
},
}),
});
const out = await res.json();
if (out.code !== 0) return; // leave the message unlabeled
const { kind, urgent } = out.data.answers;
const p = kind.probabilities ? kind.probabilities[kind.choice] : 0;
const emoji = { bug: "bug", access: "key", how_to: "question" };
const names = [p < 0.7 ? "eyes" : emoji[kind.choice]]; // 0.7 is a placeholder
if (urgent.noul >= 0.8) names.push("rotating_light"); // placeholder threshold
for (const name of names.filter(Boolean)) {
await fetch("https://slack.com/api/reactions.add", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.SLACK_BOT_TOKEN,
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify({ channel: e.channel, timestamp: e.ts, name }),
});
}
}
消息被打上标签之后应该怎么做?
优先选择可撤销、可见的动作。表情回应最轻:负责人可以按表情筛选,任何人都能撤掉错的表情。对 how_to 或 bug 标签,在线程里追问截图或订单号很有用,但只在你测试过的阈值之上才发,因为在热闹的频道里,一条错误的自动回复很吵。标签可靠之后,把链接转发到团队频道或在你自己的系统里建工单都没问题。不要根据标签删除、隐藏或移动消息;在确认 Noul 的出错频率之前,也不要只凭它呼叫值班人员。只发送判断需要的内容:通常消息文本就够了,用户名、邮箱和资料字段都别带。上线前,从频道导出几百条历史消息,人工标注,跑同样的问题,按标签对比,再从这份样本里定阈值。示例中的 0.7 和 0.8 只是占位,不是推荐值。关注 other 和 eyes 表情的占比:占比高说明标签定义需要改。
评估 Jev 工作流Jev 概率、置信度与人工复核阈值运行记录与隐私该选哪种方案?
如果无代码工具已经在跑你的其他自动化,而且延迟一分钟左右可以接受,就选它;Zapier、Make、n8n、Power Automate 和 Pipedream 的指南分别演示了同一个 Jev 请求。如果想要近实时的表情回应、完全掌控重试和回复,或者多个频道各有不同的标签集,就自建 Slack 应用。如果目标是一次性分析某个频道被用来做什么,而不是实时分流,就导出频道再用 Console → Batch。无论哪种的检查清单:1)写带明确 other 的标签定义;2)紧急程度用单独的 Noul,不要做成标签;3)按 event_id 去重并忽略机器人消息;4)发送文本,不发资料;5)先只加表情,在标注样本上测过一致率后再加回复或工单。JevModel 只处理文本,且不是开源项目;Slack 里的图片、文件和语音不会被这套方案分类。如果一条消息可能同时属于多个类别,扩大 Choice 之前先看 multi-label-text-classification。
多标签文本分类:一次 Choice、多道是否题,还是第二轮请求Make.com Categorize Text:AI Toolkit vs HTTP → Jev(类型化标签与置信度阈值)Power Automate 文本分类:AI Builder 类别模型 vs HTTP → JevPipedream 文本分类:Hugging Face 动作 vs HTTP → Jev(类型化标签)Jev 是多模态 / 可下载模型吗?JevModel 实际是什么用 AI 自动给 GitHub Issue 打标签:调用 Jev 的 GitHub Actions 工作流用 AI 做邮件分类:入站邮件该用规则、LLM 提示还是类型化决策 API常见问题
给 Slack 消息分类需要公网服务器吗?
不需要。用 Socket Mode 时,你的进程主动连上 Slack 的 WebSocket,不需要公网 Request URL 也能收到同样的事件。带 Slack 触发器的无代码工具也是一个无需自建服务器的选择。
为什么同一条 Slack 消息被分类了两次?
通常是因为应用回应 Slack 超过了 3 秒,Slack 用同一个 event_id 重试了投递。立即确认,保存已处理的 event_id,跳过重复项;JevModel 没有幂等键,无法替你去重。
机器人能给私有频道或私信分类吗?
只有订阅这些会话类型对应的消息事件和 history 权限,并且机器人是成员时才行。本文只讨论公开频道;涉及私密内容时,要先获得工作区管理者的明确同意,并只发送最少必要的文本。
用 Jev 给 Slack 消息分类要花多少钱?
每条被分类的消息都是一次普通 API 请求,按 Token 从余额扣费;验证邮箱的账号先获赠 100,000 免费 Token,同一请求里的 Choice 和 Noul 一起计费。调用前过滤掉机器人消息、编辑和很短的闲聊,可以减少用量。当前套餐见定价指南。
JevModel 为独立项目,与 TypeSafe AI 无关联。