Skip to content

Latest commit

 

History

History
311 lines (222 loc) · 34.4 KB

04-API.md

File metadata and controls

311 lines (222 loc) · 34.4 KB

04 · 通用任务平台 API 契约

阅读入口与阶段状态见00开发导航。本文是HTTP请求、响应、读模型、错误和接口幂等的正式来源;不代表生产身份系统或接口实现已经完成。

版本2.3;前:环境与装配,后:数据库。本文定义任务定义、任务实例、场景命令、Signal、Action诊断和站内收件API。公共业务语义以01为准,场景专有语义以场景目录为准,能力范围以能力目录为准,扩展边界以02为准。

1. API边界与身份

公开路径前缀为/api/v1,内部可信路径前缀为/internal/v1。公开HTTP只通过timeimprint-task-web → timeimprint-task-gateway → timeimprint-task-service-application进入平台,不直接暴露Mapper、Action领取、租约、执行令牌或场景内部表。

网关必须通过ActorContextProvider生成ActorContext。公开任务接口(E01—E13)必须携带Authorization: Bearer会话令牌。令牌只证明账号已登录,actorId等于用户 id,tenant 取配置中的LOCAL_TENANT_ID,请求头和请求体都不得改写。X-Debug-Actor-Id无效。请求体中的 userId、ownerId 或 tenantId 不能声明当前身份。资源查询和命令授权始终使用ActorContext独立判断。无令牌、坏令牌或停用账号返回401 UNAUTHENTICATED。

注册、登录、发短信、校验验证码、忘记密码和微信配置/回调可以不带令牌。其余用户端点必须带令牌;用户列表还要求管理员。

/internal/v1在 local 与 test 只允许本机受信入口,不把终端用户令牌当作内网入口的替代。生产安全不能仅因存在账号登录而标为通过。

2. 通用协议

  • GET用于读取,POST用于创建和命令;一期不提供DELETE和通用PATCH。
  • JSON媒体类型为application/json;请求体最大64KiB;未知字段、重复JSON键和非法类型返回400。
  • ID在JSON中使用字符串;UUID使用标准小写36字符;时间使用ISO-8601 UTC秒精度,例如2026-09-10T10:30:00Z。
  • revision、generation、version、count、limit和weekday/day/interval等数值使用JSON整数;不得用字符串或小数表示。
  • title为1—200个Unicode码点,不允许控制字符或换行;description/body最大4000码点,允许\n,拒绝其他控制字符。
  • 所有文本作为纯文本保存和返回,不执行HTML、模板或脚本,也不自动trim或改变大小写。
  • scenarioKey、providerKey、commandKey和handlerKey使用小写字母开头,后续只允许小写字母、数字、短横线和下划线,最长64字符。
  • 所有写操作必须包含requestId;修改已有定义或实例的命令必须包含expectedRevision。
  • 响应中本契约列出的字段始终出现:不存在的可空标量序列化为null,空数组为[],空扩展对象为{};不得通过省略字段表达null或空集合。

统一响应:

{
  "code": "OK",
  "message": "已查询任务实例详情",
  "traceId": "01J7...",
  "data": {}
}

code为稳定英文机器码(成功恒为OK,失败见第7节)。message必须为简体中文可读说明,面向调用方与AI编排:成功时说明本接口刚完成的动作及关键结果要点(例如创建了哪个定义、执行了哪个命令、revision/场景状态如何变化、是否幂等重放);失败时说明拒绝原因与可执行的下一步(例如应改用allowedCommands中的命令、应先刷新revision)。禁止仅返回OK、success、error等无语义占位词。code=OK表示当前HTTP操作已成功提交,不等于外部通知已送达或用户已读。错误响应同样使用该信封,data为null;不得返回SQL、堆栈、凭据、executionToken或其他用户资源信息。

2.1 JSON实现替换的兼容边界

JSON库、Mapper类型和HTTP消息转换器属于实现细节,从Jackson 2迁移到Jackson 3不得改变本文定义的任何HTTP路径、媒体类型、字段名、必填/可空性、整数与时间格式、空数组/空对象表示、错误码、未知字段拒绝或重复键拒绝语义。

专项迁移开始前,必须对P01发布制品建立可重复的JSON黄金契约,至少覆盖:未知字段、重复键、非法类型、尾随内容、基本类型空值、枚举大小写、日期时间、空值输出、属性顺序、数字边界和错误信封。已由本文明确的项以本文为准;本文未明确但P01存在可观测行为的项,先以黄金契约保持兼容。如果希望改变其行为,必须先按08的公共API变更流程单独评审,不得把Jackson默认值变化当成无意义的底层差异。

