CounterCall-📞 政府柜台前置清单查询 是做什么的?
Pak Yanto 请了半天假,骑了四十分钟摩托车到 Samsat 柜台,结果因为复印件是 Folio 尺寸而非 A4 而被拒之门外。他损失了一天的收入,而且仍然不知道明天还需要带什么。
公共服务柜台发布的材料清单并不完整,有些已经过时,或者实际上被窗口人员否定。唯一可靠的信息来源是办公室的电话线,但那个电话总是占线、需要 IVR 导航、而且得打到第四次才有人接。所以人们不打。他们直接去现场,排队,然后因为一份文件被拒之门外。
本技能代为拨打这个电话,并返回一份清单,用户可以截图并随身携带。
它与 CALL-E 的设计非常契合:低频、个人化、高风险的电话工作,拨打的是一个人工可能需要反复重拨整个上午的号码。
在开始之前
本技能会真正拨打一个真实公共办公室的电话。 请向用户确认:
- 用户要询问的流程名称,使用办公室自己的术语
- 具体哪个办公室,以及该办公室官网上公布的电话号码(E.164 格式)
- 用户是否希望立即拨打电话
永远不要推断电话号码。 不要来自目录、相似的办公室,或猜测国家代码。电话号码只有在人类用户从办公室自己的官方页面上读到后才能进入流程。拨打错误的号码意味着一个自动呼叫者联系到了陌生人,这是本技能最糟糕的事情。
安全边界
本技能仅收集 柜台要求。
- 它不提交申请、预约、支付费用或让用户承担任何义务。它只询问并报告。
- 它不提供法律或移民建议。如果工作人员建议另一种途径,则如实报告为工作人员的说法,绝不作为推荐。
- 工作人员的口头回答是 信息性的,不具有法律约束力,每个生成的结果都会明确说明这一点。要求会变化,不同柜台会有裁量权。
- 通话开始时明确声明这是一个自动化助手以及拨打原因。如果对方拒绝回答,则感谢并结束通话。不会坚持、重新询问或当天再次致电。
- 每天每个办公室每个流程只打一个电话。 这些是公共服务电话线,工作人员面前有排队等待的人。幂等键强制执行此限制;不要删除它。
- 当工作人员不确定时,结果会注明不确定。它从不填补空白,也从不将模棱两可的内容呈现为事实。用户可能根据此回答跨越城市。
- 仅在办公室公布的营业时间致电。一条公共电话线在晚上 10:00 响铃不是数据点,而是骚扰。
在将此技能适配到新机构前,请阅读 references/safety.md。有些被呼叫者不适合作为自动呼叫的目标。
与普通通话记录的区别
大多数通话技能返回文本并让读者自行解读。此技能返回一个 固定的、经过验证的对象,并拒绝返回其他任何内容。
Goal 的 result_schema 设置了 additionalProperties: false,并将四个字段约束为枚举值 — payment_method、appointment_required、originals_or_copies 和 clerk_certainty,每个都包含一个表示“工作人员不知道”的值。如果通话结束后返回的内容与此不同,结果将被隔离而非渲染。一个解析一半的清单比没有清单更糟糕,因为用户会依据它行动。
完整契约见 references/result-contract.md。
工作流程
1. 收集请求
必要信息:流程名称、办公室名称、办公室电话号码(E.164 格式)以及该号码来源的官方页面 URL。
在发生任何其他操作之前验证号码。 E.164 格式为加号、非零国家代码数字,然后是 7 到 14 位数字:^\+[1-9]\d{7,14}$。拒绝其他任何格式并要求用户。永远不要猜测国家代码。本地格式号码被发送到拨号器是导致错误人员被呼叫的方式。
运行 scripts/preflight.mjs 在不拨号的情况下检查号码和契约。
2. 检查已发布的契约是否发生变化
在每次运行之前,读取实时的 Goal 接口并将其与本技能编写的契约进行比较。
import { diffContract, publishedRunSpec } from './scripts/_lib.mjs';
const goal = await client.goals.get(GOAL_ID);
const drift = diffContract(PINNED_CONTRACT, publishedRunSpec(goal));
if (drift.length) throw new ContractDrift(drift); // 拒绝拨号
使用 publishedRunSpec(goal) 辅助函数,而不是直接使用 goal.published_run_spec。 网络格式和文档拼写为 published_run_spec.result_schema;TypeScript SDK 将其转换为 camelCase 为 publishedRunSpec.resultSchema。只读取文档中的名称会导致 TS 客户端返回 undefined 而不是抛出错误 — 然后检查会得出“没有已发布的规范”的结论,并且每次拨号都拒绝,同时看起来正常工作。 该辅助函数同时读取两种拼写。这让我们浪费了一天时间;写在仓库根目录的 FEEDBACK.md 中。
CALL-E 文档建议在部署变量更改之前进行此比较。在每次运行时进行此操作需要一次 API 调用,并将静默失败转换为明显的拒绝。用过时的 schema 拨打一个真实的人会产生看起来正常但实际上有问题的结果。
3. 拨打一个电话
goals.run 使用一个范围为办公室、流程和日期的 Idempotency-Key,以便重试不会重复拨打公共电话线:
countercall:{office}:{procedure}:{yyyy-mm-dd}:v1
4. 轮询直到得到结果或错误
goals.waitForResult 有一个完成规则:当 result 或 error 非 null 时停止。完成的调用可能会在 CALL-E 解析结构化结果时短暂返回两者都为 null,因此不要将该窗口视为故障。
5. 渲染,或诚实地失败
所有八个已发布的 GoalRunError 代码都对应一个明确的结果。下表是 scripts/render.mjs 实现的一组 — 如果 CALL-E 增加了代码,请保持同步。
| 结果 | 用户看到的 |
|---|---|
| 有效结果 | 清单,包括工作人员的原始说法和来源 URL |
no_answer | “电话无人接听”。无清单 |
declined | “办公室拒绝接听自动呼叫”。无清单 |
result_invalid | 通话完成但答案不符合契约。已隔离 |
result_unavailable | 通话完成但未产生结构化答案。无清单 |
result_failed | 无法将答案处理为清单。无清单 |
timed_out | 截止时间前“没有可用的答案”。无清单 |
call_failed | 无法完成通话。无清单 |
canceled | 在产生答案前取消。无清单 |
任何分支都不渲染部分清单。 每个分支的工作示例见 references/examples.md。
运行方式
scripts/call.mjs 默认是 模拟运行(dry-run)。它会打印它将发送的精确请求,但不拨打电话。实际拨号需要显式的 --live 参数:
# 模拟运行 — 打印它将发送的请求,不拨号,无需凭据
node scripts/call.mjs --office <id> --procedure "<procedure>"
# 实际拨打电话
node scripts/call.mjs --office <id> --procedure "<procedure>" --live
默认拨号的技能就是会意外拨号的技能。
data/offices.json 中的电话号码是屏蔽的(+62XXXXXXXXXX),因此种子条目无法拨号,并且对其执行的每个命令都会退出 3 并提示 拒绝拨号。这是故意的:电话号码只有在人类从办公室自己的官方页面上读到并记录了 source_url 和 source_checked 后才能进入文件。在运行任何命令之前添加自己的条目。
要立即查看模拟输出,测试夹具包含一个有效的虚构号码:
node scripts/call.mjs --offices ../../test/fixtures/offices.test.json \
--office fixture-sourced --procedure "perpanjangan paspor"
何时不适合使用
- 紧急情况。 这是一个具有多分钟延迟的材料查询。它不是紧急联系任何人的方式。
- 拥有已发布、可靠、最新的材料页面的办公室。 如果网站正确,请阅读网站。仅在已知已发布答案不完整时才需要拨打电话。
- 任何已要求不要被自动系统呼叫的机构,或当地法律限制自动呼叫的机构。在添加办公室前检查。
- 高量查询。 每天每个办公室每个流程一个电话是设计上限。如果您需要批量查询,需要数据共享协议,而不是电话。
- 任何容错成本较低的事情。 这里的价值来源于一次不必要旅行的成本。如果用户可以直接去现场了解情况,那就让他们去。
诚实的局限
- 覆盖范围仅限于种子文件中的办公室,每个办公室都有已发布的来源 URL。
- 公共服务线路的接听率因办公室和时段而异,很大一部分电话不会被接听。这种不可靠性正是本技能存在的原因,并且会被如实报告而非隐藏。
- 结果是一个工作人员在某一天的答案。它是证据,而非保证。