响应JSON的对象属性顺序除本文或黄金样例明确要求外不构成普通HTTP客户端契约,但任何用于哈希、幂等摘要或稳定Action Key的JSON必须在05/06指定的规范化字节序列上计算,不得依赖库默认顺序。

3. 公共请求与响应对象

3.1 Calendar schemaVersion 1

S01/S02必须且只能传一个bindingKey="primary"、providerKey="calendar"、schemaVersion=1的trigger binding。config是以下七种互斥对象之一,表中字段全部必填且不允许额外字段。后两种由 P07 增加,不改变前五种的发生结果:

类型 配置精确字段 语义
ONCE type, localDate, localTime, zoneId 在本地日期时间发生一次
DAILY type, startDate, localTime, zoneId 从startDate起每个自然日发生
WEEKLY type, startDate, weekday, localTime, zoneId 从startDate起每周在weekday发生
MONTHLY type, startDate, dayOfMonth, localTime, zoneId 从startDate起每月发生;无该日取月末
EVERY_N_DAYS type, startDate, intervalDays, localTime, zoneId 以startDate为固定锚点每N个自然日发生
EVERY_N_HOURS type, startDate, intervalHours, localTime, zoneId 以startDate与localTime为固定锚点每N小时发生
EVERY_N_MINUTES type, startDate, intervalMinutes, localTime, zoneId 以startDate与localTime为固定锚点每N分钟发生

localDate/startDate格式固定yyyy-MM-dd;localTime固定HH:mm:ss且必须有秒;zoneId一期只能是Asia/Shanghai;weekday为ISO 1—7(周一至周日);dayOfMonth为1—31;intervalDays为1—3650;intervalHours为1—168;intervalMinutes为1—1440。startDate是包含边界,算法只返回同时满足规则且严格晚于after的发生;它可以早于当前日期,不能因重启改写。ONCE在E03/E06提交判定时必须严格晚于事务时间T,E02预览若已不晚于after则返回空列表。

occurrenceAt由本地日期时间按zoneId转换为UTC整秒,occurrenceKey固定为其UTC格式yyyyMMdd'T'HHmmss'Z';唯一性范围还包含definition、binding、scheduleGeneration和controlGeneration。预览、创建和Planner必须调用同一规范化与计算实现。规范化只展开S02默认值、统一日期/时间格式和对象键顺序,不trim文本、不改变数组顺序,也不替调用方猜测缺失的日历字段。

3.1.1 Calendar schemaVersion 2

P09 增加 schemaVersion=2。它接受上一节七种类型,并增加 YEARLY 与 ANCHOR_OFFSETS,字段与语义见 CAP01 的 P09 正式范围。schemaVersion=1 传入这两种类型必须拒绝,且不得改变已有规则的下一次发生。S03 使用 YEARLY 且 offsetDays=[-1,0]。S04 使用 ANCHOR_OFFSETS 且 dailyAfterAnchor=false。S05 使用 ANCHOR_OFFSETS 且 dailyAfterAnchor=true。S14 使用 ANCHOR_OFFSETS、offsetDays=[0] 且 dailyAfterAnchor=false。这些场景仍然每个定义只有一个 primary / calendar 绑定。

E02请求精确为scenarioKey, scenarioSchemaVersion, triggerBindings, scenarioConfig, after, limit,不含requestId;limit为1—100,after为UTC整秒。PreviewResult精确返回scenarioKey, scenarioSchemaVersion, normalizedTriggerBindings, normalizedScenarioConfig, occurrences;每个occurrence含occurrenceKey, occurrenceAt, dueAt。S01 与 S03 的 dueAt 为 null。S02、S04、S05、S14 的 dueAt 等于 occurrenceAt。预览不返回definitionId或写入任何幂等记录。

3.2 创建任务定义

{
  "requestId": "c38aaf21-d8d4-4e67-a077-34855877b220",
  "scenarioKey": "recurring_todo",
  "scenarioSchemaVersion": 1,
  "title": "提交周报",
  "description": "整理本周工作",
  "participants": [
    {"principalType": "USER", "principalId": "zhangsan", "roleCode": "OWNER"},
    {"principalType": "USER", "principalId": "zhangsan", "roleCode": "RECIPIENT"}
  ],
  "triggerBindings": [
    {
      "bindingKey": "primary",
      "providerKey": "calendar",
      "schemaVersion": 1,
      "config": {"type": "WEEKLY", "startDate": "2026-09-01", "weekday": 5, "localTime": "18:00:00", "zoneId": "Asia/Shanghai"}
    }
  ],
  "scenarioConfig": {
    "chaseOffsetsMinutes": [60, 240, 720],
    "notificationExpireAfterMinutes": 1440,
    "maxSnoozeCount": 3
  }
}

CreateTaskDefinitionRequest恰好包含上述字段;除description可为null、S02的scenarioConfig三个字段可省略并展开默认值外,其余字段必填。ParticipantInput精确包含principalType, principalId, roleCode,sourceCode由平台固定写为DIRECT,调用方不能提交metadata或来源。S01 schemaVersion 1的scenarioConfig必须是空对象。scenarioConfig、trigger config和command payload在HTTP边界是JSON对象,但进入场景或能力前必须按key + schemaVersion转换为已注册的强类型对象;找不到类型、版本不兼容或校验失败时不得持久化定义。

S02 schemaVersion 1只接受上述三个场景配置字段:chaseOffsetsMinutes为0—3个严格递增且不重复的整数,每项范围1至notificationExpireAfterMinutes - 1;notificationExpireAfterMinutes范围60—10080,默认1440;maxSnoozeCount范围0—3,默认3。默认催办偏移为[60,240,720]。S01/S02都至少有一个OWNER;RECIPIENT去重后最多10个。缺省RECIPIENT时由场景把OWNER投影为接收人,显式提供RECIPIENT时不自动追加OWNER。公开接口的主体类型只接受USER;OWNER和RECIPIENT的principalId必须是同一tenant内的活跃用户id。停用账号不能新加入参与人。

3.3 定义update的完整替换语义

E06 commandKey=update固定使用commandSchemaVersion 1,payload必须完整提供scenarioSchemaVersion, title, description, participants, triggerBindings, scenarioConfig六个字段且不允许其他字段。description用null显式清空;其余字段不能为null或省略。scenarioKey、definitionId、controlState、createdAt和createdBy不可更新;scenarioSchemaVersion一期必须等于当前版本,未来版本升级使用另行批准的迁移命令,不能借update静默升级。

update是“可变定义快照整体替换”,不是JSON merge、字段级PATCH或数组增量操作。participants整体替换定义级关系;triggerBindings按bindingKey整体替换;scenarioConfig整体替换并先展开默认值。update允许ACTIVE或PAUSED定义:ACTIVE时按01同步更新/重建未来窗口,PAUSED时只保存新定义快照、增加必要版本并保持无未来窗口,待resume从恢复时刻之后规划。规范化后的六个字段全部不变时返回NoChange,revision、scheduleGeneration、Transition和Action均不变。任一字段真实变化时definition revision只增加1;只有calendar配置变化时对应binding的scheduleGeneration增加1,标题、description、participants或S02配置变化不增加scheduleGeneration。更新不得覆盖ACTIVE/TERMINAL实例快照。

pause、resume、retire的payload必须是空对象。pause允许ACTIVE,resume允许PAUSED,retire允许ACTIVE或PAUSED;目标状态已经相同且expectedRevision仍匹配时返回NoChange。RETIRED不可resume或update。DefinitionCommandRequest外层始终为requestId, expectedRevision, commandSchemaVersion, payload。

3.4 场景命令

{
  "requestId": "a7b40754-ff07-45eb-938c-d253d671de01",
  "expectedRevision": 3,
  "commandSchemaVersion": 1,
  "payload": {"reason": "本周无需执行"}
}

定义级稳定命令update/pause/resume/retire由application处理通用控制语义;实例级命令由场景声明,例如S02的complete/skip/snooze。实例commandKey只能路由到(scenarioKey, INSTANCE, commandKey, commandSchemaVersion)唯一TaskCommandHandler,平台不得按具体场景写if/switch,也不得回退给ScenarioExtension。

定义级和实例级命令统一返回CommandResultView:resourceType、resourceId、resourceRevision、changed、resourceSnapshot和scenarioResult。Applied时changed=true;NoChange时changed=false。resourceSnapshot分别是TaskDefinitionView或TaskInstanceView;scenarioResult允许场景返回经版本化、已脱敏的附加结果,没有附加结果时为空对象。API不能因commandKey不同改变最外层响应结构。

S02 complete的payload只允许可选reason,长度0—500个Unicode码点;skip必须提供长度1—500的reason;snooze必须提供UTC秒精度的snoozeUntil且不接受其他字段。成功snooze的scenarioResult返回snoozeCount、actionGeneration和新的剩余提醒时间列表;超过次数、没有可移动Action、时间不在合法窗口或平移后达到失效边界时返回STATE_CONFLICT且不写数据。

3.5 公开读模型

公开响应不得使用“至少返回”或由实现自行增减首期字段。字段固定如下;新增字段必须先更新契约和契约测试。

视图 精确字段
ScenarioMetadataView scenarioKey, displayName, contractVersion, supportedScenarioSchemaVersions, definitionCommands, instanceCommands, requiredCapabilities
ParticipantView principalType, principalId, roleCode, sourceCode
TriggerBindingView bindingId, bindingKey, providerKey, schemaVersion, config, bindingState, scheduleGeneration, nextFireAt, exhausted, revision
TaskDefinitionView definitionId, scenarioKey, scenarioSchemaVersion, title, description, controlState, controlGeneration, revision, participants, triggerBindings, scenarioConfig, allowedCommands, createdAt, updatedAt, pausedAt, retiredAt
TaskInstanceView instanceId, definitionId, scenarioKey, scenarioSchemaVersion, lifecycleCategory, scenarioState, revision, titleSnapshot, descriptionSnapshot, participants, occurrenceAt, dueAt, allowedCommands, scenarioProjection, deliverySummary, createdAt, updatedAt, terminalAt
CommandResultView resourceType, resourceId, resourceRevision, changed, resourceSnapshot, scenarioResult
InboxView inboxId, notificationId, actionJobId, definitionId, instanceId, scenarioKey, purpose, title, body, readAt, createdAt

CommandMetadataView精确包含commandKey, supportedCommandSchemaVersions;版本数组升序。S01/S02的definitionCommands固定声明update、pause、resume、retire四项及commandSchemaVersion 1,具体某一时刻是否可用由TaskDefinitionView.allowedCommands表达;instanceCommands来自已注册TaskCommandHandler。requiredCapabilities是按capabilityKey字典序排列的字符串数组。

allowedCommands只返回当前Actor在响应生成时、基于当前revision可尝试的commandKey,是界面提示而不是后续授权承诺。scenarioConfig、config、scenarioProjection和scenarioResult都是已按版本投影且脱敏的JSON对象;无附加结果返回空对象,不返回null。S01 schemaVersion 1的scenarioProjection只有displayState一个字段,值取PLANNED、TRIGGERED、CANCELLED之一;S02只有snoozeCount, maxSnoozeCount, actionGeneration, chaseOffsetsMinutes, notificationExpireAfterMinutes五个字段,值来自实例快照。complete/skip的scenarioResult为空对象,snooze使用3.4规定的结果。不存在的通用时间返回null。

所有响应数组有稳定顺序:participants按roleCode、principalType、principalId升序;triggerBindings按bindingKey升序;allowedCommands按commandKey升序;occurrences按occurrenceAt、occurrenceKey升序;attempts按attemptNo升序。实现不得依赖数据库未声明顺序。

DeliverySummary固定返回deliveryState, totalCount, readyCount, runningCount, retryWaitCount, succeededCount, deadCount, cancelledCount, expiredCount, unknownCount, inboxCount, unreadInboxCount, updatedAt。计数覆盖该instance全部Action;控制代次失配但尚未物理清理的READY/RETRY_WAIT或未开始副作用的RUNNING Action等效计入cancelledCount,不再计入原技术状态;已提交effectStartedAt的EXTERNAL RUNNING仍计入runningCount直至按实际结果闭合。deliveryState按以下顺序唯一计算:totalCount=0为NOT_SCHEDULED;存在UNKNOWN为UNKNOWN;存在READY/RUNNING/RETRY_WAIT为IN_PROGRESS;全部SUCCEEDED为DELIVERED;有SUCCEEDED且同时有DEAD/CANCELLED/EXPIRED为PARTIALLY_DELIVERED;无SUCCEEDED且有DEAD为FAILED;其余只要有EXPIRED为EXPIRED;最后为CANCELLED。updatedAt取相关Action.updatedAt、Inbox.createdAt和Inbox.readAt中的最大非null值;totalCount=0时为null。这里的DELIVERED仅表示Handler成功,站内信可由inboxCount证明,外部渠道最终送达仍以未来渠道回执为准;用户是否阅读只由unreadInboxCount/readAt表达。

I01的SignalAcceptedView固定返回signalId, duplicated, processStatus, receivedAt;首次接受duplicated=false,幂等重放为true。I02/I06的SignalDiagnosticView固定返回signalId, definitionId, instanceId, providerKey, signalKey, schemaVersion, processStatus, attemptCount, maxAttempts, nextAttemptAt, leaseOwner, leaseUntil, resultCode, resultSummary, occurredAt, receivedAt, processedAt, parentSignalId, redriveNo。I03/I04/I07的ActionJobDiagnosticView固定返回actionJobId, definitionId, instanceId, transitionId, handlerKey, executionMode, schemaVersion, targetType, storedStatus, effectiveStatus, attemptCount, maxAttempts, availableAt, expiresAt, nextAttemptAt, leaseOwner, leaseUntil, outcomeCode, outcomeSummary, completedAt, parentActionJobId, redriveNo, attempts;storedStatus是表中原值。控制代次失配时,READY/RETRY_WAIT以及尚未开始副作用的RUNNING返回effectiveStatus=CANCELLED;已经提交effectStartedAt的EXTERNAL RUNNING仍返回RUNNING直至按实际结果闭合;其他情况effectiveStatus等于storedStatus。Attempt摘要只含attemptNo, startedAt, finishedAt, effectStarted, outcome, errorClass, errorCode, providerReference, safeSummary。I05的TransitionDiagnosticView固定返回transitionId, definitionId, instanceId, sourceType, sourceKey, commandKey, fromControlState, toControlState, fromLifecycle, toLifecycle, fromScenarioState, toScenarioState, fromRevision, toRevision, actorType, actorId, traceId, createdAt,不返回summaryJson原文。I03的status过滤参数匹配storedStatus,I03/I05使用3.6的Page结构。所有内部视图都不返回payload、hash、executionToken或通知正文。

3.6 分页

列表请求使用limit和cursor,默认20、最大100。cursor是不透明base64url字符串,绑定ActorContext、端点、过滤条件、排序字段和上一页边界;不能跨身份、租户、端点或过滤条件复用。非法、过期或不匹配的cursor返回INVALID_CURSOR。

统一分页data:

{"items": [], "nextCursor": null, "hasMore": false, "asOf": "2026-09-10T10:30:00Z"}

定义默认按updatedAt降序、definitionId降序;实例默认按occurrenceAt升序、instanceId升序,历史终态查询可明确选择terminalAt降序;收件默认按createdAt降序、inboxId降序。所有排序必须有ID作为稳定次序。分页采用READ COMMITTED下的弱一致性keyset语义:asOf是响应生成时间而不是数据库快照。不可变排序字段的列表在并发新增下不得重漏;definition的updatedAt在翻页期间变化时允许项目移出本次遍历,调用方需要完整刷新时应从第一页重新开始。

4. 公开端点

编号 方法与路径 请求重点 返回
E01 GET /api/v1/task-scenarios cursor、limit Page
E02 POST /api/v1/task-definitions/preview scenarioKey、版本、triggerBindings、scenarioConfig、after、limit 规范化配置和未来发生预览;不写业务数据
E03 POST /api/v1/task-definitions CreateTaskDefinitionRequest TaskDefinitionView
E04 GET /api/v1/task-definitions/{definitionId} 路径ID TaskDefinitionView
E05 GET /api/v1/task-definitions scenarioKey、controlState、participantRole、cursor、limit Page
E06 POST /api/v1/task-definitions/{definitionId}/commands/{commandKey} DefinitionCommandRequest CommandResultView,内含TaskDefinitionView
E07 GET /api/v1/task-instances/{instanceId} 路径ID TaskInstanceView
E08 GET /api/v1/task-instances definitionId、scenarioKey、lifecycleCategory、scenarioState、participantRole、from、to、cursor、limit Page
E09 POST /api/v1/task-instances/{instanceId}/commands/{commandKey} InstanceCommandRequest CommandResultView,内含TaskInstanceView
E10 GET /api/v1/inbox unreadOnly、scenarioKey、cursor、limit Page
E11 GET /api/v1/inbox/{inboxId} 路径ID InboxView
E12 GET /api/v1/inbox-unread-count 无 unreadCount、asOf
E13 POST /api/v1/inbox/{inboxId}/commands/mark-read requestId InboxView;重复读取保留首次readAt
E30 GET /api/v1/inbox/events Bearer 令牌;无查询参数 text/event-stream。不是 JSON 信封。见 4.2

E01只返回已装配且允许公开的ScenarioMetadataView,不提供运行时上传代码。E02可在不创建定义的情况下调用,但仍要执行场景和触发配置的完整强类型校验。E05 与 E08 只返回当前登录人作为定义级参与人可见的记录。当前账号没有任务时返回空页,状态码仍是 200,不把「别人的任务」或「空列表」报成 404。

E06的update严格遵守3.3完整替换语义;pause、resume、retire遵循01。定义控制命令不得路由TaskCommandHandler。E09先检查场景是否声明并唯一注册commandKey,再检查ActorContext、生命周期、场景状态和revision。allowedCommands只是界面提示,服务端每次仍重新鉴权和校验。

E01—E13 是任务公开接口,不另建第二套任务状态。未来若增加提醒友好入口,只能在gateway转换成上述通用命令。E30 是同一收件模型上的只读事件流,不新增收件字段,也不代替 E10—E13。

4.1 账号端点

E14—E29 由 TimeImprintUser 提供,Task 不再注册这些路径。Task 收到 Authorization: Bearer 后只请求用户服务核对令牌,不查询任务库中的会话表。账号端点的字段、错误码和微信规则以 TimeImprintUser 的 docs/04-API.md 为准。

历史契约仍使用同一响应信封。成功 code 为 OK。密码、验证码、授权字符串、令牌摘要和微信原始报文不得出现在错误响应中。登录令牌只在签发成功的 data.token 返回一次;库中保存其 SHA-256。

编号 方法与路径 请求重点 返回
E14 POST /api/v1/users/sms phoneNumber 只确认已受理;不返回验证码
E15 POST /api/v1/users/sms/verify phoneNumber、verificationCode 验证码是否仍在 10 分钟内有效
E16 POST /api/v1/users/register phoneNumber、code、password、可选 nickname 注册成功后的登录结果;验证码未通过则不创建用户
E17 POST /api/v1/users/login phoneNumber、password 已授权则含 token;未授权则含 authorizationRequired 与 challenge,且不签发 token
E18 POST /api/v1/users/login-authorization challenge、verificationString 校验通过后的 token;失败不签发 token
E19 POST /api/v1/users/logout 令牌 作废当前用户令牌
E20 GET /api/v1/users/me 令牌 当前用户安全字段
E21 POST /api/v1/users/change-password oldPassword、password 修改结果
E22 POST /api/v1/users/change-nickname nickname 修改结果
E23 POST /api/v1/users/password-reset phoneNumber、code、password 重置结果;不存在的手机号不创建用户
E24 GET /api/v1/users keyword、page、pageSize;须管理员 用户页;不含密码、令牌和社交原始数据
E25 GET /api/v1/users/wechat/config appType、可选 next state 与授权地址;不含 appSecret。live 且 appType 为 web 时,state 为一次性随机值,授权地址为微信网站应用扫码地址
E26 POST /api/v1/users/wechat/callback code、state、appType 查找或创建用户后的登录结果
E27 POST /api/v1/users/wechat/bind code、state、appType;须登录 绑定结果
E28 POST /api/v1/users/wechat/unbind 令牌 解绑结果;解绑后必须仍有手机号或可用密码
E29 POST /api/v1/users/wechat/bind-phone phoneNumber、code、password;须登录 绑定或合并后的登录数据

手机号为大陆 11 位。密码非空且不超过 128 字符,只存摘要。验证码为 6 位,10 分钟有效,错误或过期拒绝。昵称最长 30,缺省为「用户」。已注册手机号拒绝再次注册。手机号或密码错误统一返回 401,不说明是哪一项错误。微信身份已属于其他用户时拒绝绑定。绑定手机号若命中另一活跃账号,必须在同一事务内迁入微信身份、更新密码并停用临时账号。空库中第一个完成授权的账号成为管理员。飞书等扫码没有端点。live 模式下只有 appType=web 返回扫码地址;ios 与 android 请求配置时拒绝。state 保存摘要,10 分钟内且未消费才能用于回调或绑定,成功校验后立即消费。capture 不访问微信,测试使用固定 state。由 EVERY_N_HOURS 或 EVERY_N_MINUTES 物化的 S02 实例,稍后命令返回冲突;实例投影含 snoozeAllowed=false。日及以上规则的稍后命令不变。

4.2 站内信事件流(P08)

E30 供已登录客户端在不刷新页面时得知自己的收件变化。阶段范围见 P08。浏览器发起请求并保持响应不结束;服务端不主动连接浏览器。

请求使用与 E10 相同的 Authorization: Bearer。缺少令牌、令牌无效或账号停用时返回 401,响应使用既有错误信封,并且不发送事件流。events 是字面路径,不得被解释为 inboxId。

成功时状态码为 200,Content-Type 为 text/event-stream,响应体按 SSE 帧追加,不使用 ApiResponse 信封。空闲时每 25 秒写一行注释 : ping。注释只用于让前置代理的 120 秒读超时重新计时,不表示有新收件。单条连接最长 30 分钟,到点后服务端结束响应。客户端断开、写出失败或到期后,服务端删除该订阅。

变化帧为:

event: inbox-changed
data: {"unreadCount":1,"latestInboxId":10,"asOf":"2026-10-12T14:00:00Z"}

unreadCount 是当前登录接收人的未读数,语义与 E12 相同。latestInboxId 是该接收人当前最大 inboxId;尚无收件时为 null。asOf 是本次帧的生成时间。帧里没有标题和正文。客户端需要列表时再调用 E10。重复帧必须可以安全忽略。

服务端在收件插入或首次写入 readAt 的事务提交之后,向该接收人当前进程上的订阅写帧。推送失败不回滚收件,也不改变 E13。没有订阅时不保存待投递帧。另一进程写入的变化,由持有连接的进程至多每 5 秒按该接收人水位核对一次后补发。客户端在重连成功后应先调用 E12。

5. 内部端点

编号 方法与路径 用途
I01 POST /internal/v1/task-signals/{providerKey} 可信触发器提交时间、事件、条件或依赖Signal
I02 GET /internal/v1/task-signals/{signalId} 诊断Signal接收、去重和处理结果
I03 GET /internal/v1/action-jobs 按状态、handlerKey、时间和资源ID查询动作积压
I04 GET /internal/v1/action-jobs/{actionJobId} 查询动作及attempt摘要,不返回敏感payload或token
I05 GET /internal/v1/task-transitions 按definitionId或instanceId查询迁移链路
I06 POST /internal/v1/task-signals/{signalId}/commands/redrive 对DEAD Signal创建有审计关联的新Signal,不修改原行
I07 POST /internal/v1/action-jobs/{actionJobId}/commands/redrive 仅对LOCAL_TRANSACTIONAL的DEAD Action创建有审计关联的新Action

I01请求精确包含requestId, signalKey, schemaVersion, occurredAt, subject, payload且不允许额外字段;subject精确包含definitionId和可选instanceId。definitionId必须直接定位一个当前Actor可提交Signal的定义,instanceId存在时必须属于该definition;首期不接受外部业务键解析、无目标、延迟解析或广播Signal。唯一性绑定providerKey + signalKey;同键同摘要返回首次signalId,同键不同摘要返回冲突。Signal成功接收只表示持久化事实已经提交,不表示场景迁移或Action已经成功。

I06/I07请求包含requestId、expectedStatus=DEAD和1—500码点的reason。服务先解析最初的正常根记录;每个根对象最多重驱3次,新行的parentId始终指向该根记录并保存递增redriveNo,不原地重置任何历史行的状态、次数或executionToken。来源记录的definitionControlGeneration必须仍等于定义当前controlGeneration,并重新通过当前控制状态、实例生命周期和Policy屏障;控制代次已变化或业务资格已失效时拒绝重驱,需走显式补偿业务。I07还拒绝EXTERNAL、UNKNOWN及任何可能已经开始外部副作用的Action。两个端点只在受信内部入口启用并写Audit;一期仍不提供Action领取、确认或任意修改次数端点。

6. 幂等、并发与校验顺序

E03、E06、E09、E13、I01、I06和I07使用持久化幂等。幂等唯一键至少绑定tenant/actor、operation、requestId;Signal另有providerKey + signalKey业务去重。请求摘要包含默认值展开后的规范对象和expectedRevision/expectedStatus,不含requestId;对象键排序,数组保序,时间归一UTC,文本不trim。

处理顺序固定为:

  1. HTTP格式、身份、路径和基础字段校验。
  2. 写入PROCESSING幂等行或等待相同唯一键的事务结束并核对摘要;相同请求返回首次COMPLETED结果,不同摘要冲突。同步命令取得command dedup行锁后,才按06顺序锁definition/instance;PROCESSING不得单独提交。
  3. 资源归属、扩展存在性、schemaVersion和强类型payload校验。
  4. 锁定目标并重验控制状态、生命周期、Policy和expectedRevision。
  5. 定义控制命令由application计算计划,实例业务命令由唯一TaskCommandHandler计算计划,Signal由ScenarioExtension计算计划;平台统一校验并原子提交业务事实、Action、Transition、审计和幂等结果。

创建、更新、命令、重驱和规划必须在写入前执行03的事务规模校验。超过参与人、接收人、触发绑定、发生、Action、ScenarioDataMutation、单JSON、计划总字节、预计变更行数或事务时间上限统一返回INVALID_REQUEST或使后台本轮安全缩小;不得静默截断,也不得返回成功后再异步补齐本应原子创建的内容。

确定性成功或业务拒绝可以提交为可重放的COMPLETED幂等结果;数据库死锁、锁等待超时、技术异常或提交结果未知时不得保留PROCESSING占位或返回伪成功。客户端以相同requestId重试;平台通过幂等记录判断返回首次结果或继续安全处理。重复同目标终态命令是否成功由场景声明,但不得重复递增revision、产生Action或审计。

7. 错误码

响应状态码 错误码 说明
400 INVALID_REQUEST JSON、字段、文本、时间、key或组合校验失败
400 INVALID_CURSOR 游标格式、版本、身份或过滤条件不匹配
400 UNSUPPORTED_SCHEMA_VERSION 场景、触发器、命令或Signal版本不受支持
401 UNAUTHENTICATED 没有可信调用身份
403 FORBIDDEN 身份存在但没有执行该操作的权限
404 RESOURCE_NOT_FOUND 资源不存在或对当前Actor不可见
404 EXTENSION_NOT_FOUND scenario/provider/handler未装配或不可用
409 IDEMPOTENCY_CONFLICT 同一幂等键对应不同请求摘要
409 REVISION_CONFLICT expectedRevision与当前版本不一致
409 STATE_CONFLICT 当前控制状态、生命周期或场景状态不允许操作
409 COMMAND_NOT_SUPPORTED 当前场景未声明该commandKey
413 REQUEST_TOO_LARGE 请求体超过64KiB
415 UNSUPPORTED_MEDIA_TYPE 媒体类型不支持
429 POLICY_REJECTED 频控或其他可重试政策暂时拒绝
500 INTERNAL_ERROR 未预期错误;响应不泄露内部信息
503 RETRY_LATER 数据库临时不可用、锁重试耗尽或提交结果待确认

同一资源对无权Actor统一返回404还是403由安全策略决定,但同一端点必须一致并通过防枚举测试。外部Action的DEAD或UNKNOWN不是创建命令的HTTP 500;创建命令成功只说明状态和Action意图已经提交。

正常停机一旦readiness转为REFUSING_TRAFFIC,尚未进入幂等事务的新写请求统一返回503 RETRY_LATER;已经进入事务的请求按06完成或整体回滚。客户端必须以原requestId重试,服务不得在停机窗口返回无幂等记录的伪成功。

8. 首期场景示例

创建S01时使用scenarioKey=reminder,calendar绑定提供五种时间规则;每次发生由平台产生实例并创建notification Action。客户端从TaskInstanceView看到“提醒已触发”,从InboxView查看站内信是否已生成。

创建S02时使用scenarioKey=recurring_todo。完成:

POST /api/v1/task-instances/101/commands/complete
Content-Type: application/json

{"requestId":"a7b40754-ff07-45eb-938c-d253d671de01","expectedRevision":1,"commandSchemaVersion":1,"payload":{}}

跳过使用commandKey=skip并在payload提供必填reason;稍后提醒使用commandKey=snooze并提供未来的snoozeUntil。首期S02的occurrenceAt与dueAt相同,INITIAL在dueAt执行,默认CHASE在其后60、240、720分钟执行,所有动作统一在dueAt后1440分钟失效。所有命令都进入同一个E09管道,不为每个场景动作新增Controller方法。