Atlas REST 参考
使用 HTTP 客户端调用受治理的 Atlas 操作。每次请求都受令牌所属组织及权限限制;写入提案需要人工确认。
请求登录验证码
公开接口 POST /auth/send-otp 接受 {email, invite?},签发验证码前仍执行现有邮箱准入与邀请校验。请求被接受时返回 HTTP 200,包含 sent、delivered、delivery、cooldownSeconds 和 codeLength。仅当状态为 provider_accepted 时两个布尔值才为 true:这表示邮件服务接受了发送请求,不代表邮件已到达收件箱。test_passcode 表示正在使用预发布通用测试验证码或指定 UAT 账户验证码;不会发送邮件,两个布尔值均为 false。请使用已配置的测试验证码,响应不会透露其值。not_configured 与 provider_error 的两个布尔值也均为 false。provider_error 可能发生于网络故障,无法确定邮件服务是否接受了请求;false 并不证明邮件未发送。非生产环境可通过 devCode 返回尚未确认发送请求被接受的随机验证码,生产环境绝不返回该字段。HTTP 200 后可继续输入验证码,客户端不应以 sent:true 为继续条件。冷却时间与 POST /auth/verify-otp 校验行为保持不变。缺少邮箱返回 400,准入被拒绝返回 403,冷却或请求限流返回 429 并带有 Retry-After。测试模式跳过发送不代表邮件服务发生故障。
首次请求:查找客户
准备:HTTP 客户端、测试环境账户,以及包含 crm:read 的委托令牌。按 MCP 身份验证说明 获取令牌,并在本地终端将其设为 ATLAS_ACCESS_TOKEN 环境变量;不要将真实令牌复制到文档或日志。
- 将 Acme 替换为你有权访问的客户名称。URL 明确使用测试环境,不代表生产环境可用性。
- 执行以下只读搜索请求。使用 POST 是此操作的协议要求,不代表会修改客户。
curl -sS https://atlas.staging.forth.ai/assistant/v1/actions/crm.customers.search \
-H "Authorization: Bearer $ATLAS_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"q":"Acme","limit":5}'检查结果:响应使用 atlas.action.v1 封装。成功时 status 为 completed;查看 data 中的匹配结果,并保留 request_id 以便排查。空匹配是有效结果,只反映该令牌可见的数据。
恢复与下一步
- 身份验证失败:检查令牌是否缺失、过期或撤销,且是否属于目标环境。权限不足时,按返回错误核对所需范围和组织权限。
- 没有匹配:检查客户名称、筛选条件及令牌所属组织,不要直接创建重复客户。
- 更多操作见 CLI 操作与 REST 对照表。写入请先阅读 提案审核与确认;HTTP 202 表示等待确认,不代表变更已完成。
如何阅读本参考
每个操作需核对方法、路径、权限、输入、结果及错误。下面明确标为内部接口或 /internal/ 的路径通过 Worker Service Binding 调用,不是公开 HTTP 入口,不能使用此处的委托令牌示例调用。
读取自己的腾讯会议私有记录
连接自己的个人版或专业版账号,读取腾讯允许访问的记录,再核对所选摘录,分别保存客户笔记或私人任务。本节说明已实现的接口约定;部署与真实服务商验收仍是独立的发布关卡。
连接并读取
- 以有效成员身份登录 Atlas,所在组织须已启用个人账户模型。打开 超能力 → 插件 → 腾讯会议。
- 选择连接账号。Atlas 会打开腾讯会议授权页。请在那里登录并授权,然后返回 Atlas。如果页面未打开,点击等待窗口中的打开腾讯会议并授权。授权申请五分钟后过期,重试会创建新的申请。取消会停止申请,除非授权已经完成。已有连接可通过重新连接账号更新授权。
- 选择账号和读取操作,填写当前发现的参数,再读取结果。不透明标识须保留为字符串。结果含
has_more:true和next_page_token时,将返回值原样作为下一页的page_token。读取失败或超限会被拒绝,不会显示为成功同步但没有记录。 - 选择原文摘录和客户,打开笔记或任务审核,编辑自己的解读,并在保存前核对目标。笔记按该客户原有权限可见;任务为仅自己可见、分配给自己的私人任务。两种保存分别明确确认;取消不保存任何内容。
以下接口只接受已登录的浏览器会话。每次读取服务商或保存来源内容,都实时核验当前所有者、组织个人账户模型及有效凭据。其他成员、管理员和支持人员不能使用你的账号。Bearer 令牌 MCP/REST 客户端及后台任务不会获得腾讯工具或内容;本页其他 Bearer 示例不适用于此流程。
会话 API
POST /crm/tencent-meeting/authorization/start- 新连接提交
{};重新连接本人账号提交{connection_id,expected_version}。返回{attempt_id,authorize_url,expires_at},其中expires_at为 Unix 毫秒。打开返回的腾讯官方地址,不要复用示例授权码。 POST /crm/tencent-meeting/authorization/:attempt_id/pollPOST /crm/tencent-meeting/authorization/:attempt_id/cancel- 由发起申请的 Atlas 会话提交
{},轮询间隔至少五秒。返回status:pending、connecting、connected、expired、cancelled、denied或failed;连接成功还返回connection_id。申请五分钟后过期,重试使旧申请失效。响应使用private, no-store,服务商凭据仅在服务器加密保存。取消与完成只有一个生效,重复完成不会新增账号。 POST /crm/integrations/tencent_meeting/connect- 仅用于兼容旧私有令牌连接;新的浏览器连接使用上述授权流程。
account_kind:"personal"、fields:{account_name,private_token}。重新连接还需connection_id和当前expected_version;所有者取自会话。成功返回201,包含connection与account,绝不返回令牌。 GET /crm/tencent-meeting/connections{items,can_connect}:仅列出自己的腾讯账号,字段为id、label、status、config_version。没有可访问账号时items为空。GET /crm/tencent-meeting/connections/:id/tools{items}:当前允许的只读操作及其input_schema。参数以发现结果为准,不要硬编码。缺失、不支持或已变化的参数结构会阻止调用。POST /crm/tencent-meeting/connections/:id/read{tool,arguments}。成功包含result_text、source_connection_ids、source_review:{receipt,source_text,provenance}。原文保持不变,上限为 8,000 字符;超限响应整页拒绝。POST /crm/communications/connections/:id/pausePOST /crm/communications/connections/:id/resumePOST /crm/communications/connections/:id/disconnect- 提交当前
expected_version。暂停会停止读取;断开会停止账号访问。生命周期变化、重新连接或自动刷新凭据后,应重新读取再保存旧来源内容。更换令牌会拒绝旧版本的未完成读取与来源凭证,新读取使用更换后的凭据。此前明确保存的笔记和任务仍保留在原目标。 GET /agent/assistant/threads/:id/private-source-status- 仅对本人拥有且仍可读取的对话返回状态;
{review_only:true}表示存在腾讯衍生来源;review_only:false不授予额外权限。关联此对话的通用工作项或个人记忆保存会以409 private_source_review_required拒绝,请使用专门的笔记/任务审核流程。不可读的对话返回404。
发布前核对来源
原样保留返回的 receipt 和完整 source_text,提交的非空 excerpt 必须是其中完全一致的子串。签名来源凭证有效期为 30 分钟,绑定组织、所有者、连接、配置与凭据版本、工具、服务商标识、取回时间及原文摘要。它只证明 Atlas 当时取回了什么,不是服务商认证证据,也不保证腾讯权限此后不变。provenance.source_kind:"provider_generated_summary" 表示智能纪要;你审核后的解读与所选原文保持区分。
请求示例
以下为带占位符的示例。请替换为当前发现结果及所选来源,使用已登录的同源浏览器会话;首个请求发送到读取接口。
{"tool":"get_meeting","arguments":{"meeting_id":"<opaque meeting ID>"}}
笔记请求发送到 POST /crm/customers/:id/facts,并单独设置 x-atlas-idempotency-key 请求头。路径中的客户即审核后的目标。
{"body":"<reviewed interpretation>","category":"other","tencent_source":{"receipt":"<receipt>","source_text":"Available next week.","excerpt":"Available next week."}}
任务请求发送到 POST /tasks。due_at 为毫秒时间戳或 null;填写自己的邮箱,并使用单独的 review_request_id,由 16–128 个字母、数字、下划线或连字符组成。不要提供 source_id、metadata、其他负责人或团队可见性。
{"customer_id":"<customer ID>","title":"<reviewed task title>","description":"<reviewed interpretation>","due_at":null,"assignee_email":"<signed-in email>","visibility":"private","review_request_id":"review_task_example_01","tencent_source":{"receipt":"<receipt>","source_text":"Available next week.","excerpt":"Available next week."}}
两种保存都会重新核验当前来源与目标权限。首次保存返回 201;原样使用同一操作键重试,返回原记录、200 和 replayed:true。同一键改用其他客户或更改审核字段会返回 409 idempotency_conflict。重试须保留原请求;只有另行审核的新操作才使用新键。来源已被撤销时,即使原样重试也可能被拒绝。
限制与恢复
固定只读范围为:get_user_meetings, get_user_ended_meetings, get_meeting, get_meeting_by_code, search_meetings, get_records_list, search_records, get_transcripts_details, get_transcripts_paragraphs, search_transcripts, get_smart_minutes。录制权限仍由腾讯决定,参会身份本身不足以授予访问权。不支持排期、修改会议、邀请、申请权限、下载或自动发布到知识库。工具参数拒绝录制密码和账号凭据;page_token 仍可作为分页游标。
409 personal_accounts_not_enabled:须先启用组织个人账户模型。支持人员不能代建休眠的腾讯账号。credential_rejected、账号暂停或断开:由本人通过腾讯授权页重新连接,或恢复符合条件的暂停账号。tool_reclassification_required:重新连接,验证并固定当前参数结构;管理员分类不能扩大只读范围。invalid_source、source_unavailable或来源凭证过期:重新读取并审核来源后再保存。记录缺失、无权读取、处理中、超时或服务商错误都属于读取失败;Atlas 不会向参会者申请权限。response_too_large:缩小请求范围或页大小。Atlas 不会静默截断一页后声称结果完整。
POST /internal/integrations/tencent-source 是 CRM 为 Tasks 提供的签名 Service Binding 校验接口,不是公开 REST 端点。请求体必须为 {org_id,actor:{principal,expires_at},source:{receipt,source_text,excerpt}};已验证发起人保留原会话有效期,其组织须与 org_id 一致。腾讯令牌保留在 CRM,不会将原始令牌交给 Agent、Tasks 或 MCP 客户端。
不使用 CLI 调用 REST
内部邮箱权限核验
POST /internal/integrations/authority 是 Auth 为 CRM 提供的内部 Service Binding 接口,不是公开 REST 端点。每次请求均对请求体签名,并绑定已验证的发起人、原始凭据有效期及单一权限操作。Auth 实时核验会话或委托令牌、组织有效成员身份和当前权限,再返回已有的不透明用户标识。权限缺失或无法核实时拒绝操作;调用方提交的所有者、组织、支持标志或操作列表均不能授予权限。
CRM 负责账户权限判断,并向内部调用方返回 allowed_actions。Agent 通过请求体签名调用 CRM 的 Service Binding 端点 POST /internal/integrations/outbound-credential 和 POST /internal/integrations/account-effect-versioned,仅取得当前已验证的 SMTP 配置,并在打开套接字前固定凭据版本;旧版 POST /internal/integrations/account-effect 仍作为兼容性核验。这些接口均不允许公开路由访问,响应使用 no-store,且绝不返回收信设置、信封 JSON 或提示信息。上线顺序为先部署 CRM,再部署 Agent;新 Agent 上线后移除 Agent Worker 的 COMM_CREDENTIAL_KEY,因为凭据解密始终只在 CRM 内完成。协助配置不授予读信或发信权限。邮箱操作开始前重新核验当前权限;权限撤销后停止后续工作,已经开始的服务商操作仍记录结果。成员枚举仅限已鉴权的授权迁移操作。未绑定已验证发起人的旧定时同步任务会在访问服务商前被拒绝;已有消息和游标均保留。个人账户创建及实名授权的启用仍等待各自后续任务验收(#2823)。
内部政策评估
类别评估(#3733)。POST /internal/policy/evaluate 是 EKB 面向 Atlas 服务的 Service Binding 接口,不是公开 REST 端点。请求以内部 HMAC 签名,携带 {principal, category, facts}:category 为 task-creation、record-change、draft-preparation、external-communication、administration 之一;facts 只接受 actor_role 与 customer_class 两个键,值为字符串,或显式 null 表示已知缺失。组织仅取自签名 principal——任何请求字段都不能选择租户。评估覆盖该组织所有已发布的政策版本(已撤回的版本不计入),最强结果胜出(none < warn < block),并返回每条命中理由及其政策标题、章节标题和至多 200 字的引文,以及确切的 policy_set_revision。当适用的规则需要请求未提供的事实时,结果为 could_not_verify 并指明缺失的事实——绝不静默返回 none;显式 null 的事实不匹配任何条件,也不会变成无法核实。规则写入(PUT /knowledge/policies/:id/rules)需要 Knowledge 规则权限;schema-2 预览(POST /knowledge/policies/:id/rules/preview,携带 {base_version, rules})报告这些规则对最近 30 天组织助理提案的影响,不产生任何效果或版本。schema-2 规则可携带可选的 permitted_alternative(允许的替代做法),至多 300 字;超长或为空的写入会以 400 拒绝,命中的每条理由都会原样携带 permitted_alternative(#3954)。章节可在英文标题与正文旁携带简体中文标题与正文(title_zh、body_zh);命中的章节若有中文标题,理由会以 heading_zh 返回。未设置这两个字段时它们会被省略——绝不是空字符串——因此既有政策的评估结果逐字节保持不变。
评测活动模型路由
路由试验(STORY-014)。POST /governance/eval/campaigns 是预发布环境中供平台运维使用的仓库接口,不是委派令牌端点。JSON 请求体可携带可选的 modelRoute,取值为 glm_only 或 deepseek_only。在任何评测世界创建之前,其他取值会以 400 model_route_invalid 拒绝;以不同路由(或增删路由)重复同一个 idempotencyKey 会以 409 idempotency_conflict 拒绝。指定路由后,每个评测世界自己的评测组织会在播种数据和第一次助手运行之前被设为该 Atlas 托管预设;租户组织的策略绝不会被写入。预设不带回退:若该路由的平台凭据缺失,试验会以网关的 model_route_unavailable 原因失败,绝不换用其他模型,所有调用仍受治理并计量。报告中的 identity.requestedModelRoute 写明请求的路由,identity.routeMismatchTrialIds 列出实际追踪模型与 bigmodel/glm-5.3-flash 或 deepseek/deepseek-v4-flash 不一致的试验;摘要同时显示两者。失败的试验同样记录每次已启动运行实际使用的模型,因此也可能出现在该列表中;报告会显示其记录的失败详情,例如 run ended failed: model_route_unavailable。不带 modelRoute 时,活动、其存储的清单与报告都与原来一致,两种情况下对比兼容键也相同。
评测活动功能预览
功能预览(STORY-167)。POST /governance/eval/campaigns 还可携带可选的 featurePreview:一个非空的已登记功能键列表,每个键只出现一次,例如 ["assistant-plan-prefix"]。其他取值(包括未登记的键)会在任何准入或评测世界创建之前以 400 feature_preview_invalid 拒绝;以不同列表重复同一个 idempotencyKey 会以 409 idempotency_conflict 拒绝。指定列表后,每个评测世界自己的评测组织会在播种数据和第一次助手运行之前,为列表中的每个键获得一条已启用的组织级覆盖,写入方式与“平台 → 功能可见性”相同并留有审计记录;租户组织绝不会被写入。这些覆盖会在清理评测世界时移除;无法写入覆盖的试验会直接失败,绝不在缺少覆盖的情况下运行。活动记录及其存储的清单会带上 featurePreview;清单摘要与报告的对比兼容键不变,因此开启开关的评测可以与未开启的评测对比。
评测模型路由对比
路由试验对比(STORY-123)。GET /governance/eval/reports/:id/compare-route?to=:id 是预发布环境中供平台运维使用的仓库接口,不是委派令牌端点:其他调用者收到 403,生产环境在认证之前即返回 404。它对比两份只有请求路由不同的报告:沿用 /compare 的全部检查(模型身份键除外),并额外要求两份报告都带有 identity.requestedModelRoute 且取值不同、两份报告的 identity.routeMismatchTrialIds 都为空、每份报告的模型身份等于其路由的模型,且两次活动的 Worker 身份相同。否则返回 {"comparison": {"compatible": false, "mismatches": [...]}},逐一写明字段(例如 requestedModelRoute、workers.agent、to.modelPolicy),routes 与 cases 为 null。兼容的一对返回 routes.from 与 routes.to,各含 passAt1、passAllThree 计数,agentMsPerTrial(试验记录的 Agent 耗时的 p50/p90),modelLatencyMsPerCall(该试验启动的每次运行在试验世界组织内的计费 usage_events.latency_ms 的 p50/p90)以及 modelCallsPerTrial;另有 cases(每个用例 pass@1 与 pass^3 的 fixed、regressed 或 unchanged)、changedCaseIds 与 safetyRegressions(退步的 safety-* 用例)。只有每个试验都有相应证据时才给出百分位或均值,否则为 null,覆盖计数说明找到了多少。已启动的运行若没有任何计费 usage_events 记录,该试验计为未覆盖(未知,而非零次调用),除非其追踪记录为未使用模型。该接口只报告,不替你选择路由。缺少 to 返回 400 missing_to;平台运维查询不存在的报告返回 404;其他调用者在查询任何报告之前即收到 403。/compare 保持不变。
助理政策准入与覆盖
每个助理操作都先经过准入(#3735)。在助理操作展示给用户确认之前,以及确认时,Agent 都会按操作类别、操作人角色和客户分类评估组织已发布的政策(POST /internal/policy/evaluate)。没有分类的客户,或不涉及客户的操作,会以“已知缺失”发送,不会匹配任何分类规则。结果以 policy 字段随操作信封返回——{outcome, category, reasons, policy_set_revision},其中 outcome 为 none、warn、block 或 could_not_verify,每条原因都带有政策标题、条款标题和引文——出现在提案(POST /assistant/v1/actions/:id 与 GET /assistant/v1/proposals/:id)、每次拒绝以及确认后的结果上。操作自身的 data 不变。若被引用的规则标注了允许的替代做法,或被引用的章节带有中文标题,每条原因还会携带 permitted_alternative 与 heading_zh——网页阻止卡片、MCP 工具结果与 CLI 应答一视同仁(#3954);未设置这些字段的政策不会返回它们。
确认。POST /assistant/v1/proposals/:id/confirm 的请求体为空,或为 {"override_reason": "..."}。评估接受助理确认时使用的所有主体,包括没有 tasks:write 的委托令牌和定时运行。block 会将提案交由组织负责人或管理员决定。其他主体(包括委托令牌和定时运行)收到附有政策原因的 403 owner_decision_required,重复提交 override_reason 也不能继续。已登录的负责人或管理员可通过 POST /agent/assistant/owner-decisions/:id/decide 提交 10–500 字的理由来决定搁置计划;获批确认会记录覆盖及引用的政策章节。could_not_verify(EKB 或 CRM 不可用、超过 5 秒超时,或组织无法读取该客户)一律以 503 policy_unverified 拒绝且不执行,任何理由都无法解除。warn 会继续执行,并记录在结果上。若确认时的结果比展示时更严格,确认会以 409 policy_changed(“指引已更改,请重新查看”)拒绝,提案显示新结果并保持待确认。被拒绝的确认不会消耗幂等键;已完成确认的重放会返回记录下的结果(含 policy 与 override),不会再次执行。定时运行提交的覆盖理由会以 403 override_not_allowed 拒绝;没有政策类别的操作会以 403 policy_category_unknown 拒绝。
不经过提案的路径。POST /agent/assistant/brief/step/accept 和针对 task_recommendation 行的 POST /agent/prepared/:id/approve(创建任务),以及 POST /agent/engagement-plan/draft 和 POST /agent/prepared/on-demand(准备草稿)接受可选的 override_reason,返回相同的代码:在提供理由之前以 409 policy_blocked 返回 policy,或 503 policy_unverified;成功时返回 policy,覆盖后还会返回 override。发送助手准备好的已批准邮件草稿也按同一方式准入(#3757):POST /agent/prepared/:id/send 和 POST /agent/prepared/:id/retry-send 接受可选的 override_reason(重试时与 confirm_duplicate_risk、prior_command_id 一同提交),并作为外部通信、以发送人身份、针对草稿客户先行准入,之后消息才会离开 Atlas——拒绝代码相同,发送成功的应答在 outbox/command_id 之外还携带 policy,覆盖后还有 override。重试自身的前置检查(400 retry_confirm_required、409 send_in_flight、409 send_already_accepted、409 retry_stale_prior)先应答,不做政策检查;只有准入通过后才会占用单槽认领。根本无法发送的草稿——其他类型、未批准或没有可解析收件人——保持原有拒绝(404 not_found、400 wrong_kind、400 wrong_status、400 no_recipient),不做准入。task_recommendation 只能批准一次:批准已不再待处理的行会返回 409 already_decided;在另一次批准进行中时批准、编辑(POST /agent/prepared/:id/edit)或忽略(POST /agent/prepared/:id/dismiss)该行,会返回 409 in_progress(请稍后再试)——均不做政策检查、不记录覆盖、不创建任务。任务创建限时 30 秒;超时返回 504 task_create_failed,该行保持待处理。
政策覆盖列表。GET /agent/assistant/policy-overrides 按时间倒序列出组织内来自所有路径的覆盖记录——{items, next_cursor},每项为 {id, at, actor_id, operation, proposal_id, subject_ref, customer_id, reason, sections, policy_set_revision};用 ?cursor= 获取下一页(?limit= 每页最多 100 条)。仅管理员和经理可见;其他人收到 200 {"items":[],"next_cursor":null}。记录只增不改。只有通过全部前置条件的尝试才会记录覆盖:因 already_pending、not_found、already_decided、in_progress 或提案已过时而被拒绝时不会记录;提案确认的覆盖与完成结果一并记录。在发送准备好的草稿时(#3757),政策准入先于发送侧自身的限流与重复检查,因此为一次随后返回 429 rate_limited 或 already_sent 的发送给出的覆盖会被保留,重试时会再次准入。GET /crm/internal/customers/classes 现在还会为每个 id 返回 found,因此组织无法读取的 id 不会被误认为是没有分类的客户。
政策决策记录与按策略统计(#3951)
每次政策评估现在都会向 Agent 专属、仅追加的 agent_policy_decisions 表(迁移 0029_policy_decisions.sql)追加一行:操作者、主体类型(person、scheduled、mcp、cli)、操作与类别、客户及其分类、结果(none、warn、block、could_not_verify)、would_block(在 warn-only 发布前恒为 0)、被引用的策略与章节 id 及其评估所用的策略集修订版,以及来源——评估本身为 rule,获批的覆盖为 override(同一时刻的第二行,以覆盖记录的 id 为键),permission 预留给权限分级决策。数据库触发器拒绝 UPDATE 与 DELETE,与 assistant_policy_overrides 一致。
按策略统计。GET /agent/assistant/policy-metrics?policy_id=&period=week|month|quarter(仅负责人或管理员;其他人收到 403 forbidden)返回 {period:{start,end}, checked, warned, blocked, would_block, overridden, could_not_verify, coverage, overrides}。统计覆盖 UTC 自然周(周一起)、自然月或自然季,期间没有记录时返回零而非报错。checked、warned、blocked 与 would_block 统计引用了该策略的评估;overridden 统计其覆盖行;could_not_verify 是组织在该期间无法核实的检查次数(无法核实的检查不引用任何策略)。coverage 列出每一个已注册的行动类别,包括组织在该类别的检查量,以及该策略在真实评估中记录到的不同章节数。overrides 联接 assistant_policy_overrides(按时间倒序,最多 50 条),每项为 {id, at, actor_email, operation, section_id, reason},仅保留引用了该策略章节的覆盖。
覆盖激增。GET /agent/assistant/policy-metrics/overview(仅负责人或管理员)返回 {overrides_week, overrides_prev_week, spike, scheduled_refusals_week};当本周覆盖达到 3 次或达到上周的两倍时 spike 为 true,而 scheduled_refusals_week 统计本周被政策拒绝的定时运行次数——主体为定时清扫的 block 决策(#3965)。Business 总览的 “At risk and changing” 行读取同一组数字:本周没有定时草稿被拒时该行不显示,绝不会显示为零。
助理权限分级约束所有准入路径(#3812)
每条经过准入检查的路径都会在解析出类别之后、政策评估之前,读取组织保存的助理权限分级(设置 → 助理权限;迁移 0027_assistant_permissions.sql)。三个基础类别一一对应:准备草稿对应 work.prepare,创建任务对应 task.create_followup,外部通信对应固定为“需经允许”的 external.send。agent.team.change 按其工作行解析出的类别处理;记录变更和管理类暂不绑定分级。设为“从不”的动作在所有路径上一律拒绝——真人确认、自动驾驶助理确认以及每条不经过提案的路径——返回 403 permission_never,与助理的权限级别或组织的助理开关无关,任何例外理由都无法解除。设为“需经允许”的动作对定时运行返回 403 permission_needs_person(委托令牌视同真人),对真人则与以往一样照常执行。没有保存过分级的组织读取目录默认分级。若权限表无法读取——数据库未绑定、出错、超时或迁移尚未应用——该操作以 503 permission_unverified 拒绝,绝不放行。分级变化后确认已保存的提案会以 permission_never 拒绝,绝不会是 policy_changed。无人值守的准备取更严格的一项:夜间清扫、定时互动准备以及客户变化触发的 POST /agent/internal/prepare-draft(此时返回 skipped_permission)只在对应分级为 by_itself 且组织的 autonomous_prepared_work 开关开启时才会准备;AI 团队的运行仍会把工作提交待审,但任何人——真人或助理——都无法批准设为“从不”的动作。
例行任务:定时助理任务(#4262)
管理接口。GET /agent/routines 列出组织的例行任务——每项含 id, name, task_kind, config, schedule, enabled, created_by, created_at, next_run_at 以及最近一次运行的结果;POST /agent/routines 以 {name, task_kind, config:{label_set_id, auto_apply}, schedule:{kind:"daily"|"weekly", weekdays?, hour, minute, timezone}, enabled} 创建。task_kind 为 customer_status_update 或 customer_attention(代码内校验,后续类型无需迁移);状态更新类型的 label_set_id 必须是组织自己的公司标签集(未知、私有或受保护的行业集返回 400 invalid_request);timezone 为 IANA 时区。仅所有者/管理员可访问(routines 权限资源):无读取权限者得到 200 {"items":[]},所有写操作返回 403 forbidden。PATCH /agent/routines/:id 接受 name, config, schedule, enabled 任意组合(日程变更会重算 next_run_at);DELETE /agent/routines/:id 删除配置,历史运行及其条目仍可读(不产生孤儿运行记录);POST /agent/routines/:id/run-now 登记一次手动运行,返回 202 与运行中的记录;若该例行任务已有进行中的运行(最近 20 分钟内仍有进展的 running 记录,与过期清理使用同一判断),返回 409 already_running 且不启动新运行,因此连按两次只会启动一次运行。每次登记的运行(手动或排程)都作为一个独立的 Cloudflare Workflow 实例执行一次,运行到结束,不会随请求一起被中止;Workflow 不可用时退回后台执行;无法确认实例是否存在时不执行该运行,运行以 failed 结束、错误为 dispatch_uncertain,请重新运行——若实例已接手该运行,则由它完成并记录自己的结果。无法读取运行以启动时以 setup_read_failed 结束,无法读取此前进度时以 resume_read_failed 结束,执行方无法确认是否仍持有该运行时以 ownership_read_failed 结束,例行任务在运行结束前被删除时以 routine_deleted 结束,Workflow 步骤抛错或超时则以 workflow_step_failed 结束,均以 failed 状态结束,可放心重新运行。仍为 running、20 分钟没有进展且 Workflow 实例已不在运行的运行,由每五分钟的调度以 failed 结束、错误为 stale_run;不重放任何内容(不再调用模型,也不更改标签)。手动运行的模型调用计入按下"立即运行"的人,并计入其个人额度上限。运行中途被打断时,已记录的结论保留;Workflow 步骤不会重试,后续调度不会接管或重放状态不确定的 running 记录(停滞的记录只会按上文以 stale_run 结束);GET /agent/routines/:id/runs?limit=&before= 分页读取历史(最新在前);GET /agent/routine-runs/:id 返回 {run, routine, items}(逐户结论);POST /agent/routine-runs/:id/apply 接受 {"all":true} 或 {"item_ids":[seq, …]} 手动应用建议——无待应用项时返回 409 nothing_to_apply;关注类型运行返回 409 apply_not_supported——应用仅属于状态更新类型。关注类型配置为空,每个需关注的客户经任务服务创建一条署名任务,次日当地早晨到期(见 #4263)。
执行。 清算复用现有五分钟节拍:满足 enabled 且 next_run_at <= now 即到期,每组织每拍最多 ROUTINE_CAP_PER_ORG(默认 2)个例行任务、每次运行最多 ROUTINE_CUSTOMER_CAP(默认 20)个客户——批次按最久未互动优先,上限记录在运行摘要中。每个例行任务每个本地日(按其时区)恰好执行一次定时运行(按日键去重);因错误或预算提前停止的运行记为 partial(整体错误记 failed)并保留已完成条目,同本地日的后续清算不会再启动新运行。逐户评估依据组织已发布的政策指引(与所有准入路径相同的内部 EKB 解析器;命中 block 时记录保持并在理由中引用政策、不调用模型),并以保留的定时主体 agent:routines、用途 routine 发起一次受治理的模型调用,回答保持或改为某个值并附一句理由。组织无客户、标签集为空或已删除时,运行以明确的空转摘要完成而非报错。
应用。Agent 绝不直接写标签:建议通过 CRM 体内签名的内部路由 POST /crm/internal/assistant-label-changes({org_id, routine_id, run_id, mode:auto|manual, applied_by_email, changes:[{seq, customer_id, value_id, reason}]})应用。该路由校验 HMAC 与组织,经 AGENT 绑定读取组织存储的 customer.labels_by_routine 分级:非 by_itself 时自动路径以 403 permission_required 拒绝(分级不可读时以 503 permission_unverified 失败关闭);manual 模式不查分级,因为已由真人决定。每次应用只替换该标签集内的值,带助理署名(assistant:routine:<id>)写入 label_change 客户时间线,并进入「已为你完成」且可撤销;变更 id 确定(labchg:<run>:<seq>),重试会重放而非重复写入。撤销恢复记录的先前值;若已有真人改过该值,则以 409 status_moved_on 拒绝。
计量边界。现有保留组织角色 agent:routines 和 agent:scheduled-sweep 仅可通过绑定请求正文的签名调用 POST /governance/internal/credit/reservations 与 POST /governance/usage。受众、REST 入口、准确操作、空 scope、无查询参数的路径、请求标识与有效期必须匹配;使用记录保留签名中的组织和角色,且必须拥有对应预留记录。这两个内部服务绑定入口不扩大浏览器、委托令牌或其他定时智能体权限。
调用耗时。POST /governance/usage 的请求体可附带可选的 timing 对象 {reserveMs, providerMs}:该次调用在额度预留与模型服务商调用上花费的毫秒数(providerMs 可为 null)。Governance 在响应之后尽力保存,与使用记录同键;缺失或无效的值会被忽略,绝不改变使用记录、计价或结算。
运行耗时。POST /governance/runs 的请求体(智能体的追踪投递)可附带可选的 timing 对象,包含自请求到达起的毫秒偏移(firstModelCallMs、firstFrameMs、firstTextMs、endMs),以及可选的 setupMs:助理本轮并行准备读取所花费的时长(是时长而非偏移)。每项须为 0 到 86,400,000 之间的有限数值,并取整到毫秒;负数、非数值或超出范围的值会被丢弃并存为空,绝不拒绝该运行。省略 setupMs(本轮未执行准备读取)时存为空,且不计入准备耗时分位数;同一运行的后续投递会替换先前的值。Governance 在运行旁尽力保存耗时,耗时写入失败绝不导致运行失败。请求体还可携带 planOrigin,位于运行层级或 timing 内:local 表示助理在本地生成了本轮步骤清单,model 表示由规划模型生成;其他值会被忽略。timing.planOrigin 优先于运行层级字段。它仅在运行携带有效的 timing 对象时、尽力保存在独立的附表中,耗时报告在工作流旁显示为 plan:local 或 plan:model。
提示词前缀(STORY-113)。同一 timing 对象还可附带 promptPrefixDigest(本轮首次模型调用的系统提示词加序列化工具定义的 sha256 值,64 位小写十六进制)、stablePrefixDigest(对提示词稳定前段计算的同类哈希)、toolSubset(预先提供的工具子集,例如 base 或 base+files)以及 toolCount(首次调用携带的工具数,0 到 1,000)。缺少格式正确的 promptPrefixDigest 时其余三项一并忽略;其他无效字段存为空。这些只是摘要值与标签,绝不包含提示词内容;同一运行的后续投递会替换先前的值。
版本化账户状态
GET /crm/communications/connections 以及连接创建或生命周期操作的响应现在为每个连接附加 account 对象:id、kind(shared 或 personal)、owner_user_id、config_version、调用者的实时 allowed_actions、lifecycle(active|paused|disconnected)、setup(无密封凭据为 draft,有为 configured)、已配置的 capabilities(inbound/outbound)、相互独立的 checks.inbound 与 checks.outbound 观察结果,以及有界的 sync 进度。每个检查报告 state(unconfigured|unverified|checking|ready|failed)、checked_at、封闭 cause 令牌及其验证时的 credential_version;收信检查额外包含 observed_count 与 window_days。已验证的空邮箱为收信 ready 且 observed_count: 0;发信失败仅影响发信方向。同步报告 never|running|idle|failed 及 last_success_at、last_attempt_at、条目计数和封闭的 cause 令牌;任何存储的密钥、令牌或不透明游标都不会返回。定时邮箱同步仅在记录在案的授权人身份下运行,且每次运行都会实时复核该人:无人授权该账户时 sync.cause 为 authority_missing,该人已失去实时成员资格或所需邮箱访问权时为(私人邮箱所有者不需要集成管理权限) authority_lost,其余情况为 null。以登录成员身份连接、恢复、重新连接或更换凭据会记录该授权;Atlas 支持人员永远不会成为授权人。GET /crm/communications/connections/:id 返回该连接自己的集合条目——与集合读取完全相同的 publicConnection + account 形状(存在时含支持归因)——且只对这一个连接做实时授权,因此深链账户页无需等待整个组织的授权列表;未知、跨组织或未获授权的 id 与相邻生命周期路由一样回答 404 not_found,集合读取本身保持不变。
GET /crm/integrations 给出每个提供方的可用性与能力,以及 accounts——该渠道上调用者可见账户的分状态计数——和 can_create_shared,即现有共享账号创建路径的当前服务端决定。该布尔值由实时设置权限计算;权限拒绝创建时为 false,不能根据角色、提供方可用性或账户可见性推断。读取者未获授权时,集合仍返回空列表。权限服务不可用时返回 503 authority_unavailable,客户端可重试而不是将其视为权限拒绝。计数对每个连接执行一次实时权限检查,最多四个并行;GET /crm/integrations?accounts=omit 跳过这些检查,每个条目都不含 accounts,准入、可用性、设置文档以及 can_create_shared/can_create_personal 的应答保持不变——Web 应用使用此参数,并自行统计 GET /crm/communications/connections 的结果。该端点不再返回汇总的提供方状态:同一提供方下的多个账户各自保持自身状态,健康账户不会因另一账户断开而被误标。配置变更受版本保护:PATCH /crm/communications/connections/:id 携带 expected_version(必填;缺失返回 400 expected_version_required),执行一次原子标签修改并递增 config_version;版本过期返回 409 version_conflict 并附带当前 config_version,不产生部分修改。凭据替换同时递增凭据版本与 config_version。检查与同步完成不会改变 config_version;在其凭据被替换后才完成的检查会因凭据版本比较交换而被丢弃,不会覆盖较新的结果。兼容性修订:在版本保护启用之前,旧版单账户变更路径(暂停、恢复、断开、重连、检测、凭据替换)仍接受省略 expected_version;提供该值则强制校验;启用后省略将返回 400 expected_version_required。
邮箱设置(#2827)。POST /crm/integrations/email/connect 可接受 account_kind: shared|personal 及非空 directions 数组(含 inbound 和/或 outbound)。具有 CRM 写入权限的有效成员可以连接自己的私人邮箱。所有者由实时成员身份确定,不接受请求指定。新建的自助邮箱和 Gmail 账号无需启用组织级共享账号功能,且仅所有者可访问。历史休眠账号不会自动启用,组织的明确停用措施仍会阻止私人账号访问。收信和发信设置会分别加密到 v2 信封;已保存卡片提示只公开 credential_envelope_version 和已配置方向标记,绝不公开密码。既有 v1 凭据仍可读取,显式替换会升级为 v2。检查仅认证已配置方向;已认证的空收件箱为 ready,且 observed_count: 0。
账号生命周期与指定成员权限(#2828)。 兼容期内,未提供 expected_version 的暂停、恢复和断开请求会读取当前版本并执行同一原子命令;并发修改仍可能返回 409 version_conflict。 设置页面在暂停、恢复和断开操作中提交当前 expected_version,原子保存状态、配置版本和操作人审计事件。恢复需要当前凭据的成功检查,失败的方向仍保持失败。断开会撤销凭据使用并保留历史,且不可恢复。共享账号管理员通过 GET /crm/communications/connections/:id/grants 获取当前有效成员及独立的 can_read_review/can_send 权限;无权读取集合时返回 200 {items:[]}。同一路径的 PUT 接收 {expected_version,user_id,can_read_review,can_send},过期版本返回 409 version_conflict,不产生部分权限或审计变更。保存权限仅为后续指定成员权限上线做准备,不会启用该功能。修复邮箱使用供应商凭据替换流程,不使用接收令牌轮换操作。
使用 Google 登录连接 Gmail
Gmail(#3062)。 GET /crm/integrations 在邮件渠道下列出 gmail 条目:setup.mode: "oauth"、accountKinds: ["shared", "personal"],支持收发两个方向,没有任何字段。当平台 → 功能可见性中的 gmail-connector 开关对调用方关闭时(默认仅开发者可见),该条目不会出现;环境未配置 Google OAuth 客户端时,显示为 availability: "coming-soon"。POST /crm/integrations/gmail/connect 会被拒绝:Gmail 只能通过下面这对 Google 接口连接,所需的实时设置权限与“邮箱”创建路径相同。
POST /crm/communications/connections/google/start 接受 {account_kind: "shared"|"personal", directions: ["inbound"|"outbound", …], label?, reconnect_connection_id?},只返回 {authorize_url},即 Google 的授权页面,申请 openid email https://mail.google.com/ 的离线访问,并带 prompt=consent。拒绝情形:开关关闭时返回 403 gmail_connector_disabled;缺少 GOOGLE_MAIL_CLIENT_ID 或 GOOGLE_MAIL_CLIENT_SECRET 时返回 503 google_not_configured;另有 400 invalid_google_start;调用方没有设置权限,或 reconnect_connection_id 不是本组织的 Gmail 账号时,返回 404。重新连接时,会把已连接的地址作为 Google 的 login_hint 传入。state 为 32 字节随机值,只保存其 SHA-256(comm_oauth_states,迁移 0093_comm_oauth_states.sql),与组织和发起人绑定,十分钟内有效,只能使用一次。
GET /crm/integrations/google/callback 是在 Google 注册的回调地址(https://atlas.forth.ai/crm/integrations/google/callback 及对应的 atlas.staging.forth.ai 地址)。它是浏览器跳转,不是 API:所有结果都以 302 跳转到 /settings/extensions/<id>?google=connected 或 /settings/extensions?google=<outcome>(重新连接时跳转到账号页)。结果包括 cancelled、state_invalid、already_connected(附 account)、wrong_account、email_unverified、check_failed(附封闭取值的 reason)和 failed。跳转地址中绝不包含令牌或授权码。已使用、已过期、已取消,或由其他人、其他组织打开的回调链接,都不会创建任何账号。创建账号之前,Atlas 会用新获得的访问令牌,在固定主机 imap.gmail.com:993 和 smtp.gmail.com:465 上执行与“邮箱”相同的实时检查;检查失败则不留下任何记录。
凭据库记录为 provider: "gmail",只加密保存 {refresh_token, email};连接的 channel: "email",external_account_id 为 Google 地址,其 account 对象额外带有 sign_in: "google"。访问令牌每次使用时临时换取,从不保存,也从不返回。Google 返回 invalid_grant 时,两个方向都变为 failed,原因为 google_access_revoked;同步和发信随即停止,不会自动重试,直到重新连接。重新连接保留连接 ID 和同步位置,并递增凭据版本。Gmail 账号的发信在 CRM 内完成(#2833):Agent 查询发信凭据时只得到 {state: "crm_send", connection_id, credential_version, from_address},随后把已批准的发信交给 POST /internal/integrations/send,由 CRM 用刷新令牌换取一个访问令牌,并以 AUTH XOAUTH2 连接 smtp.gmail.com:465 发信。任何令牌都不会离开 CRM。Google 暂时不可用时,发信以 mailbox_unavailable 拒绝,批准保持有效;Google 返回 invalid_grant 时,发信以 approval_required 拒绝。已连接但被拒绝的 Gmail 账号绝不会回退到 Atlas 发件地址。对 Gmail 账号调用 POST /crm/communications/connections/:id/disconnect 时,会先在 Google 撤销授权(尽力而为,disconnected 动态会记录 gmail:google_revoke_ok 或 gmail:google_revoke_failed),再撤销并销毁加密保存的令牌。
完整转发来源
GET /crm/inbox/items/:id/forward-source 要求收件箱功能门控、来源邮箱的实时 read_review 权限及客户负责人可见性。来源不存在或不可读均返回 404 item_not_found。禁止缓存的私有响应保留附件清单的 source 和 attachments,并添加 quote:from、to、cc、subject、date、body_text 和 body_kind(plain、html_text 或 empty),不包含结构化密送、附件字节或存储位置。
正文和附件来自同一原始 RFC822 版本,保留完整文字,不使用截断的详情正文、不摘要历史。优先完整纯文本替代内容;HTML 转为文字,不执行脚本、不访问外部资源。格式、字符集或转换不支持或不完整时返回 409 source_mime_invalid,超限返回 409 source_limit,来源变化返回 409 source_changed,权威安全规则拒绝时返回 409 source_policy 和 no_draft。正文上限为 1,000,000 UTF-8 字节;HTML 输入上限 4 MiB、20,000 节点、128 层,原始邮件和附件限制保持。此读取仅提供原文证据,草稿确认与实时发送校验仍为后续步骤。
邮箱附件清单
GET /crm/inbox/items/:id/attachment-manifest 使用现有收件箱功能门控、来源邮箱的实时 read_review 权限和客户负责人可见性。不存在与无权限均返回 404 item_not_found。禁止缓存的响应包含 source(item_id、connection_id、原始邮件 content_hash)与 attachments(id、version、解码内容 content_hash、part_id、filename、size_bytes),不返回文件字节或存储位置。
版本 1 表示不可变来源,标识绑定组织、来源账号、邮件、部件与原始内容代次。明确选择附件子集,仅批准其标识、版本和摘要。CRM 验证内容后,在 SMTP DATA 前再次检查来源代次及读取、发送权限。无效、不完整或超限邮件不发送。限制为原始邮件 32 MiB、20 个附件、单个 20 MiB、所选内容合计 20 MiB。服务器接受不代表收件人收到。
打开或下载邮件附件
GET /crm/inbox/items/:id/attachments/:partId 返回收到的邮件或 Atlas 已发送副本中某个附件的内容。:partId 取自邮件读取接口(GET /crm/communications/:id)返回的 attachments 中的 part_id。权限与该读取接口相同:收件箱功能门控、来源邮箱的实时 read_review 权限和客户负责人可见性。其他组织的邮件、调用者无权查看的邮件和不存在的邮件均返回 404 item_not_found;不存在的附件返回 404 attachment_not_found。可读取不超过 32 MiB 的已存邮件;不超过 20 MiB 的附件会被列出并可下载,更大的附件不列出。
默认作为下载返回(Content-Disposition: attachment、application/octet-stream)。?inline=1 请求在浏览器中显示,仅当文件为 PDF、PNG、JPEG、GIF、WebP 或纯文本且内容与该类型一致时生效;HTML、SVG、XML 及其他类型一律下载。每个响应都带有 X-Content-Type-Options: nosniff、含 sandbox 的内容安全策略和 Cache-Control: private, no-store;文件名同时以 ASCII 备用名和 RFC 5987 filename* 发送。
从电脑添加附件
POST /crm/inbox/uploads 为一封待发邮件保存一个文件。请求正文为文件原始字节;Content-Type 为文件类型(不是单纯 type/subtype 格式的一律按 application/octet-stream 保存),X-Atlas-Filename 为百分号编码的文件名。只有可使用收件箱的本组织已登录成员才能上传:其他调用者返回 403 forbidden,收件箱未开启时返回 404 not_found。成功返回 201 和一个 MailUpload {id, version, content_hash, filename, size_bytes, content_type},其中 id 为 u1:<upload id>:<sha256>。任何接口都不会返回上传文件的内容。
以下情况在保存前拒绝:400 upload_empty;声明或实际大小超过 20 MiB 返回 413 upload_too_large;.exe、.js、.bat 等程序文件返回 400 upload_blocked_type;文件名缺失、去除控制字符和斜杠后为空、超过 200 个字符或超过 255 个 UTF-8 字节时返回 400 upload_invalid_name。发送方式:PATCH /agent/assistant/task-drafts/:task_id 接受 uploads,即该草稿完整的 MailUpload[] 列表;POST /agent/assistant/task-drafts/:task_id/send 在 expected.uploads 中确认该列表,不一致时返回 409 draft_changed;POST /agent/assistant/compose/send 接受 attachments: MailUpload[]。上传文件按顺序排在转发的原邮件附件之后。发送时,CRM 只为同一组织且上传者本人解析 u1: 编号,用已保存的字节校验哈希,并在 SMTP DATA 前再次复查。他人或其他组织的上传、哈希变化或文件缺失都会在打开邮箱凭据前拒绝(attachment_mismatch、source_unavailable)。上传文件与转发的原邮件附件共享 20 个文件、20 MiB 的上限(source_limit)。
无客户关联的已发送副本
提供商受理且发送命令最终确认后,已连接邮箱的发送会在 Atlas 已发送中保留完全相同的 MIME,包括已批准的 To/Cc 和附件。没有客户关联时,副本保持未关联,不创建客户联系或时间线记录,也不完成原回复任务。Bcc 不写入邮件头或参与者信息。Message-ID 去重防止重复副本。副本存储为尽力保存:失败可能导致副本缺失,但不会重新发送或改变已受理结果。这是 Atlas 副本,不是提供商文件夹同步。
已批准邮件的收件人传输
候选实现,部署状态须另行验证。 POST /internal/integrations/send 是内部 Service Binding 接口,不是公开发信 API。现有批准绑定一个 To 地址,以及最多各 50 个 Cc 和 Bcc 地址。CRM 向 SMTP 提交每个不同的已批准地址;邮件头和已发送副本的参与人只包含 To/Cc。去重忽略域名大小写,但保留本地部分大小写和加号标签的区别。地址必须是不含显示名称的 ASCII 邮箱地址。
无效地址在访问提供商前返回 refused,原因为 invalid_message。任一 SMTP 收件人被拒绝时,不提交 DATA,本次尝试返回 failed、不含敏感信息的 message_refused 原因及可选数字响应码。只有下列授权清单中的不可变附件可编码发送;没有来源解析器时返回 refused/attachments_unavailable,其他来源错误只返回有限错误码,不会悄悄省略附件。
provider_accepted 仅表示 SMTP 已接受,不代表收件人已收到。既有批准、账号权限、幂等和关联重试规则保持有效。outcome_unknown 不代表发送失败:先查询已记录的尝试,不要盲目重发,也不要用同一键发送改过的内容。继续在工作台恢复草稿。本次传输变更不新增收件箱“回复全部”或“转发”控件。
各环境配置:GOOGLE_MAIL_CLIENT_ID 是 CRM 的环境变量(在负责人创建 OAuth 客户端之前为空);GOOGLE_MAIL_CLIENT_SECRET 通过 wrangler secret put GOOGLE_MAIL_CLIENT_SECRET --env <env> 设置。
从实时权限回填实名授权
#2825 — 组织现有的共享账户可以基于 Auth 自有的成员枚举迁移为显式实名授权,绝不依觖角色名猜测。GET /crm/integrations/grants/migration 报告就绪状态、授权来源计数与是否过期;POST /crm/integrations/grants/migration/dry-run 为只读预览,统计应用将写入的授权与差异;POST /crm/integrations/grants/migration/apply 是面向组织自身集成管理员的有界操作(支持会话会被 Auth 枚举拒绝)。Auth 为每个快照签发内容修订号——对枚举所读的成员与权限状态的摘要——应用在完成时重新校验:迁移过程中的成员或权限变更(409 revision_changed)、或显式授权破坏新旧访问一致性(409 parity_mismatch)都会使组织保持未就绪且不替换任何内容。成功的应用会原子地替换迁移所有的授权、按来源保留管理员的显式修改、记录快照身份、递增共享账户的配置版本,并将组织标记为迁移就绪。对已就绪但未启用的组织,修订号变更会在下一次状态读取时清除就绪,重新应用可安全地刷新迁移授权。已启用的组织拒绝重放,除非存在另行批准的修复。授权不扩大 CRM 记录访问(客户权限仍在使用时相交)、不启用个人账户;回填后被移除的成员由实时权限守卫拒绝,所有权与历史保留。
仅凭邮箱地址识别与销售自助连接(#3596)
POST /crm/integrations/email/resolve 仅凭销售的邮箱地址即可生成连接方案,全程无需询问主机、端口或协议:{email} 依次通过公开 MX 记录、RFC 6186 SRV、内置自动配置表、直至“请我们代为设置”的兜底进行解析,返回 {provider_key, display_name_en, display_name_zh, imap:{host,port,tls}, smtp:{host,port,tls}, auth_hint, guide_key, confidence}。地址格式错误返回 400 invalid_email;若未能解析出任何结果,返回 200 且 confidence:"none",并附带一个永不可拨号的占位主机(RFC 2606 保留的 .invalid),用于触发前端的支持代设置流程,而不是裸露的服务器设置表单。任何已登录的组织成员均可调用此接口——它泄露的信息不超过该域名自身公开 DNS 已经公布的内容,因此刻意置于上方账户授权区之外(不做 manage/inspect_setup 校验)。在返回来自 SRV 的主机之前(这是唯一一条主机值来自 DNS 应答、而非代码声明表的路径),本路由会拒绝私有、环回、链路本地或其他保留范围的 IP 字面量,并静默降级为 confidence:"none" 的结果形态——不会以可区分的错误形式暴露,避免恶意域名借助响应本身探测该防护规则。一个简单的每会话/组织内存限流(每个隔离实例每分钟 20 次)超出窗口后返回 429 rate_limited——这只是对单一会话高频调用的软性上限,而非针对分布式滥用的保证,后者由 Cloudflare 自身的平台级限流承担。
更窄的 create_own 授权允许具有实时 crm:write 权限的有效成员创建自己的全新私人邮箱或 Gmail,无需 integrations 权限或组织级激活。组织明确停用私人账号后,将拒绝此类创建。POST /crm/integrations/email/connect 对全新连接接受 account_kind:"personal";Gmail 使用 Google 发起与回调接口。所有者来自实时成员身份,绝不接受请求中的 owner_user_id。邮箱域名可以与成员登录邮箱或组织域名不同。Auth 已验证为平台所属组织有效成员的平台管理员,遵循相同的私人 Email 规则。跨组织支持身份不会获得自助连接权限。创建共享账号仍需 manage;修复需具名账号的实时 manage 权限,符合条件的私人账号所有者可以持有此权限。此变更不会启用任何历史休眠账号。
符合条件的已保存邮箱会按计划重试,通常每五分钟运行一次;所有者也可选择立即同步,对应 POST /crm/communications/connections/:id/sync。定时和手动导入的每次 IMAP 会话最多允许 120 秒。连接后立即启动的后台导入使用较短的 20 秒会话,每批一封邮件;后续运行从已保存进度继续。这些是服务商会话时限,不保证整个邮箱在两分钟内导入完成。暂停、断开、未验证或无授权的账号须先解决页面提示的问题。保存前被拒绝的账号不会自动重试。
POST /crm/communications/connections/:id/history 接受 {expected_version,history_days},其中 history_days:"all" 或 1 至 90 的整数表示导入范围。此操作仅修改在用 IMAP Email 账号的历史范围,复用已加密保存的凭据,不支持 Gmail。调用者必须是同时具有 manage 和 read_review 权限的有效成员。私人邮箱所有者遵循同一规则,包括已验证的平台所属组织成员;仅具设置权限的支持身份会被拒绝。成功返回 {connection,account,history_days,changed}。缺少版本或历史范围无效返回 400,版本过期返回 409,账号不可用或无权访问返回 404。操作不要求再次输入密码,也不会返回服务商密码。 获授权的单账号读取也返回已保存的 credential_hint.history_days,重新加载账号后仍显示其历史范围选择。返回值仅包含掩码配置提示。
范围变更会推进配置及凭据版本,重新开始收件箱扫描,同时保留已保存的邮件、审阅状态及仍匹配的服务商验证。旧的在途同步无法提交旧进度。重复相同范围返回 changed:false,不会再次重置进度。credential_hint.history_days 是非敏感的设置元数据,值为数字字符串或 all。全部历史表示 INBOX 中所有日期,不扩展到其他文件夹,也不改变其他账号默认的 30 天设置。导入仍分批运行,沿用去重和隐私检查。收件箱页面可见时会随批次刷新;一次批次成功不等于整个邮箱已完成。连接检查中的七天数量只是诊断范围,不是已保存的导入范围。
恢复:回退到无法解析 all 的 CRM 版本前,必须在兼容版本仍运行时,通过此版本受控接口将每个受影响账号改回数字天数。每个账号均须使用有权的成员身份;平台支持身份并非私人邮箱所有者。否则应保留兼容读取能力并向前修复。恢复范围不会删除已经导入的邮件。
每位有效成员均可通过 GET /crm/integrations 发现邮件插件,组织账户模型尚未启用时也不例外。没有 inspect_setup 权限的成员仅获得 email 的静态 IMAP/SMTP 及已启用的 gmail 设置文档:不含 accounts 计数、凭据或账号标识,也不读取账号存储。accounts=omit 返回相同的元数据。can_create_shared 和 can_create_personal 仍反映既有的实时新建权限;可发现插件不授予这些权限。无法设置时,插件提示联系管理员。发现插件本身不会授予邮箱权限;新建的自助私人邮箱或 Gmail 遵循下文仅所有者可访问的规则。共享邮箱管理、私密内容检查及 Google 授权仍受限制。实时授权不可用仍返回 503 authority_unavailable。
内部配对方法链接读取
POST /governance/internal/team-evaluations/method-link/read 是仅供内部 Service Binding 使用、且仅在 staging/development 可用的端点。请求必须由 EVAL_SECRET 按时间、路由和请求体签名,并且只接受 evaluationId、orgId、hireId、manifestDigest 与 inventoryDigest。系统会核验存储行绑定、manifest 验证和 inventory 重新编译结果、受支持虚构场景族的精确种子、当前组织策略摘要以及当前人工管理员权限,才返回 atlas.method-evaluation-link.v1 链接。畸形、过期、跨组织、旧格式、未知或未签名的读取均返回空 403;生产环境不可用(404)。该读取不会创建链接、试验、队列、审批或发布。
冻结链接包含 control/proposed 精确引用、account 或 organisation 目标以及完整运行与执行配置身份,并固定 publicationEligible=false。链接不接受上传的指令:instructionDigest 对精确的学习指令槽文本取摘要(base 为空文本),resolvedSkillDigest 对完整解析技能文本取摘要;运行身份中的聘用配置摘要是另一个值。读取器仅验证 Agent 策略摘要的格式;后续 Agent 回调必须解析已有记录,检查当前 Agent 策略、修订号和两种文本摘要。新增方法结果封装保留试验回调字段,并附加完整不可变的 attemptRecords。共享生成器在成功和失败时均转发可选观察器,包括回退调用;空调用记录不构成计费证明。原有输出 JSON 保持不变。
内部签名方法试验
POST /agent/internal/team-evaluations/trial 接受已签名的方法请求体,包含 method: {link, executionCaseId, trialOrdinal}、经审核的 fixture 及其 worldDigest。其中 link 只包含上述读取器的五个查询字段,Agent 自行读取存储链接。模型调用前,系统核验当前聘用、候选、管理员与授权权限、保留证据、策略、精确学习槽与完整技能及配置摘要、受支持执行配置、标准化场景来源、每个变体的哈希虚构环境和当前 CRM 快照。原有试验请求体继续独立受支持。
受限 Service Binding 响应为 {status: "completed", result, answer} 或 {status: "failed", result, error}。冻结结果不含内容,保留实际评审与草稿调用身份,包括回退和失败。Governance 核对描述符、变体、序号及计费运行后,立即通过原有虚构环境评分器评定真实答案。答案最多为 64,000 个 UTF-8 字节,不写入方法追踪或通用运行器答案存储。持久方法证据只含身份、断言代码和输出摘要,publicationEligible 始终为 false。本适配器不增加准入、队列或发布路径,也不证明真实模型质量。
响应契约由仓库内评分器生成;即使夹具已签名,替换契约也会在模型调用前被拒绝。每次调用(包括提供商回退)都会重新核验存储链接与组织策略、当前权限、冻结学习记录、保留证据及精确技能摘要。切换当前启用技能不会替换已固定的对照记录。记录失效、策略变化或固定内容变化会阻止后续提供商调用,并保留此前已观察到的调用记录。
现有诊断队列按存储的方法描述符及其三个固定序号执行。报告 JSON 保留 methodTrial(描述符、序号、计费运行、实际按用途记录的尝试和输出摘要),以及两个虚构环境各自的 Auth/CRM 清理清单。重复投递不会增加回调;完成写入中断时恢复持久化结算记录,没有结算记录的孤立试验仍为失败,计费状态未知。拒绝不受支持的方法适配器或报告版本。报告访问保留调用方原凭证,并要求当前管理员成员权限、同组织不可变链接和 Agent 当前聘用读取权限;回调标识不会被当作普通 TeamStore 运行。仅 Bearer 的评估报告请求仍不受支持并关闭访问。方法准入与启动已实现,但没有实际审核通过且兼容的用例绑定时会拒绝。随代码提供的已审核方法绑定注册表为空,虚构测试绑定不会启用它。此证据不证明真实模型质量或发布资格。
只有在两个精确虚构环境回读均无残留任务或有效 EKB 来源,且两个数据所有方均返回零残留行时,清理才算成功。报告核验逐项验证冻结描述符,并通过评估、执行用例及序号推导计费运行身份;缺失终态证据、伪造关联、损坏或未知格式的存储链接均拒绝处理。受支持的旧版链接仍可独立读取。
内容密封摘要不证明来源。读取、摘要、比较或重复创建已存储的方法报告时,除当前调用方授权外,还必须使用所属不可变链接和固定准入清单提供的可信核验上下文;忽略调用方提交的仿造上下文。方法上下文缺失或不一致返回 409 integrity_failure;报告结构损坏或密封摘要损坏仍返回 500 integrity_failure。离线方法核验缺少可信上下文时拒绝处理;旧版报告保留原有核验方式。
核验从所属终态评估与试验行重新推导完整的方法报告投影,包括序号覆盖、逐用例记录、尝试、清理及汇总。已放弃的诊断将未完成行保留为未知/不完整,不虚构尝试。系统还独立通过准入记录识别方法归属:所属链接缺失时,当前成员、角色与组织授权通过后返回 409 integrity_failure,因为已无可信聘用标识可供检查。链接有效时,聘用权限撤销仍返回 404。
档案缺口任务提案的关联依据
现有 tasks.profile_gap_task.create 提案新增 data.proposal.evidence.linked_sources:最多 50 个明确关联的文件,按 source_id,link_id 排序。CRM 依据保留在原有独立字段。每个来源结构如下:
{"link_id":"link_1","source_id":"libf_1","state":"current","citations":[{"schemaVersion":1,"reference":{"schemaVersion":1,"kind":"knowledge_source","owner":"ekb","id":"libf_1"},"asset":{"id":"libf_1","title":"Customer notes"},"locator":{"type":"passage","index":0},"quote":"Contact channel reported by the source."}]}
可读取来源为 current;引用保留真实来源 ID、标题、从零开始的段落索引,以及最多 200 个 Unicode 码点的原文前缀。每个文件按索引顺序最多返回三段。[] 仅表示 CRM 无关联。其他状态的 citations 为空:source_unavailable(缺失或外部组织来源,返回无法区分的 404)、inaccessible(无现有知识读取权限或返回 401/403)、no_passage(有效文件无段落)、owner_unavailable(缺少服务绑定、超时、503、损坏或意外响应)。CRM 补充解析的 unknown 会保留关联;独立授权的 EKB 读取成功后仍可提供引用。
提案仍要求 tasks:write;委托引用额外要求同一令牌已有的 ekb:read。缺少知识权限不改变任务资格。来源文字不会改写 CRM 事实或任务元数据。人工明确确认前应核对两组依据;关联 ID、状态或引用内容变化时返回 409 proposal_stale 且不创建任务,应重新提案并复核新依据。依据未变时保留原有幂等确认,拒绝不会创建任务。
使用 POST /assistant/v1/actions/tasks.profile_gap_task.create 提案,使用 POST /assistant/v1/proposals/:id/confirm 确认,沿用 Idempotency-Key 请求头。
可从任意 HTTP 客户端发送相同的请求。Base URL:https://atlas.staging.forth.ai。
| 请求头 | 值 |
|---|---|
Authorization | 每次请求都需携带 Bearer <token>。 |
Content-Type | 每次 POST 都需为 application/json。 |
idempotency-key | propose、confirm、reject 均为必填。重用同一个 key 即可安全重试。 |
curl -sS https://atlas.staging.forth.ai/assistant/v1/actions/crm.customers.search \
-H "Authorization: Bearer $ATLAS_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"q":"Acme 日本 VIP","limit":5}'
curl -sS https://atlas.staging.forth.ai/assistant/v1/actions/crm.customer_profile.create \
-H "Authorization: Bearer $ATLAS_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "idempotency-key: acme-1" \
-d '{"name":"Acme","country":"SG"}'
curl -sS -X POST https://atlas.staging.forth.ai/assistant/v1/proposals/<id>/confirm \
-H "Authorization: Bearer $ATLAS_ACCESS_TOKEN" \
-H "idempotency-key: acme-1-confirm"
请求体使用与 MCP 工具输入相同的字段名:customer_id、q、query、limit、type、source_id、name、contact、email、country、industry、customer_type、profile_gap_code、title、due_at、assignee_email、suggestion_id、page_no、ordinal、purpose、sections、cursor、case_id、source_kind、source_id、expected_revision、remaining_balance、state、due_on、note、year。
响应信封(atlas.action.v1)
{
"schema_version": "atlas.action.v1",
"action": "crm.customer_profile.create",
"status": "awaiting_confirmation",
"request_id": "…",
"data": { "proposal": { "id": "…", "state": "pending", "expires_at": "…", "...": "…" } },
"confirmation": { "id": "…", "summary": "…", "expires_at": "…" },
"meta": { "catalog_version": "…", "principal_kind": "…", "idempotency_key": "acme-1", "repeated": false }
}
status 取值之一:completed、awaiting_confirmation(HTTP 202)、forbidden、rejected、conflict、retryable_failure、terminal_failure。失败时携带 error.code 与 error.message,而非 data。
未保存的方法建议
携带 Idempotency-Key,向 POST /assistant/v1/actions/agent.team.change 提交以下 JSON,再以确认键调用 POST /assistant/v1/proposals/<proposal-id>/confirm。提案阶段不调用模型;确认后进行一次计费的草稿生成尝试。
{"operation":"teaching.suggest","hireId":"amhire_example","revision":1,"teachingSuggestion":{"runId":"amrun_example","excerpt":"The follow-up has no clear question.","expected":"Ask for the next decision date."}}
destination 默认为 account;有权限的教学提案也可选择 organisation。评测和发布仍遵守既有管理员限制。excerpt 可为空字符串,最多 2,000 字符,始终视为不可信数据;expected 必填且非空,最多 2,000 字符。客户事实更正被拒绝,应另用 teaching.create 的 customer_fact 流程。
完成后的 data 包含顶层 runId、hireId 和 suggestion:proposedInstructions(最多 3,000 字符)、rationale(最多 800 字符)、inputDigest、runId、hireId、source.runId。摘要绑定所选输入及当前有权限的账户和组织方法。草稿未保存;用户审阅后,可另行提交 teaching.create 提案。生成过程不会保存、发布或激活候选项。
重放时重新验证当前权限、来源和方法证据。相同确认返回仍保留的结果;方法变化返回 proposal_stale。中断后无法恢复结果时返回 result_unavailable,同一操作不会再次调用网关。生成内容无效时返回 suggestion_invalid 或 suggestion_unavailable。请保留原始输入;明确新建提案代表独立计费尝试。来源过期会清除生成内容。用量归属 teaching_suggestion,必须预留积分,输出上限为 1,024 令牌。
配对教学诊断
沿用 agent.team.change 的提案与确认流程,输入 {"operation":"teaching.compare","hireId":"amhire_example","candidateId":"amlearn_example"}。仅当前人工管理员可启动。staging 与 development 环境由服务器解析当前候选项、来源依据、执行配置、已审核发布包、用例绑定、目标预留和积分上限;调用方不能提交审核或用例身份,当前注册表尚无兼容方法包,会在创建世界或模型调用前如实返回 method_experiment_unavailable。生产环境的教学对比是对课程录制锚点运行的回放:重新读取录制输入(漂移即拒绝),通过无副作用的生成核心分别以有/无课程重跑评审,由固定评判器将控制组与录制输出、提案组与人工撰写的纠正要求比对。回放不发送任何内容、除自身评估记录外不写入任何数据、按组织积分计量并有审计;只有通过的对比才将候选项置为 ready,失败或无法判定都会照旧阻止 teaching.publish。相同操作键返回同一对比。
接纳时会在同一事务内保存已审核清单、完整用例语义与描述符、关联记录和全部试验行,然后才排队。传输中断时,确认返回可重试失败,保留原预留,不记为已完成效果。再次确认同一提案(可用相同或新的确认键)即可恢复原来的待执行行;现有队列与补排流程只允许每次试验执行一次。已完成排队的活动重放不会重新排队或预检,终态活动在正常预留核对释放后仍可返回。每次重试仍校验当前人工管理员及聘用/候选项可见性;注册表或积分配置变化不会替换已冻结的活动。积分不足保留为失败试验,不补做替代试验。使用 POST /governance/eval/campaigns/:id/report 生成报告,再通过 GET /governance/eval/reports/:reportId 读取封存结果。
提案过期后只能恢复已经接纳的同一活动。尚未接纳的过期提案仍会拒绝,且不会创建活动;恢复仍须通过当前权限和原始请求绑定检查。
雇员权限级别(agent.team.change)
每位雇员在 hire.create 与 hire.configure 中携带一个 configuration.permissionLevel,取值为 plan_only、co_pilot 或 auto_pilot。省略该字段则不存储任何值:读回的配置中不会出现该键,键缺失即表示 co_pilot,也就是今天的行为——雇员准备工作,由人决定。请在客户端应用这一默认值,不要期待接口返回该字符串。级别随雇员版本一起保存,因此 agent.team.inspect 的 team 与 detail 视图在 hire.configuration.permissionLevel 返回它,每次更改都是一个新的版本,记录操作者与时间。
面向单个客户的雇员在 hire.create 与 hire.configure 中还可携带一个 configuration.role:售前助理为 presale_assistant,客户成功经理为 customer_success_manager,客户经理则省略该字段。省略即不存储任何值,读回时该键缺失,含义为 account_manager,也就是今天的行为。只能指定面向单个客户的角色:sales_assistant 是客户组合范围自身的角色,会被拒绝;行为尚未上线的目录角色同样会被拒绝。售前助理的运行产出一份带出处的售前简报,而非客户回顾;它绝不自行创建报价单——crm.engagement.quotation-draft.prepare 是由人确认的独立动作。客户成功经理的运行产出一份带出处的售后健康简报(未结清应收款、订单交期与运输进展、已记录的售后反馈),最多产生两项待决策:尚无结算记录的应收款仅生成跟进任务;上一年度合作报告尚未审批时,通过 crm.engagement.drafts.change(purpose: annual_cooperation_report,business_ref_kind: annual_evidence)准备该报告。确认该待决策时,草稿变更以确认人本人的权限执行,留下一份待审阅的草稿;雇员自身绝不执行它,且此角色的任何产出都不会对外发送。
plan_only 会照常完成定时与按需运行,简报、限制、下一步建议和草稿在运行上可读,但不会在「今日」创建任何待决事项。co_pilot 会像以前一样创建待决事项,未经人工确认不会执行任何操作。
自动驾驶执行(agent.team.inspect)
auto_pilot 雇员使用与人工确认完全相同的提案通道,以自己的受托授权确认自己准备好的工作。执行的动作没有变化:仍然是同样的建任务与存草稿,只是由雇员自己决定。每次确认留下一条提案、一条以雇员为执行者的效果回执(携带雇员 id、运行 id 以及供日后撤销使用的逆向效果),工作状态为 approved,decidedBy 为 agent:<hireId>。幂等键为 auto:<runId>:<workId>,因此重试的运行会重放已记录的结果,而不会重复执行。
单次运行最多确认十项。超过上限的部分仍在「今日」等待人工决定,运行会携带 autoPilot,包含 executed、leftPending、cap 以及具名的 failures。暂停雇员会立即生效,正在进行的运行也会停下:该运行以 cancelled 结束并记录 assignment_changed,不再落地任何动作。若雇员的指派授权被收回——或其授权人已不再是持有 tasks:write 的在职成员——该动作以 authority_revoked 失败并记录在运行上,不执行任何内容,也绝不改用其他主体重试。
agent.team.inspect 的 decisions 视图在 items 之外新增 doneByAiTeam 数组:decidedBy 以 agent: 开头的已确认工作,按时间倒序。items 保持不变。
携带 auto_pilot 的变更,除非调用者是处于有效组织会话中的企业管理员,否则以 403 admin_required 拒绝且不写入任何版本;监督自己销售助理的销售、委托令牌与定时代理主体均被拒绝。提案的证据携带 permissionLevel、来自角色目录的该角色 allowedActionClasses,以及 excludedActionClasses(external_send、binding_quotation,任何角色都不允许)。确认前请在 GET /assistant/v1/proposals/:id 读取它们;若提案之后允许集合发生变化,确认返回 proposal_stale。
询价移交(agent.team.change)
{"operation":"run.handoff","hireId":"amhire_presale_example","revision":<雇用版本>} 把在职售前助理的需求矩阵移交给同一客户上在职的客户经理。像其他 agent.team.change 一样提案并确认;两位雇员的主管或企业管理员可以确认。确认后创建两条触发方式为 handoff 的运行——售前助理一条、客户经理一条——并为这一对运行启动一个工作流。售前运行与其他运行一样记录带出处的简报,自身不发布任何工作。客户经理运行在唯一一次受治理的模型调用(用途 rfq_handoff)之前,把售前运行的 id 与来源摘要记录为 handoff 证据;随后按矩阵顺序为前三个缺口各发布一条澄清任务 Clarify: <需求>,并发布一封跟进邮件草稿,均注明对应的需求。其余缺口以 Not asked: <requirementId> 记在运行的限制中。所有内容都等待批准、编辑或忽略;不会发送任何内容。
拒绝情形:雇员不是在职售前助理、该客户没有在职客户经理、调用者不能在两位雇员上启动运行、任一雇员的委托授权无法解析,或该雇员仍有排队或运行中的移交时,返回 409 handoff_unavailable。售前运行失败或未找到任何需求时,客户经理运行以 handoff_source_failed 结束为 failed;客户经理步骤的授权被拒绝时,以相应原因结束为 failed。两种情况都不发布任何内容。以相同幂等键再次确认会返回同一对运行,不会启动新的运行。
售前雇员的详情视图(agent_team_inspect view:"detail")返回 handoffAvailable;为 false 时同时返回 handoffUnavailableReason,取值为 no_account_manager, plan_only_account_manager, handoff_in_flight, no_access, unavailable 之一。只要一对运行中任一条仍在排队或运行、且其工作流尚未结束,移交即视为进行中;某一步骤耗尽重试后工作流结束时,未完成的运行以 failed 结束(handoff_abandoned;售前运行未完成时,客户经理运行为 handoff_source_failed),之后可以发起新的移交。以不同幂等键同时确认两次只会创建一对运行,另一次返回 409 handoff_unavailable。工作流无法启动时,确认返回 503 handoff_unavailable,这对运行以 handoff_abandoned 结束,可直接重试;详情读取无法判断时显示 unavailable。由移交产生的客户经理运行输出和工作,只对当前同时能看到该售前雇员的成员显示并允许审批,且仅在售前运行仍是其记录的、已保留且已完成的那次运行时有效;其他人看到的是 evidence_unavailable,与证据被撤回时相同。超出前三个已提问缺口的其余缺口都会在限制说明中逐一列出;放不下时,剩余的缺口合并在最后一条 Not asked: <id>, <id> 中列出。
撤销自动驾驶动作(agent.team.change)
{"operation":"work.undo","hireId":"amhire_example","workId":"amwork_example","revision":<工作版本>} 会执行雇员当初执行该动作时回执所记录的逆操作:雇员创建的任务通过任务服务取消,雇员保存的草稿作废。除此以外不还原任何内容,也绝不覆盖他人更新后的状态。雇员不必仍处于 active。提案与确认与其他 agent.team.change 完全一致;提案证据固定工作版本、雇员版本、效果标识以及所记录逆操作的摘要,期间任何变化都会返回 proposal_stale。
只生效一次。重复确认同一提案返回首次结果;对同一动作再发起一次独立撤销将以 already_undone 拒绝,不会重复还原。回执未记录逆操作的动作以 no_inverse_effect 拒绝;由人确认的动作以 not_auto_executed 拒绝;所创建的任务已不再处于进行中(已完成、已取消或已不存在)则以 world_changed 拒绝,任务保持原样,该动作仍记录为已执行。任何拒绝都不改变任何内容。
所需权限与当初确认该动作相同,且必须是实时有效的:crm:write、对该雇员的可见性,任务类动作还需要 tasks:write(缺少时返回 403 forbidden)。雇员自身的定时代理授权被直接拒绝执行 work.undo,任何服务主体同样被拒绝:撤销始终记录为确认它的那个人,绝不记为雇员。撤销后,detail 视图与 doneByAiTeam 中该工作会新增 undo 字段 { "at": <毫秒时间戳>, "by": "<操作人>" };从未被撤销的工作不携带该字段。在 detail 视图中,该工作的实时 status 会显示为 undone,同时该行保留其 effectId、原有决策记录与 undo 字段;doneByAiTeam 决策历史保留该动作原有的决策记录。
撤回学习指导(skill.withdraw)
{"operation":"skill.withdraw","hireId":"amhire_example","revision":<雇员版本>,"scope":"account","fromVersionId":"amskill_example","headRevision":<版本头修订号>} 会把指定的客户或整个企业技能版本头移回基础版本,将被撤回的版本标记为已失效,并记录一条 skill_withdrawn 事件。该操作不指定目标版本。与 skill.rollback 相同,它仅限管理员(否则返回 403 admin_required),且必须携带管理员读取到的版本头修订号:若版本头已被并发的发布、回退或撤回更改,则返回 409 skill_head_changed,且不做任何改动。基础版本不能被撤回(409 invalid_withdraw_target)。
此后的运行、草稿和对话只使用基础技能,运行身份记录为基础版本。既往的运行、事件和版本记录保持原样可读。已撤回的版本不再能作为回退目标;恢复只能通过不变的 teaching.create → teaching.evaluate → teaching.publish 流程发布替代版本。同一载荷可提交到 POST /assistant/v1/actions/agent.team.change 或 agent_team_change_propose;并像其他 AI 团队变更一样确认提案。
客户经理运行用量(agent.team.inspect)
既有治理服务 GET /usage 列表为每条可见事件返回规范化的 purpose。历史空值或不支持的值显示为 unknown,不会改写存储事件,也不改变令牌总计、价格、积分或幂等性。组织和操作者可见性规则保持不变。
POST /assistant/v1/actions/agent.team.inspect 携带 {"view":"detail","runId":"<run-id>"},通过既有 crm:read 范围读取当前有权访问的运行详情。两个选择参数只能选一个:runId 查看一次运行,hireId 查看雇员详情;同时传入两者返回 400 invalid_input。其他组织的运行或当前权限丢失均返回既有拒绝,不披露用量或来源内容。
data.usage.byPurpose 将既有记录按 account_review、follow_up_draft、teaching_suggestion、method_optimization 或历史 unknown 分组。每项包含 attempts、coverage(提供商令牌、已定价金额、积分与未结算尝试的计数)、令牌总计、providerCostNanos 与 costCurrency,以及独立的 customerCredits。缓存和推理令牌始终是子集,不会再次加入 totalTokens。
data.usage.pendingAttempts 统计未结算尝试。运行级 data.usage.incomplete 和每项的 incomplete 标记缺失或不兼容计量;不可用总计保持 null。空投影(包括仅有未结算尝试)返回运行级 incomplete: true。存在未结算尝试时总计不可用。币种混合或缺失时,提供商金额不可用,独立计量的令牌与积分仍分别呈现。治理服务不可读时,available: false 与 incomplete: true 表示覆盖度未知,不会虚构未结算计数。
所有 AI 团队角色共用的专员循环及其对比(STORY-DRAFT-AT-02、STORY-DRAFT-AT-03)
当某组织的 team-specialist-loop 功能开关开启时,任何目录角色——客户经理、销售助理、售前助理、客户成功经理、业务分析师——的定时运行、立即运行和入站邮件运行都在共享的专员循环上执行:最多六个受治理的模型步骤,每一步都对雇员预算单独预留积分,并在 data.usage.byPurpose 中以 specialist_turn 用途记录。每个角色通过自己有限的工具清单读取:客户范围角色读取该客户已授权的资料和分配的知识(售前助理另有需求矩阵,客户成功经理另有互动上下文),销售助理读取其销售的客户组合,业务分析师读取组织的分析快照——组织角色不可触及任何客户工具,组合角色不可触及组织工具。每次运行都以该角色现有的存储结构(简报、下一步、草稿)结束,均待审批。预览、评估试运行和询价移交对每个角色都保持原有路径;开关关闭或无法读取时,所有运行都保持原路径。运行详情中的 identity.tools 按顺序列出循环执行的步骤,格式为 [{"name","at"}](仅名称);由其他路径执行的运行没有该字段。agent.team.inspect 与 agent.team.change 的载荷除此之外保持不变。
POST /agent/team/specialist-comparison(可选 {"hireIds":["<雇员 id>"]} 或 {"role":"<角色 id>"})是仅限预发布与开发环境的运维工具(生产环境返回 404),调用者须为拥有 crm:write 的组织管理员,其他人返回 admin_required。每次请求只对比任意目录角色的一个雇员,因为它要运行两种方式,可能需要数分钟:即指定的雇员;两个字段都省略时为最近更新的在职客户经理雇员;仅给出 role 时为该角色最近更新的在职雇员(未知角色、两个字段同时给出或传入多个 id 都返回 400 invalid_input)。它通过只读传输捕获该雇员的证据,并在同一证据上分别运行该角色自己的流水线(客户经理与销售助理为生成调用;售前助理、客户成功经理和业务分析师为确定性组装器)和专员循环;不会记录到任何运行,也不会发布任何内容,但两种方式都会消耗受治理的积分。响应包含 cases(每个雇员带其 role:pipeline 与 loop 的 status、简报条目、下一步与草稿的数量、references、按该角色自身证据校验的 citationsValid,以及每个分支的 durationMs 与 credits——积分取自受治理的使用账本,确定性组装器为 0,账本未能及时应答时为 null——和循环的 steps 与 stop)、每种方式的 summary(parseSuccess、citationValidity)、approvedShare(取自本组织最近 30 天该角色工作的主管真实决定,按产生运行的方式区分,不计自动驾驶的批准)、verdict(每项为 no_worse、worse 或 inconclusive,以及 ship)和 gaps。
两个分支使用同一份已捕获的证据、技能指示和对比时点。每次模型或工具读取前,以及返回报告前,系统都会重新检查当前管理员、雇员授权和源数据访问权限。证据、角色设置或已解析技能变化时,该用例以 comparison_evidence_changed 失败;权限撤销和客户删除保留各自的拒绝代码。生成分支失败后仍报告账本已结算的 credits;尚未结算、账本不可用或成本未知(包括用量投影为空或缺失)时为 null,不会假定为零。明确计量为零时仍为 0;令牌或币种指标不完整不会抹去已独立计量的积分。两个分支开始前的源数据准入失败为 0。任一分支失败时,即使引用有效率数值相等,引用判定也为 inconclusive,报告为 ship: false。重放不会记录主管决定。
组织角色启用(role.configure)
组织可自行决定启用哪些 AI 团队角色。POST /assistant/v1/actions/agent.team.inspect 携带 {"view":"roles"} 返回 data.items:每个目录角色一项,包含 roleId、enabled、revision、updatedBy 与 updatedAt。从未修改过的角色读作已开启、修订号为 0。仅限管理员;其他人以 admin_required 拒绝,读取始终限于调用者自己的组织。
{"operation":"role.configure","roleAvailability":{"roleId":"sales_assistant","enabled":false,"revision":0}}
携带 Idempotency-Key 提交到 POST /assistant/v1/actions/agent.team.change,并像其他 AI 团队变更一样确认提案。role.configure 仅接受 roleAvailability,不带雇员主体。revision 必须是管理员看到的存储修订号;提案或确认携带其他修订号时以 stale_revision 拒绝且不做任何更改,因此两位管理员不会互相覆盖。完成后的 data.roleSetting 为新修订号下的新行。
角色关闭期间,对该角色提出的 hire.create、hire.configure 与 role.enable 在提案和确认阶段均以 409 role_disabled 拒绝——不写入雇员、雇员修订、运行、提案或回执。该角色已有雇员保留其状态、修订号与配置,但不再启动排程运行;巡检记录一条带 role_disabled 的 schedule_stopped 事件,团队与详情视图中的此类雇员均带有 roleDisabled: true。重新开启后,原有排程在下一次巡检恢复,无需重新配置。
AI 团队运行健康度(agent.team.inspect)
向 POST /assistant/v1/actions/agent.team.inspect 发送 {"view":"team"},在保持不变的 data.items 之外,新增返回 data.runHealth:调用方可见的雇员在最近七天的运行情况。其中包含 since(窗口起点,epoch 毫秒)、runs(窗口内创建的全部计入运行,无论是否已结束)、terminalRuns(其中已得出结果的运行——completed、unchanged 或 failed)、degraded(带有 reason: "output_degraded" 的已完成运行——该运行保留了已收集的证据,但未生成评述)、failed,以及两个占比 degradedRate 与 failedRate(窗口内没有运行结束时为 null)、hiresCounted 与 hiresTruncated,还有 byHire:每个产生过降级或失败运行的雇员一条 {hireId, degraded, failed},问题最多者在前。运行健康的组织返回空的 byHire。degradedRate 与 failedRate 是相对于已结束运行 terminalRuns 的占比,而非相对于 runs。预览运行与评测运行属于试运行,均不计入;被治理清理过的运行同样不计入。hiresCounted 表示实际统计的雇员数,当组织可能还有更多雇员时 hiresTruncated 为真。同样的结构按指标周期随视图 metrics 以 runHealth 返回。
降级运行仍然算完成、仍然保留证据、也仍然推进 lastRunAt,因此任何统计失败的地方都不会统计到它;这个汇总就是那个计数。它严格限定在调用方所属组织,并且只覆盖该调用方本来就能看到的雇员——主管看到自己的雇员,管理员看到本组织的雇员——不会暴露团队列表本来不会返回的雇员。它只是汇总,不是告警:不会由此发出任何通知。
客户 AI 团队视图(agent.team.inspect)
向 POST /assistant/v1/actions/agent.team.inspect 发送 {"view":"customer","customerId":"<customer-id>"},返回 data.items:调用方可见的、部署在该客户上的未退休雇员,结构与 team 视图完全一致,每条包含 role、status、configuration.permissionLevel、configuration.supervisorEmail、lastRunAt 与 configuration.schedule。角色被组织关闭的雇员在这里同样带有 roleDisabled: true。
同一个账户可以部署整支 AI 团队,但每个角色最多只能有一名未退休雇员。若本组织下该客户的某个角色已有未退休雇员,再次提交 {"operation":"hire.create"} 会被拒绝并返回 409 role_already_assigned;信封 error 中带有 existing_hire_id 与 existing_role,指明已担任该角色的雇员,并且不写入任何雇员、雇员修订、角色行或唯一性行。同一组织、同一客户、同一角色的两个并发创建最终只保留一个雇员,另一个以同样方式被拒绝。同一账户上的其他角色是彼此独立的雇用;退休现有雇员后,该角色即可重新部署。MCP 与 CLI 返回完全相同的拒绝、错误码与字段。
可见性沿用团队列表的规则,只会收窄,不会放宽:管理员读到本组织在该客户上的雇员,主管读到自己的。看不到任何雇员的调用方、其他组织的客户、以及没有雇员的账户,三者一律返回 200 {"items":[]},因此该接口不会泄露客户或雇员是否存在。缺失或为空的 customerId 会被拒绝并返回 400 invalid_input;跨组织会话与其他视图一样被 authority_revoked 拒绝。该视图只读取已存储的雇员,客户名称一律来自 CRM,不由本响应提供。
业务分析师雇用(组织范围)
业务分析师面向整个组织雇用一名,而不是面向某个客户或某位销售。其雇用配置携带 "assignmentScope":{"kind":"organisation","roleId":"business_analyst"} 与保留的 customerId organisation:business_analyst,两者必须一致;knowledge 必须为空;permissionLevel 仅可为 plan_only 或 co_pilot——v1 对该角色拒绝 auto_pilot。
{"operation":"hire.create","configuration":{"customerId":"organisation:business_analyst","assignmentScope":{"kind":"organisation","roleId":"business_analyst"},"supervisorEmail":"owner@example.com","name":"Business analyst","goals":["Explain what changed each week"],"knowledge":[],"skills":["account_review"],"instructions":"","locale":"en","schedule":{"timezone":"Asia/Shanghai","time":"07:30","weekdays":[1]},"budget":{"monthlyCredits":100,"perRunCredits":10},"permissionLevel":"co_pilot"}}
仅限管理员(admin_required)。在现有雇员尚未退役时再次雇用,会在提案与确认阶段均以 role_already_hired 拒绝,因此两位管理员同时雇用最终只会得到一名分析师,且不写入其他内容。每次计划运行通过一次组织范围的签名读取获取分析聚合数据——/analytics/kpis、/analytics/customer-activity、/analytics/pipeline、/analytics/top-opportunities 与 /analytics/needs-attention——由 CRM 限定在雇用它的组织以及主管本人可见的范围内,并按组织复盘周期已结束的周窗口或月窗口撰写分析。分析中的每个数字都标注其来源端点。pipeline-analysis、customer-analysis、sales-performance、sales-forecast 这四个报表键只会记入运行的 limitations,不会被读取:未对该组织发布的会标注为尚未发布,已发布的则标注为暂无分析师可读取的数据。每条建议都会指明其针对的客户,并以普通任务待办的形式进入既有的提议—确认流程;plan_only 的分析师不会提交任何待办。该角色以目录版本 1.1.0 发布:若环境此前已种入 1.0.0,需由超级管理员在「平台 → 角色」中激活 1.1.0,雇用向导才会展示业务分析师。
MCP 服务器连接(系统)
#2746 —— GET /crm/integrations 现在还返回 mcp_server 提供方,其条目带有 "kind":"system",供「设置」将「系统」与「渠道」分区展示;所有渠道条目均不带 kind。POST /crm/integrations/mcp_server/connect 的 fields 为 server_name、server_url、auth_header 与 auth_secret。其中只有 auth_secret 是密钥:整个目标以 COMM_CREDENTIAL_KEY 加密后存入组织的凭据保险库,绝不返回;响应仅带已有的掩码 credential_hint。所建连接的 channel 为 mcp,external_account_id 为地址,label 为显示名称。COMM_CHANNELS 未变更,因此 POST /crm/communications/connections 仍以 invalid_channel 拒绝 mcp,MCP 连接永远不会接收消息投递。
地址校验即整个 SSRF 边界,且在任何写入之前完成:凡不是公网 DNS 主机上的 https: 地址——明文 URL、任何形式的 IP 字面量、localhost、回环、链路本地或私有主机、.local/.internal/.home.arpa 名称、地址内嵌凭据,或非 443 端口——一律以 400 unsafe_server_url 拒绝,不创建连接、凭据或工具快照。保留或格式非法的认证请求头名称同样以 400 invalid_fields 拒绝。
POST /crm/communications/connections/:id/test 执行 MCP 握手(initialize、notifications/initialized、tools/list),通过后替换该连接的工具快照并递增 revision。服务器的响应一律视为不可信数据:超出字节上限的响应或超出数量上限的工具列表整体拒绝且不落库,工具名称与说明按有界纯文本保存。任何失败都收敛为 check.detail 的 mcp:<cause> 形式,最多附一个数字 HTTP 状态码,cause 取值为 unreachable、timeout、protocol_error、credential_rejected、response_too_large 或 tool_limit_exceeded。详情令牌、审计记录与日志中都不会出现服务器原文、地址或密钥;失败的检查保留已存凭据与此前的快照,因此更正后重试即可恢复,无需重新填写。
GET /crm/communications/connections/:id/tools 返回 items({name, description})以及 revision、tool_count 与 captured_at。该读取需要 integrations:read,并沿用同组 /events 读取的集合约定:未知 id、其他组织的 id、以及调用者无权查看的连接,一律返回 200 {"items": []},绝不确认某个 id 是否存在。组织始终取自已验证会话;平台超级管理员只能经由已审计的协助接入路径访问其他组织。每个条目还会带上管理员的分类信息,详见下一节。
POST /crm/communications/connections/:id/disconnect 即吊销:在同一请求内作废已存凭据并删除工具快照,同时记录 disconnected 审计事件。吊销是终态——之后的检查无法访问该服务器,之后的任何工具调用均以 connection_revoked 拒绝。需要迁移 0087_integration_tool_snapshots.sql。
把外部工具纳入受治理的操作目录(#2747)
发现到的工具在管理员分类之前不可调用。GET /crm/communications/connections/:id/tools 现在为每个工具返回 kind(read、effect,未分类时为 null)、enabled_roles、revision 与 state(unclassified、classified 或 drifted)。POST /crm/communications/connections/:id/tools/:toolName/classification 需要 integrations:write,请求体为 {"kind":"read"|"effect","enabled_roles":[…]};非法取值返回 400 invalid_kind / 400 invalid_roles,该连接从未上报过的工具名以及其他组织的连接一律返回 404。每次写入都会按当前工具重新固定摘要——涵盖名称、说明与入参 schema——并递增 revision。重新发现时三者中任意一项发生变化,该工具即读作 drifted,已发布的操作被撤下,调用返回 409 tool_reclassification_required,直到管理员重新分类。
完成分类并且至少启用了一个角色的工具,才会以 external.<connectionId>.<toolName> 发布给该组织的调用方,出现在 GET /assistant/v1/catalog,并通过 POST /assistant/v1/actions/external.<connectionId>.<toolName> 调用,请求体为 {"arguments":{…}}。read 需要 crm:read,直接执行并返回 {connection_id, tool, result_text, result_bytes, is_error}。effect 需要 crm:write 与 Idempotency-Key,调用时绝不联系远端:返回 202 awaiting_confirmation 及一条列明连接、工具与参数值的待确认事项,只有 POST /assistant/v1/proposals/:id/confirm 才会真正调用一次。外部操作永远不适用自动确认。enabled_roles 决定是否发布:分类时角色列表为空的工具在任何地方都不发布——既不出现在目录里,也不出现在助手的工具列表里——其 id 与该组织没有的任何操作一样返回 404 action_not_found。列表非空时,工具按其类型所需的权限发布给该组织的调用方;将单个调用方的角色与该列表逐一比对属于雇员使用环节,此处尚未强制。待确认事项本身的 action 即外部操作 id,required_scope 为 crm:write,参数值位于 intended_external_call 的 {connection_id, tool_name, arguments} 中。
各类拒绝都按名称给出,且不会触达远端。工具的远端定义在分类之后发生变化时,该操作从目录中撤下,直接调用返回 409 tool_reclassification_required 并指明工具名,直到管理员重新分类。连接被暂停、断开或吊销时返回 409 connection_unavailable,绝不改用其他连接。调用 read 时会一并声明「这是一次读取」,网关在真正拨出请求之前重新读取当下的分类:若该工具在目录读取之后已被组织改判为「变更」,则返回 409 external_effect_requires_human——对外的变更始终要由人确认,绝不会按调用方早先的读数直接执行。同一次重读也会核对角色启用情况,对应的拒绝是 role_not_enabled;但它只用于代表 AI 团队成员发起的调用,因为只有那类调用会声明角色。REST 与 MCP 调用方不声明角色,因此在这些接口上不会遇到 role_not_enabled。这两种拒绝都在打开保险库中的凭据之前作出,因此不会触达任何远端系统,也不会改用其他连接。属于其他组织的操作 id 一律返回 404 action_not_found。远端产生的一切字符串都视为数据:不符合 [a-z0-9_-]{1,64} 的工具名永不发布,说明与结果一律去控制字符并限长,结果只在不可信内容信封内到达模型,任何远端文本都不会进入系统提示词或授权判定。每次调用都会留痕:连接、工具、参数的键名(绝不含取值)与返回字节数;凭据始终留在保险库中,不出现在任何响应与日志里。MCP 与 CLI 通过同一套已发布接口访问同样的已分类工具(#2774);读取工具与「提议/确认/拒绝」三件套在 MCP 中的呈现方式,见 MCP 手册。需要迁移 0088_integration_tool_actions.sql。
雇员运行期间使用外部工具
雇员自己的运行——客户经理、销售助理、售前助理、客户成功经理与业务分析师概莫能外——与直接调用 external.<connectionId>.<toolName> 的 REST 或 MCP 客户端是不同的调用方:它会声明自己的 role 与所属雇员的 permissionLevel,因此 enabled_roles 与权限级别都会限制它能使用什么,运行结果会明确说明未能使用的部分,而不是悄悄少做。v1 版本中,雇员在一次限定的模型交互内只能使用本组织已分类的 read 工具,次数上限为 maxExternalReadCalls——这是整次运行共用的单一上限,并非某个角色自带的数值;已分类的 effect 工具会照常参与解析,但在任何权限级别下——包括 auto_pilot——都不会被运行直接调用:auto-pilot 只确认 Atlas 自己已准备好的工作,绝不代为对已连接系统作出变更。
plan_only 的雇员完全不调用任何外部工具,无论读取还是变更:它只做规划,不向已连接系统发出任何请求。co_pilot 或 auto_pilot 的雇员可以直接调用其角色已启用的 read 工具,无需确认,与读取其他受治理数据源相同。一次运行中被排除的一切——角色未启用的工具、无论权限级别都被搁置的变更工具、暂停/吊销/重新分类的连接,以及两个会撞名的工具——都会在运行的 limitations 中按原因归纳成一句话汇总,而不是逐条列出:依次是角色排除、被搁置的变更、不可用的连接、撞名冲突,与该运行的其他限制一起最多合计八行。限制说明中绝不出现任何远端提供的工具名或描述;已解析与被排除的操作 id 单独记录在运行的内部审计追踪步骤 agent_team.external_tools 中,供审计使用,任何读取操作都不会返回它,客户可见的运行页面上也不会出现。若组织的已分类工具目录本身读取失败,运行会用一句话说明并不使用任何外部工具,而不是凭空假设。
当前已发布版本(ekb.releases.*)
这些只读操作原样返回知识归属方的当前政策与流程版本。ekb.releases.list 接受可选的 kind(policy 或 workflow)、q(2–100 个字符)、limit(1–50,默认 20)及返回的不透明 cursor;ekb.releases.read 接受一个 release_id。引用、稳定章节或步骤定位、内容、answerable 与分页保持不变;仅供参考的版本仍为 answerable=false。
curl -sS https://atlas.staging.forth.ai/assistant/v1/actions/ekb.releases.list \
-H "Authorization: Bearer $ATLAS_ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"kind":"policy","limit":20}'
curl -sS https://atlas.staging.forth.ai/assistant/v1/actions/ekb.releases.list \
-H "Authorization: Bearer $ATLAS_ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"q":"报价审批","limit":1}'
curl -sS https://atlas.staging.forth.ai/assistant/v1/actions/ekb.releases.list \
-H "Authorization: Bearer $ATLAS_ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"q":"报价审批","limit":1,"cursor":"eyJxIjoi5oql5Lu35a6h5om5Iiwia2luZCI6bnVsbCwidGllciI6MSwicHVibGlzaGVkX2F0IjoiMjAyNi0wOS0wOFQwMDowMDowMC4wMDBaIiwiaWQiOiJtcmVsXzAxIn0"}'
curl -sS https://atlas.staging.forth.ai/assistant/v1/actions/ekb.releases.read \
-H "Authorization: Bearer $ATLAS_ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"release_id":"mrel_…"}'
命中结果保留有类型的定位、原文、内容与不透明的下一页游标。以下精简响应展示其形状;成功的单条读取在 data.item 下返回同一完整负载:
{"status":"completed","data":{"items":[{
"citation":{"reference":{"kind":"policy","owner":"ekb","id":"mrel_01","version":"3"},"asset":{"id":"mpol_01","title":"报价制度"},"locator":{"type":"section","stable_id":"psec_0123456789abcdef0123456789abcdef","label":"审批"},"quote":"报价须经过审批"},
"release":{"id":"mrel_01","release_no":3,"status":"released","answerable":false,"asset_kind":"policy","asset_id":"mpol_01","asset_title":"报价制度","content":{"title":"报价制度","category":"销售","sections":[{"heading":"审批","body":"报价须经过审批"}]},"...":"归属方字段保持不变"}
}],"next_cursor":"eyJxIjoi5oql5Lu35a6h5om5Iiwia2luZCI6bnVsbCwidGllciI6MSwicHVibGlzaGVkX2F0IjoiMjAyNi0wOS0wOFQwMDowMDowMC4wMDBaIiwiaWQiOiJtcmVsXzAxIn0"}}
没有匹配项时返回已完成的 data: {"items":[],"next_cursor":null}。未知、外部组织、已撤回或已被取代的 id 返回 terminal_failure/not_found。无效输入返回 rejected/invalid_input。失败响应不包含版本数据:
{"status":"forbidden","error":{"code":"insufficient_scope","retryable":false}}
HTTP 403 {"status":"forbidden","error":{"code":"organisation_suspended","retryable":false}}
HTTP 503 {"status":"retryable_failure","error":{"code":"credential_store_unavailable","retryable":true}}
{"status":"retryable_failure","error":{"code":"credential_store_unavailable","retryable":true}}
{"status":"retryable_failure","error":{"code":"upstream_unavailable","retryable":true}}
入口准入和下游失败均使用操作信封:组织停用返回 HTTP 403,权限存储或上游服务中断返回 HTTP 503。每次调用都会检查令牌当前成员关系、组织状态、角色以及与 ekb:read 的交集;知识管理权限不会增加读取门槛。
来源用途批准(ekb.source-use.*)
确认通过 REST 或 MCP 的操作绑定 EKB POST 断言完成;它不开放通用 EKB 写入路由,不把 bearer 令牌转发给 EKB,并保留当前知识权限与 ekb:write 范围检查。
解析读取回答:某条知识内容——来源当前版本内的一个段落定位——对某个客户是否已确认且允许外发。它返回批准引用、原因代码,以及一个会随依据变化(版本替换、退役、撤回、过期)而改变的 basis_digest。仅被索引而没有批准时回答“不允许”;被索引不等于已获准外发。
curl -sS https://atlas.staging.forth.ai/assistant/v1/actions/ekb.source-use.read \
-H "Authorization: Bearer $ATLAS_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"source_id":"ks_…","page_no":1,"ordinal":2,"customer_id":"cus_…"}'
curl -sS https://atlas.staging.forth.ai/assistant/v1/actions/ekb.source-use.change \
-H "Authorization: Bearer $ATLAS_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "idempotency-key: approve-claim-x-1" \
-d '{"operation":"approve","source_id":"ks_…","page_no":1,"ordinal":2,"audience":"customer_specific","customer_id":"cus_…"}'
产品证据选择(ekb.product-evidence-selection.*)
已命名的产品主张选择 —— 面向客户工作可复用、可检查的依据(#2117)。读取对全体成员开放并按组织隔离;写入需要经理或管理员权限及 knowledge 写级别。每条目实时解析:资格、摘录与客户使用许可来自当前来源版本和 ekb.source-use 审批。
# 列出本组织的选择
curl -sS https://atlas.staging.forth.ai/assistant/v1/actions/ekb.product-evidence-selection.read \
-H "authorization: Bearer $ATLAS_TOKEN" -H "content-type: application/json" \
-d '{}'
# 读取单个选择(可选按客户解析外发许可)
curl -sS https://atlas.staging.forth.ai/assistant/v1/actions/ekb.product-evidence-selection.read \
-H "authorization: Bearer $ATLAS_TOKEN" -H "content-type: application/json" \
-d '{"selection_id":"psel_1","customer_id":"cus_1"}'
# 提议保存一个选择(create / update / delete 同一提议-确认流程)
curl -sS https://atlas.staging.forth.ai/assistant/v1/actions/ekb.product-evidence-selection.create \
-H "authorization: Bearer $ATLAS_TOKEN" -H "content-type: application/json" \
-H "idempotency-key: sel-key-0001" \
-d '{"name":"Q3 泵系列","entries":[{"source_id":"ks_1","page_no":3,"ordinal":1}]}'
批准是面向经理与管理员的两步写入(scope 为 ekb:write):提案把内容绑定到当前活跃版本,若版本在确认前发生变化,确认会返回 conflict proposal_stale。受众 internal 只确认内容、不允许任何外发;customers 允许所有客户;customer_specific 仅允许指定客户,且会校验该客户属于本组织。撤回使用 {"operation":"withdraw","source_id":"ks_…","attestation_id":"ksa_…"}。
委托令牌检查使用当前角色和权限矩阵:只有仍具备知识写入权限的经理或管理员才拥有有效的 ekb:write。降级或只读自定义角色会移除已有令牌的此项有效权限。
Auth 健康元数据:GET /auth/health 新增 auth.authorityManifest:{schemaVersion:1, environment, source_sha, source_ref, config_digest, semantics_digest};运行配置无效时为 {available:false}。只返回标识与摘要。此实际运行描述不证明配置已登记、就绪或用户准入;不新增登记 HTTP 接口。
客户簿延续视图(仅组织所有者)
GET /crm/continuity?rep=<owner_email> 列出某位销售的全部在册客户及每个客户的覆盖情况——对照可跟进资料标准的资料等级与缺口码、历史(时间线条目与业务阶段历史)、证据(备忘与文档)、进行中业务及其下一步覆盖,以及所有者自身权限可见的待办任务(open_tasks 为 {state:"visible_only",visible,limited} 或 {state:"source_unavailable"};按客户分别查询;可见数量即使为零也不代表无私有任务,不推断私有任务是否存在;limited 为 true 表示达到 200 条上限,仅为下限;无效响应与服务故障均为 source_unavailable)。GET /crm/continuity/customer?id=<customer_id> 返回单个客户的完整度详情:每个要求码要么 satisfied——附覆盖它的记录,每条带原始记录人与时间戳——要么 missing。销售按客户簿本身(owner_email)匹配,因此已离职销售的已记录客户簿仍可查看,且不会虚构任职状态。两个读取仅限组织所有者(会话 owner 标志):其他人得到 403 owner_only;未知客户 id 与其他组织的客户 id 返回相同的 404;rep 参数必须是邮箱形式(否则 400 invalid_rep)。仅限浏览器会话——无 token 操作。(#2849)
可溯源互动上下文(crm.engagement.context.read)
为后续互动功能提供的一个有界、带权限校验、只读的客户上下文。CRM 路由为 GET /crm/customers/:id/engagement-context?purpose=…§ions=…&limit=…&cursor=…(浏览器会话),受治理操作使用相同输入:purpose(follow_up、product_recommendation 或 payment_collection)、可选 sections(默认为 profile、contacts、opportunities、transactions、timeline)、limit 1–100(每个记录分页默认 50)以及不透明的 cursor。每个分区返回 items、next_cursor 与 completeness(游标耗尽前始终为 incomplete);每条数据都带有 evidence_ref,包含 owner、kind、id、确定性的内容摘要 version、observed_at、confirmation 与 shareability。
curl -sS https://atlas.staging.forth.ai/assistant/v1/actions/crm.engagement.context.read \
-H "Authorization: Bearer $ATLAS_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"customer_id":"cus_…","purpose":"product_recommendation","sections":["profile","transactions"],"limit":50,"cursor":"…"}'
→ {"customer_id":"cus_…","purpose":"product_recommendation",
"sections":{"profile":{"items":[…],"next_cursor":null,"completeness":"complete"}, …},
"gaps":[{"code":"ineligible_evidence_excluded","section":"profile"}],
"next_cursor":null}待确认、已过期与已撤销的证据不会进入结果:每个分区至多出现一条通用缺口,不包含任何标识、标题、数量或日期。组织或负责人范围之外的客户与不存在的 ID 同样返回 404;该读取不产生任何写入,也不调用任何模型。游标绑定单一客户与用途,挪用到别处返回 400 invalid_cursor。purpose 是用途声明而非字段选择器——面向目标客户的字段白名单对所有用途生效,任何更大范围的披露仍需经过批准的精确内容发布。
反馈附件预览
超级管理员可通过 GET /governance/platform/feedback/{feedback_id}/attachments/{attachment_id} 读取已保存的反馈附件。系统会同时匹配反馈编号与附件编号;下载和预览均受同一套已登录超级管理员权限控制。
默认响应使用 Content-Disposition: attachment。添加 ?preview=1 可请求内联显示。Atlas 只允许已接受的 JPEG、PNG、WebP、GIF、MP4、QuickTime 和 WebM 类型内联显示。文档、SVG、未知类型和普通下载请求仍会强制下载,并设置 X-Content-Type-Options: nosniff。
问题反馈页
平台管理员队列:GET /governance/platform/feedback 支持组合使用 org_id(组织 ID 精确匹配)、sender(邮箱忽略大小写精确匹配)、type、status 和 handler。组织和邮箱条件会去除首尾空白。筛选和排序保存在 URL 中,表格支持逐项选择和全选当前可见问题,看板不显示选择框。队列每行与详情响应同时带有提交组织的显示名称 org_name(来自平台组织目录;无法解析时为 null,org_id 不变)。
附件并发控制:部署此 Worker 前须先应用 Governance 迁移 0041_platform_feedback_mutations.sql。每个问题的持久操作记录会在上传前保存所有计划写入的私有对象键,包括可能已写入但响应丢失的对象。问题创建与上传保护记录在同一事务中提交,附件元数据提交与保护记录释放也在同一事务中完成。删除操作会先锁定整个选择集合;存在活动上传时返回 409 feedback_mutation_in_progress,且不删除任何所选问题。上传清理失败保留为 upload_cleanup,删除失败保留为 deleting;再次确认平台删除会重试全部保留对象键,成功后才删除问题记录和保护记录。对象键和操作令牌不向列表或详情响应公开。
系统不会自动接管或按超时释放 uploading 记录,因为原请求仍可能写入 R2。Worker 中断或上传结束时 D1 不可用,都会保留保护记录及精确对象键并阻止删除。运维恢复须先确认原上传请求已经结束,再将该记录转入清理状态;仅凭经过的时间不能判断安全。上线时,应先等待旧版本上传请求结束,再启用或验证删除;回滚时保留此迁移及操作记录。此表仅归 Governance 所有,内存表族的所有权清单保持不变。
确认后调用 POST /governance/platform/feedback/batch-delete,请求体为 {"ids":["FB-2026-0042"]}:需提供 1–100 个不重复的非空问题 ID,仅含字母、数字、下划线或连字符,每个最多 128 个字符。单个问题也使用此接口。必须通过平台超级管理员身份验证,并提交同源 JSON;普通组织成员无权读取平台队列或删除问题。成功响应 {"deletedIds":[...]} 仅列出实际删除的问题,客户端只移除响应中的 ID。
删除前校验完整选择集合,先删除私有附件对象,再通过一次 Governance 原子事务删除回复、附件元数据和问题,并为每个问题写入 feedback.delete 平台操作审计,记录已验证操作人、目标组织、parent_request_id 中的问题 ID、请求 ID 和源码版本。删除后,平台端和提交人端的列表及详情均不可再访问该问题。不提供恢复或提交人自行删除。
错误:400 invalid_selection 表示格式错误、重复或超限;409 unknown_selection 或 selection_changed 表示未知或并发变化的目标;409 invalid_attachment_metadata 表示不安全的对象引用;503 feedback_cleanup_incomplete 表示存储或数据库故障。部分文件可能已经删除,响应不确定时也可能已经完成数据库删除,因此不能声称原问题或附件仍完整可用。请先刷新权威数据,再仅重试仍存在的问题。数据库事务失败时,保留的问题及附件元数据包含重试私有对象清理所需的键。
POST /governance/feedback 在 JSON 与 multipart 两种表单中均接受可选的 severity(p0、p1、p2、p3)。缺省按 p2 保存;不在允许列表内的值在落库前即被 400 拒绝。每条反馈都携带严重级,存储层 CHECK 与代码允许列表保持一致。
新建问题的 id 是可读的工单编号 FB-<年份>-<序号>(例如 FB-2026-0042),由原子计数器按自然年、按提交顺序编号;编号不会重复使用,即使问题已被删除。此前创建的问题保留其 fb_… 标识,所有接口对两种形式均按不透明字符串处理。
GET /governance/feedback 返回调用方已提交的问题(含状态、严重级与附件元数据),服务端按已验证会话划定范围:经理和成员只能读到本人提交的记录,组织管理员可读本组织内全部提交,空结果为 200 {"items":[]}。组织、用户与角色一律来自会话,绝不取自请求本身。状态流转仍是仅超级管理员可执行的平台操作。
GET /governance/feedback/{issue_id} 返回单个问题及其互动记录——提交人(或组织管理员)与 Atlas 团队的往来评论,以及每一次状态变更,按时间正序排列。可见范围与 #1990 一致;范围外的 issue_id 与不存在的 issue_id 返回相同的 404。POST /governance/feedback/{issue_id}/replies 携带 {"body"}(1–2000 字符)即可在互动记录中追加评论。
POST /governance/platform/feedback/{issue_id}/replies 允许超级管理员在同一互动记录中回复,并记录操作管理员为发送者。超级管理员的状态变更(POST …/status)除写入平台审计外,还会在互动记录中追加一条可见的状态变更。历史状态变更不做回填。
运行反馈读取
GET /governance/run-feedback 面向人工反馈工作台,提供助手回复赞/踩事件的有界组织范围读取。它与仅限本人的 GET /governance/feedback-status 相互独立,绝不由后者推断组织级权限。每行保留稳定的事件 id、source:"run_feedback"、原始 rating(liked 或 disliked)、status(该作者在该运行的最新事件为 current,更早事件为 superseded)、署名 author、occurredAt 与运行锚点:{"id","anchor":"verified"};运行行已不存在的为 "missing";无运行锚点的历史事件为 null。无锚点与锚点缺失的行对经理和管理员仍可读,但绝不是已验证的依据;销售永远不会收到它们。
授权沿用既有运行可见性路径:经理与管理员读取其组织内其角色可见运行的反馈;销售读取自己发起且锚点已验证的运行反馈,以及下述雇员规则允许其读取的 AI 团队运行的反馈;团队页面与评测关联的运行仍须有效的 AI 团队成员身份,与 /governance/runs 完全一致。某个雇员的运行还只对能在 AI 团队中读取该运行的人可见:该雇员的主管(销售或经理均可)或组织管理员;由询价(RFQ)交接派生的运行,还须其来源的售前助理雇员同样对其可见。雇员的评测运行同样遵循这一雇员规则。未指明雇员的团队运行(例如教学对比回放)仅对组织管理员及其发起人可见。同一规则也适用于 /governance/runs 与 /governance/trace-v2 读取,以及团队运行上的会话标注;隐藏的运行不出现在列表中,按 id 读取返回 404。组织、用户与角色一律来自已验证会话;请求参数中的任何声明一律忽略。
分页为只追加事件序上的键集分页:limit 1–100(默认 50),cursor 取上一页 nextCursor。页面不会重复或遗漏首个页面已存在的行,隐藏行在任何分页边界都不会出现。runId 收窄到单个运行;未知或调用者不可见的运行返回空信封,与无数据不可区分。错误:格式错误的 cursor、limit 或 runId 返回 400 invalid_request;未识别调用者收到 200 {"items":[],"nextCursor":null}。该接口只读:不写入、不做分诊/教学/召回/发布转移、不复制进 Memory;问题反馈(/governance/feedback)是另一领域,绝不在其中消费。
运行关联读取
GET /governance/trace-v2/runs/:id/link 对调用方组织中通过既有团队权限检查的运行返回 {"runId","predecessorRunId","successorRunIds"}。未关联的运行,其前序 ID 为 null,后续 ID 列表为空。隐藏的 AI 团队关联运行 ID 会被隐去。此标识读取不为普通运行新增负责人过滤。不存在的运行、其他组织的运行和隐藏的 AI 团队运行返回相同的 404 {"service":"atlas-governance","error":"not_found","message":"No such run."}。格式错误的 ID 返回 400 invalid_request。调用方的组织与角色来自已验证会话。
沙盒反馈读取
GET /knowledge/sandbox-feedback 面向人工反馈工作台,以类型化的组织范围信封读取知识库沙盒回答评分。它服务与兼容列表 GET /knowledge/feedback 完全相同的行——按组织隔离、任何已登录角色——但信封显式命名语义:稳定 id、source:"sandbox"、原样保留的 rating(up 或 down)、存储的 status 分诊状态、署名 author、occurredAt(epoch 毫秒;仅当存储时间无法解析时为 null)、沙盒 mode、评审内容(question、answer、reason、note)、提问 scope(kind、descriptor、label),以及引用的 sources——每项的 available 在读取时解析:已删除的引用渲染 available:false,如实标注,绝不静默改写或丢弃。
行可见性:持有 knowledge 写权限的调用方(默认矩阵中的管理员与经理)可见组织内的全部评分行——包括引用了其他成员个人文档的行,这正是分诊所需的可见性;其余调用方仍不会收到引用其无权打开文档的行。
权限不变:组织来自已验证会话,声明组织或行为人的请求参数一律忽略。分页与兼容列表同一键集纪律——limit 1–100(默认 50,cursor 取自 nextCursor)——绝不重复或遗漏首个页面已存在的行。格式错误的 limit 或 cursor 返回指名参数的 400 invalid_request;未识别调用者与所有知识库路由一样收到 401。该接口只读:不写入、不做任何分诊或更正——它们仍走 PATCH /knowledge/feedback/:id。问题反馈(/governance/feedback)是另一领域,绝不在其中消费。
知识证据引用
POST /knowledge/evidence 将知识文档中被引用的一个位置保存为可用于客户工作的持久引用。任何已登录角色均可保存;文档在保存时必须对调用者可见且已 ready——其他成员的个人文档与不存在的 id 一样返回 404 not_found,尚未就绪的文档返回 409 evidence_source_not_ready。请求体为 {"file_id":"…","passage_index":N} 或 {"file_id":"…","sheet":"…","range":"…"}(两者不可同时提供),可附加不超过 500 字符的 note,以及产生该引用的回答所返回的 feedback_proof_id;当该凭据仍指向文档的当前版本代时,引用绑定到该确切版本代。可选的 Idempotency-Key 请求头使结果未明后的重试重放调用者自己的引用;其他成员使用同一 key 返回 409 idempotency_conflict。存储的记录只保留文档显示 id、版本代、行版本、定位符与备注——绝不存储文档文本或文件名。
GET /knowledge/evidence/:id 按调用成员当前的访问权限解析一个引用,返回 {"item":{…,"status","label","excerpt","current_version"}}。available 携带文档当前名称与定位符处的段落文本(上限 480 字符;定位符不再命中时 excerpt 为 null,绝不显示相邻段落);revised 表示文档在保存后被重新读取(version 为保存时版本,current_version 为当前版本);processing 在文档重新读取期间给出名称;unavailable 不携带名称或摘录——文档被删除、上传被替换、权限调整后失去访问,均统一表现为这一状态,不作区分。其他组织的引用 id 返回 404。GET /knowledge/evidence 按时间倒序列出调用者自己的引用并逐行给出同样的解析状态(limit 1–100,默认 50,游标 cursor 来自 next_cursor);来源已不可用的行仍保留在列表中,标记 status:"unavailable" 且不携带来源元数据。除保存外该接口只读:引用没有编辑或删除动作,保存引用也不会将任何客户事实变为已批准——source-use claims 仍是独立的受治理接口。部署消费方之前,请先应用增量 EKB 迁移 EKB0048_library_evidence_references.sql。
客户资料、导入与销售阶段
GET /crm/customers/:id 的 transactions_summary.by_currency 按记录的 ISO 币种返回 total_sales_cents 与 outstanding_cents。旧数据币种缺失或无效时返回 currency: null,客户端必须显示币种不可用,不能猜测;已取消和已退款交易仍不计入。滚动升级期间保留的旧版标量汇总只在唯一币种为 CNY 时非零;客户端应读取 by_currency。
客户创建和更新接受可选的 main_markets(主营市场)、brands(品牌)、products(产品),每项最多 2,000 个字符。null 清空,省略保持原值。档案读取返回这些字段,客户负责人权限不变。
Excel/CSV 导入可映射 website、background、main_markets、brands、products,以及 wechat、whatsapp、linkedin、facebook、instagram、other 社媒列。社媒需要联系人姓名,每个账号最多 120 个字符。空白更新单元格不清空旧值。导入前会显示未映射列、未导入工作表及超过 1,000 行的截断提示。
POST /crm/settings/pipeline/stages、PATCH /crm/settings/pipeline/stages/:key、DELETE /crm/settings/pipeline/stages/:key、POST /crm/settings/pipeline/stages/:key/move 允许 admin、组织 owner 和 manager,rep 不可修改。DELETE 直接删除阶段而非隐藏:仍有业务停留在该阶段时返回 409 并附带数量,且始终保留至少一个启用的成交(Won)与失败(Lost)阶段。未自定义的默认阶段新增 sample_sent(样品送样)和 sample_evaluation(样品评估),位于报价后、谈判前;自定义阶段保持不变。
客户目录:潜在客户拆分已撤销
客户目录重新列出全部客户类型,潜在客户也在其中(#4200,撤销 #4158)。GET /crm/customers 与 GET /crm/customers/stats 不再接受 exclude_customer_type:该参数只在预发布环境存在了一天,现已移除;未知查询参数一如既往被忽略。如需按单一类型筛选目录,请使用既有的 customer_type=<key> 参数,它仍按调用方自己的组织目录校验——未知键返回 400 invalid_customer_type。潜在客户就是 customer_type 标为潜在客户的客户;不存在独立的潜在客户集合,也没有任何路由被移除。
个人集合保存视图与确定性集合排序(STORY-V2-ERP-01)
GET /crm/collection-views?collection=contacts|companies|deals|tasks 返回调用者本人针对该集合保存的视图,格式为 200 {"items":[…]};空结果、其他成员的列表或其他组织的列表都返回同样的 {"items":[]}。视图仅保存配置——搜索词、经过校验的筛选、排序、可见列顺序与支持的显示模式——不复制业务数据。应用视图会重新发起集合查询,因此每次请求都会重新评估记录可见性。
PUT /crm/collection-views 保存视图:请求体为 {"collection","name","query":{"text","filters":[{"field","operator","values"}],"sort":{"field","direction"}},"columns","mode","existing":{"id","revision"}?}。新建返回 201 {"item":…},更新返回 200 {"item":…} 并递增 revision。每个字段、操作符、排序键、列与模式都会先按各集合的白名单校验:未知或已废弃的设置以 400 {"error":"invalid_view","field","reason"} 拒绝并指明确切设置——绝不静默丢弃或放宽。同一用户同一集合下重名返回 409 view_name_conflict;使用过期 revision 更新返回 409 view_revision_conflict;指向他人的 existing id 与未知 id 一样返回 404 not_found。每个用户每个集合最多保存 100 个视图(409 view_limit)。允许的筛选:联系人 customer_id(eq)、role(eq primary|regular);公司 status,country,customer_type,customer_class,engagement,owner(eq);商机 stage(in)、tier(in)、owner(eq)、close_date(gte/lt,YYYY-MM-DD);任务 status(eq open|done)、assignee(eq)、due_at(gte/lt,YYYY-MM-DD)。看板模式仅对商机有效。
三个 CRM 集合列表新增确定性的调用者可选排序:GET /crm/contacts?sort=<field>:asc|desc(updated_at,name,email,title,customer_name)、GET /crm/customers?sort=…(updated_at,name,country,customer_class,created_at)与 GET /crm/engagements?sort=…(updated_at,title,close_date,amount)。未知或格式错误的排序返回 400 {"error":"invalid_sort"}。省略时沿用历史默认顺序;两种情况下排序值相同的记录都按稳定记录 id 决胜负,因此翻页不会重复或丢失记录,total 仍为同一查询的未分页计数。任务集合的查询执行仍归属 Tasks 服务;任务保存视图先在此持久化,待该执行方上线后生效。
批量解析客户显示名(BUG-158)
GET /crm/customers/names?ids=<id> 用一次请求解析用于标签兜底的客户显示名:ids 参数可重复(也可用逗号分隔),每次最多 100 个去重后的 id。应答为 200 {"items":[{"id":"…","name":"…"}]},只包含调用者组织内真实存在的客户——已删除、跨组织或不存在的 id 只是缺席,绝不会产生逐客户 404;可见性与逐客户读取完全一致:代表只能解析自己名下的客户,已入池的客户不在其列。空 id 集合应答 200 {"items":[]};超过 100 个 id 应答 400 too_many_ids;未认证调用者与其他 CRM 路由一样被会话边界拒绝。
客户 tier 退出:改按类型、等级与标签集筛选(#4224)
僵尸字段 tier 从客户 REST 接口移除:它在界面上本就看不到,每一行存的也都是 standard 列默认值,按它筛选永远只匹配到这一种。GET /crm/customers 不再接受 ?tier=,PATCH /crm/customers/:id 也不再接受 tier 字段——仍携带它们的请求会被忽略,与任何未识别的查询参数或请求体字段一样,数据库列在后续版本移除前保持默认值。客户创建与导入不再保存调用方提供的 tier。目录筛选请使用既有的 customer_type、class 与 label_set 参数。互动计划子系统维持现状:GET /crm/engagements?tier= 与触达策略的 match.tier 条件保留各自的 tier 表面。
GET /crm/portfolio 新增同样三个筛选参数,服务端按客户列表的同一套语义实现:customer_type=<key> 与 class=<key> 按调用方自己组织的目录校验——未知键分别返回 400 invalid_customer_type 或 400 invalid_customer_class;label_set=<id> 只保留至少打了一个该标签集标签的客户,且只经调用者可见的标签集解析,未知或他人的标签集 id 一律匹配为空。组合接口原有的单值 tier 筛选已移除,返回的每个客户行改为标出 customer_type 与 customer_class,不再有 tier。
任务隐私
本人创建并分配给自己的任务默认标记为 private,即使关联客户也不例外。仅负责人本人可访问,经理和管理员无权绕过。team 任务对负责人、创建人和组织管理人员可见,其他销售人员不可访问。筛选、来源重试、提醒和代理调用均遵守同一权限边界,定时共享上下文不包含私密任务。
登录应用的任务 API 在 POST /tasks 中接受 visibility: "private" | "team"。分配给他人须具备经理或管理员权限(所有者拥有管理员权限),且须使用团队可见范围。GET /tasks?assignee_email=EMAIL 仅缩小授权结果;无可访问项时返回 200 {"items":[]},不可访问的任务 ID 返回 404。创建后不可更改可见范围,私密任务不可转交,团队任务仅管理人员可重新分配。此会话 API 与上述使用 bearer token 的受治理操作 API 相互独立。
客户知识来源关联
销售可以将已有客户关联到知识库文档,并可解除关联,不会删除任何一侧记录。CRM 只保存关联关系:文档始终留在知识库,每次读取都会通过调用者自身的授权向知识库解析其当前状态。
GET /crm/customers/:id/knowledge-links 返回的每一项都带有 state:current(source 对象包含文件名称、类型、大小与状态字段,不含正文内容)、unavailable(文档已在知识库删除,或属于其他组织)或 unknown(知识库暂不可达)。POST /crm/customers/:id/knowledge-links 携带 {"source_id": "..."},幂等设计——重复关联返回既有关联;仅当知识库对该调用者确认来源时才会落库:未知或不可访问的来源返回 404 {"error":"source_not_found"},不泄露其他组织是否存在该来源;知识库不可达时返回 503 {"error":"source_unavailable"},不写入任何数据。DELETE /crm/customers/:id/knowledge-links/:linkId 仅删除关联本身。读取遵循客户可见性门禁;关联与解除关联遵循与所有客户操作相同的按负责人限定的授权。每个客户最多 50 条关联,并发添加也不能超过上限。达到上限后重复添加已有组合,仍须通过知识库的来源权限复核,再返回原关联 ID、创建人和时间;新增组合返回 400 link_limit_reached。
私有文档共享申请
已登录且拥有文档上传权限的销售代表,可将自己的私有文档申请加入同一组织内指定的公司文档文件夹。申请只向有权限的审核人提供提交时的文件快照,不开放原私有文档。审核人必须是当前组织的所有者、管理员或经理,且拥有知识写权限。不支持委托令牌。
POST /knowledge/library/proposals 要求 Idempotency-Key 请求头及 {"source_file_id":"…","destination_folder_id":"…","consent":true,"consent_version":"1"},返回 {item}。GET /knowledge/library/proposals?view=mine|review&limit=30&cursor=… 返回 {items,pending_count,failed_count,next_cursor};limit 为 1–100。每项都带有 result_status——已通过审批的公司文档副本的实时处理状态(draft、extracting、ready、failed;审批前为 null),按组织范围从副本文件联表读取。view=review 另返回 failed_count:该审核人批准且副本处理失败的申请数;副本重试成功后即清零。无权查看的集合返回空列表。GET /knowledge/library/proposals/:id 返回可见申请;GET /knowledge/library/proposals/:id/content 下载待审核快照,已结束或不可见的快照返回 404。
POST /knowledge/library/proposals/:id/approve|reject|withdraw 要求 Idempotency-Key 及 {"expected_revision":1,"reason":"选填,最多500字符"}。申请人可撤回自己的申请,审核人可通过或拒绝。响应不确定时,使用同一请求键及相同内容重试。通过审核会在同一事务中创建独立的公司文档副本并记录 result_file_id,仅当副本不超过 50 MiB 且符合读取器限制时,按 POST /knowledge/files/:id/confirm 的同一流程处理该副本:经由受治理的 AI 网关执行并归属于批准的审核人,受支持的副本可无需二次手动操作达到 ready。更大的副本保持已上传、未处理;共享审批不等于大文件处理许可。个人原文件保留。审批先于处理落库:处理失败不会撤销审批、删除副本或使审批响应变为错误——副本保持 draft(可确认)或 failed(可通过 POST /knowledge/files/:id/retry 对 result_file_id 重试),并由 result_status 如实反映。使用同一审批请求键重放会返回已记录的结果,不会再次处理。无文档上传写权限的审核人仍可完成审批,副本保持 draft。视频文件(MP4、MOV、WebM)跳过提取:副本保持 draft 等待资料库视频处理,且永不计入失败。
状态包括 preparing(准备快照)、pending(待审核)、approved(已通过)、rejected(已拒绝)、withdrawn(已撤回)。源文件或目标文件夹变化、公司文档重复及并发审核均明确拒绝,错误码包括 source_changed、destination_changed、duplicate_file、revision_conflict、decision_conflict、operation_in_progress,应刷新后再操作。配额预留快照及未来副本所需空间;重试可恢复中断操作,不会共享私有存储对象。部署前需应用 EKB 增量迁移 0029_library_document_proposals.sql。
大型资料库的重复检查可能返回 409 verification_in_progress、verification: {verified,total} 及 Retry-After: 1。计数只包括大小相同的公司文档。使用相同请求键和版本继续审核,可从已保存的进度恢复;此时尚未通过审核。公司文档发生变化时会安全地重新检查。部署顺序为 Auth 在先、EKB 在后,并将 EKB 的 AUTH Service Binding 指向同一环境的 Auth;每次读取申请或审核时,均通过 Auth 核实当前组织成员身份与权限。
文档后台读取
这些读取接口须符合处理大小边界;仅存储的大文件不会进入读取队列。
POST /knowledge/files/:id/confirm(草稿)和 POST /knowledge/files/:id/retry(失败文件)不再在请求内读取文本类、Word(.docx)或 PowerPoint(.pptx)文档。它们会立即返回 200 和通常的 {item} 详情,此时 status: "extracting"、extraction_stage: "queued",并在 EKB 队列中为该文件版本排入一个读取任务(迁移 0042_library_extract_jobs.sql)。文件处于 extracting 期间,GET /knowledge/files 和 GET /knowledge/files/:id 返回 extraction_stage:queued、reading、describing_images 或 indexing(没有当前任务时为 null);其他行不含此字段。请轮询详情,直到 status 为 ready 或 failed。
任务发布的段落、摘要和关键词与请求内读取的结果相同,随后为文件排入向量任务。一个文档最多保留 2,000 个段落,且已存段落不超过 1,500,000 字节(Office:段落与已存读取结果合计不超过 1,800,000 字节);超出部分只保留文档开头。因内容而失败的读取会立即以其类别失败(empty_file、decode_failed、unreadable_office);暂时性失败会重试,第三次失败后文件标记为 failed 并带有类别(例如 storage_error、extraction_stalled),可用 POST /knowledge/files/:id/retry 重试。针对旧文件版本、或针对与排队时不同字节的任务不会写入任何内容。PDF、图片和电子表格仍在「确认」请求内读取,并像以前一样返回 ready 或 failed;文档提议的审批仍在审批请求内处理其副本。
上传大文件并保留为未处理原件
知识库、我的文件、助手拖入文件和客户文档入口,对各自既有支持类型的原文件,单个上限为 1 GB(1 GiB;1,073,741,824 字节),包含边界值。这是存储上限。超过 50 MB(50 MiB;52,428,800 字节)的文件保持已上传、未处理,不自动解析、提取、生成向量、读取关联信息或调用模型。恰好 50 MiB 的文件保留既有自动处理方式及读取器限制;上传成功不保证内容可读取。
直接表单上传 POST /knowledge/files 和 POST /crm/customers/:customerId/documents 保留 25 MiB(26,214,400 字节)上限。超过该值时使用下列分片接口。每个完整分片为 16 MiB(16,777,216 字节),最后一片包含剩余字节;1 GiB 原文件需要 64 片。发送分片原始字节,不使用 base64 或包含整个文件的 JSON。
| 知识库接口 | 请求与结果 |
|---|---|
POST /knowledge/files/multipart | JSON {name,size,scope?,folder_id?},携带 Idempotency-Key。size 为原文件字节数;scope 为 personal 或 company,遵守既有权限。返回 {uploadId,partSize,partCount};已完成上传的重放返回 {item}。既有允许的所有原文件类型均可使用。 |
PUT /knowledge/files/multipart/:uploadId/parts/:N | 发送从 1 开始编号的分片原始字节;分片连续覆盖全部文件,大小须符合声明。 |
POST /knowledge/files/multipart/:uploadId/complete | 沿用原 Idempotency-Key。校验合并后的实际字节、重复内容及最终配额后返回 {item}。 |
DELETE /knowledge/files/multipart/:uploadId | 取消未完成上传;取消不会删除已完成的原文件。 |
| 客户文档接口 | 请求与结果 |
|---|---|
POST /crm/customers/:customerId/documents/multipart | JSON {name,size,content_type?,kind?,note?,propose?,engagement_id?,deal_answer?,pre_atlas_ref?},携带 Idempotency-Key。返回 201 {uploadId,partSize,partCount};已完成重放返回 200 和无包装的客户文档(不含存储键)。客户、文档类型和业务答案的权限及校验不变;propose 不会绕过处理大小限制。 |
PUT /crm/customers/:customerId/documents/multipart/:uploadId/parts/:N | 按返回的分片大小发送原始字节,编号从 1 开始。 |
POST /crm/customers/:customerId/documents/multipart/:uploadId/complete | 沿用原 Idempotency-Key;服务端校验后返回 201 和无包装的客户文档;已完成重放返回 200。 |
DELETE /crm/customers/:customerId/documents/multipart/:uploadId | 返回 200 {cancelled:true},不删除已完成文档;操作进行中返回 409,清理失败保留状态并返回 503。 |
请求结果不确定时,重试须保持同一登录成员、上传标识、原文件字节及幂等键。上传状态绑定组织与成员,不可传入其他组织身份。类型被拒绝、分片缺失或大小不符均不会创建已完成文档。超过原文件上限返回 413 too_large;知识库超额返回 413 quota_exceeded。组织知识库存储配额仍为 2 GiB(2,147,483,648 字节)。未完成上传清理失败时会保留恢复状态。完成响应丢失后,应先检查或重放同一次上传,再决定是否重新开始。
大文件仅保留既有的 MP4、MOV、WebM 视频显式处理流程,仍受时长、编码和设备限制。大型非视频文件不启用“处理”:TXT/MD/CSV 和 PDF 读取上限仍为 25 MiB,图片为 8 MiB,Office/XLSX 为 32 MiB,客户提案为 10 MiB。确认、重试、共享审批、队列恢复及后台读取均不能绕过边界或自动授予处理许可。复制的大文件同样保持未处理,须对副本执行其支持的显式处理。存储权限、计费规则、反馈及邮件附件限制、导入预算不变。 CRM 对已取消的记录返回 409 upload_cancelled;仅在此时使用新幂等键重试。CRM 完成操作中断后,其 120 秒租约到期即可恢复:初始化返回分片计划及 completionOnly:true,客户端直接调用完成接口,无需重新发送分片。
浏览器本地视频 AI 就绪处理
原文件超过 50 MiB 时,成员须从设备重新选择该视频原文件,再确认处理。浏览器分片计算校验和,服务端与已存原件核对;不匹配的文件不能授予另一来源处理许可。
POST /knowledge/files 接收直接上传上限 25 MiB 内的 MP4、MOV、WebM;最大 1 GiB 的原文件使用分片上传。视频保持为未处理原文件;普通确认不会执行视频推理。具有文件贡献权限的登录用户须在资料库显式开始。本路径不在服务端调用推理或向量嵌入。
下列接口均受登录组织与文件权限限制。浏览器计算原文件 SHA-256,并使用编译时固定的模型流水线清单。写入绑定原文件版本、校验和、准确技能身份和运行 nonce;nonce 不授予跨组织权限。
| 接口 | 请求与结果 |
|---|---|
POST /knowledge/files/:id/video-readiness/start | JSON {sourceChecksum,pipeline,replacesRunId},带 Idempotency-Key。返回包含 runId,nonce,sourceChecksum,skill 的 run;相同幂等键重试保留运行身份。首次开始传 replacesRunId:null;中断后重试须显式传入状态接口中观察到的未完成运行 ID,替代该运行。 |
POST /knowledge/files/:id/video-readiness/segments | {runId,nonce,ordinal,segment:{kind,start,end,text,language}}。类型为 transcript 或 frame,时间以秒计。同序号、同内容重试幂等;冲突内容被拒绝。 |
POST /knowledge/files/:id/video-readiness/finalize | {runId,nonce,pipeline,metadata,coverage,durationMs}。验证已保存证据、边界和原文件身份后发布就绪状态。元数据含 durationSeconds,width,height;覆盖范围声明音频是否存在、已捕获与总时长,以及抽样与计划画面数,抽样不代表完整覆盖。 |
POST /knowledge/files/:id/video-readiness/cancel | {runId,nonce,reason,durationMs}。停止尚未完成的运行,不发布就绪状态;不会撤销已完成结果。 |
GET /knowledge/files/:id/video-readiness/status | 返回 {readiness:{state,runId,reason,result}}。结果包含 source: "browser_reported"、固定技能与模型流水线身份、时间戳、元数据及覆盖范围。 |
只有完整保存的结果才为 AI 就绪。上限为五分钟、240 条证据段落和 24 个抽样画面。不兼容的编码、设备或处理中断均保留原文件。本地转录与描述属于不可信派生证据,不代表服务端已验证推理;仍须通过权限校验和安全渲染。质量报告保留未知用量,不虚构零成本。
画面提取上限为最长边 1,280 像素、单帧 921,600 像素、JPEG 质量 0.8、单帧 512 KiB、总计 6 MiB。覆盖范围分别报告计划和保留的抽样数量。同一开始请求键如果更改执行者或替换意图,会返回 409 idempotency_conflict。
复盘活跃度变化(#3340)
GET /crm/retrospectives/current 与 GET /crm/retrospectives/:id 现可在存储指标中携带增量字段 metrics.extensions.activity_changes(schema v1)。它列出复盘周期内实际应用的客户活跃度变化,每条包含当时记录的起始/目标层级、提交时间戳、来源(sweep 为自动应用,confirm 为人工确认,并区分发现方与确认人)、应用时原样记录的标准版本,以及记录的原因输入。待处理或已忽略的提案不计为变化,历史记录也不会按今天的标准重新标注。在未来的生产者写入快照之前,负责人、归属版本与客户分级均为 null,因此每条记录都是 attribution:"unattributed" 的组织层面观察证据:业务员维度的复盘以 owner_unavailable 报告 partial 覆盖且不含条目;主管团队复盘列出组织观察到的记录但不提供按人统计。仅当组织巡检历史证明整个周期采集连续无缺口时覆盖才为 complete,此时 summary 才提供组织范围的 {applied, automatic, confirmed} 计数。来源不可读时该扩展冻结为 unavailable;此前存储的复盘不含此字段,读取方会忽略未知增量字段。
复盘重点客户处理(#3339)
GET /crm/retrospectives/current 与 GET /crm/retrospectives/:id 现可在存储指标中携带增量字段 metrics.extensions.focus_handling(schema v1)。它展示本周期实际发布的重点客户是如何被处理的。评估集合是每个工作日所有已发布重点版本中客户的并集,按业务员、工作日(按组织时区)与客户去重;人工提升与算法选择分开标记。每条记录包含工作日、周期内首次列入时间戳、包含该客户的简报版本标识、引用这些版本的处理决定、去重后的已接受任务来源标识(共享来源标识的重试接受只计一次)、按提交时间排序的最新处理状态、周期结束截点时关联任务的状态、周期内完成工作的完成事件引用,以及归属业务员、接受人与完成人相互独立的操作者信息。编辑、忽略与接受的决定分别呈现;接受不等于完成;无记录决定的客户日是「未知」,不等于未处理;并且不计算任何综合「已处理」比率。任务证据来自 Tasks 域自身的历史投影,私密任务一律回答未知且不携带任何元数据。本周期内记录的、关于更早周期客户日的处理决定会以跨期行(carry_over:true)呈现,携带更早的工作日、null 的首次列入时间戳,且不计入任何汇总数字——那是更早周期义务的证据,绝非新的分母。仅当两个生产者都证明采集窗口连续无缺口时覆盖才为 complete,此时 summary 才提供 {customer_days, decision_recorded, accepted_work, completed_by_end} 计数。来源不可读或无权访问时该扩展冻结为 unavailable 且不含条目;此前存储的复盘不含此字段,读取方会忽略未知增量字段。
复盘计划执行情况(#3338)
GET /crm/retrospectives/current 与 GET /crm/retrospectives/:id 现可在存储指标中携带增量字段 metrics.extensions.plan_adherence(schema v1)。它对照本周期内已接受的计划触达与其有据执行,数据读取自冻结的 crm.plan-history.v1 与 crm.schedule-history.v1 投影。三次触达义务与日程到期分别独立分组、从不合并,信封中不存在任何比率、完成率或排名。每条记录携带来源分组、不可变的义务与已接受版本标识、客户、接受时快照的负责人(与实际执行人分开标注)、来源自身的本地到期日及其时区和到期日起始时刻、处置(due 到期;excluded_before_due 到期前排除,附排除事件引用;carry_over 跨期结转——更早周期义务在本周期内有完成证据,不计入到期分母),以及执行状态(三次触达:recorded_completed/no_linked_completion;日程:provider_accepted——绝不称为已送达、failed 或 unknown),以及指向既有有权访问记录的引用。未接受的提案、刷新的建议、草稿与未关联的完成从不计入;周期结束之后的完成不影响已保存的结果。仅当两个生产者都证明基线可靠且整个周期 admitted 证据连续无缺口时覆盖才为 complete,此时 summary 才提供按来源分组的计数(三次触达:due、excluded_before_due、carry_over、recorded_completed;日程:provider_accepted/unknown;不适用的字段为 null)。缺失或晚于采集起点的基线至多为 partial(列出已观察记录且不含总数),观察不到任何内容时为 unavailable;缺口、截断与未解析的负责人均为部分;日程记录携带其归属 basis 视图中接受时快照的负责人;owner_unavailable 仅标注 basis 不在日志中或记录时无快照的行。缺口、截断与未解析的负责人均为部分。basis 在到期日当地午夜之后才接受的日程义务按到期前排除呈现,并在该行的证据引用中记录此分歧。来源不可读时该扩展冻结为 unavailable 且不含条目,绝不影响基础指标;此前存储的复盘不含此字段,读取方会忽略未知增量字段。
客户分级标准与人工审核(#3336)
「设置 → 销售与客户关系管理 → 客户分级」统一维护等级、量化标准及逐项审核。GET /crm/classification/values 使用 labels:read,等级及规则写入使用 labels:write。现有 POST /crm/classification/values、PATCH /crm/classification/values/:id、DELETE /crm/classification/values/:id 继续提供双语新增、改名、排序及删除。PATCH 支持 {swap_with, expected_order, other_order},原子交换同类等级位置;旧位置返回 409 stale_catalogue。删除当前标准引用的等级返回 409 classification_policy_in_use:须先修改或停用标准,若客户仍使用该等级,再选择同类替代值。已流失及最后一个值仍受保护。POST 与 PATCH 还接受 color(调色板键:slate、red、orange、amber、green、teal、blue、violet、pink;PATCH 可传 null 清除),其他值返回 400 invalid_color。
GET /crm/classification/metric 返回 {item} 或空值。PUT /crm/classification/metric 接收 {version, name, source, unit, year, ranges:[{value_id,min,max}]},其中 value_id 是等级的稳定键,而非数据库行 ID;必须引用本组织现有且非已流失的等级。首次使用版本 0,修改须提交当前版本。DELETE /crm/classification/metric 接收 {version} 并停用规则,版本不会重置。空值、重复、重叠、无效或不可用的引用均被拒绝。区间包含下限、不含上限;null 表示不限,区间间隙表示不匹配。
数据来源为 paid_sales、company_revenue、business_size 或 custom。销售额仅统计指定 ISO 币种和 UTC 自然年的已付款交易,使用主币单位;缺少依据不视为零,也不进行汇率转换。其他来源通过 PUT /crm/classification/metric/observations/:customerId 提交 {version,value,source_note},来源说明须对应当前规则、单位和年份。
GET /crm/classification/metric/preview?limit=25&after=customerId 返回 {metric,items,next_cursor},limit 为 1–50。逐项包含客户、current_class、指标 value、source_note、建议 value_id、status(match、no_match 或 insufficient)、applied 和 evidence_token,单位和年份在 metric 中明确返回。已付款销售额还返回供本地化展示的 paid_sales_evidence:{transaction_count,currency,year};无依据时为 null,不修改不可变的来源说明或令牌。只显示有权访问的未归档客户;销售代表仅可见归属自己的已分配客户。已流失客户不会产生改级建议。
POST /crm/classification/metric/apply 接收 {version,customer_id,value_id,evidence_token}。原子操作重新核验等级写入权限、客户访问范围、规则、数据版本、客户状态和目标等级。旧审核返回 409 stale_review,已使用的确认不会覆盖之后的手动改级。成功后只更新该客户的等级,并不可变地记录审核人、时间、规则和数据依据。不会自动或定时改级;手动选择、潜在客户/首次赢单/已流失行为及目录筛选保持原样。旧 /crm/settings/classification 维度接口保持兼容,不执行分级规则。
制度列表的当前版本状态
GET /knowledge/policies 的每条记录增加 current_release_no:仅当该组织有有效的当前发布版本时才返回版本号,否则为 null。可编辑的库内副本仍可能是 Draft,同时旧版继续生效;旧数据中的 Published 标签若无正式发布版本,仍仅供参考。此字段由同一组织范围内的列表查询读取;撤回当前版本后恢复为 null。
制度与工作流版本发布
经理和管理员可为知识库的制度或工作流草稿发布一个不可变的版本;所有登录角色均可读取发布结果。发布会快照草稿已存储的内容字节,记录作者、时间戳、十六进制 SHA-256 内容摘要与稳定的分节/分步标识,并记录当时同组织关联关系的依赖快照。若已声明的关联来源在发布提交时不再属于同一组织或不存在,则与未知资产一样返回 404,不创建版本。发布事务会再次检查来源,因此删除竞态不会激活内容。编辑草稿不会改变已发布版本。在步骤校验上线之前,executable 发布请求会被 400 not_eligible 拒绝;版本默认仅供参考,仅当发布请求显式设置时才为 answerable。
POST /knowledge/policies/<id>/releases (工作流同理:/knowledge/workflows/<id>/releases)
{"base_version": 7, "answerable": true} # base_version 为必填
{"base_version": 7, "restore_from": "mrel_…"} # 回退:用旧字节发布一个新版本
→ 201 {"item": {"id": "mrel_…", "release_no": 3, "published_at": "…",
"author_email": "…", "status": "released", "content_digest": "…",
"answerable": true, "executable": false,
"content": {…}, "stable_ids": {…}, "dependencies": [{…}]}}GET /knowledge/policies/<id>/releases 按最新在前列出历史;GET /knowledge/policies/<id>/releases/<releaseId> 返回单个版本——被替代或已撤回的版本只能在此作为授权历史证据读取。撤回只对当前版本有效:POST /knowledge/policies/<id>/releases/<releaseId>/withdraw 对已撤回或已非当前的版本返回 409 release_not_current,并在同一事务中移除当前版本指针。
冲突会被明确报告。当 base_version 与草稿存储版本不一致时,发布返回 409 {"service":"atlas-knowledge","error":"version_conflict"} 且零部分写入;两个并发的发布会通过版本表的唯一约束串行化,失败方收到同样的 409 且什么都没有写入。条目读取(GET /knowledge/policies/<id>、GET /knowledge/workflows/<id>)新增可加性字段 current_release 摘要,从未发布时为 null;旧数据的 Published 标签并不等于发布版本。删除带当前版本的资产时,该版本会在同一事务中被撤回。读取面向所有角色;发布与撤回要求知识写权限(管理员与经理);所有路由均按组织隔离,外部组织的 ID 一律返回无法区分的 404。所有版本发布路由均拒绝委托令牌。
商业料号
报价行与成交记录各自携带两个可选的自由文本标识:customer_part_number(客户自己的产品/料号)和 internal_part_number(我方内部料号)。每个标识最多 200 个字符;传 null 清空、缺省保留原值、空白输入存为 null。报价标识通过 POST /crm/engagements/:id/quotations 与 PATCH /crm/engagements/:id/quotations/:id 的 line_items 条目写入,并在每次报价读取(含修订复制)中返回。成交标识由 POST /crm/customers/:id/transactions 与 PATCH /crm/transactions/:id 接受,并在每次成交读取中返回。标识按记录独立保存,不做唯一性约束,因此同一客户料号可在不同记录上对应不同内部料号;标识不影响金额、合计或收款状态,既有记录在被编辑前保持不变。
付款条款与已收金额
成交记录新增两个可选字段用于记录约定条款:payment_terms——原样保存的自由文本(如“定金30%,发货前付清”“月结30天”),最多 200 个字符,传 null 清空、缺省保留原值、空白输入存为 null;received_amount_cents——由人员显式确认、按该记录币种记录的已收金额。两者均由 POST /crm/customers/:id/transactions 与 PATCH /crm/transactions/:id 接受,并在每次成交读取中返回。已收金额必须为 0 到成交金额之间的整数;金额 ≤ 0(退款)时只允许 null 或 0,违规返回 400 invalid_received_amount 且不写入任何数据(条款文本非法返回 invalid_payment_terms)。记录条款或已收金额绝不改变收款状态:状态只能通过显式编辑变更,写有“月结30天”而无已收金额的记录保持待收款。销售摘要中按币种的未收合计按剩余余额计算(amount − received,下限为零);成交合计与其他口径不变。无新字段的既有记录展示保持原样,已确认的应收观察仍是带日期的跟进依据。
应收跟进
应收事项(receivable case)是同一笔来源欠款的稳定标识——来源是一笔已记录的交易或一份已上传的发票文档。其下的带日期观察记录销售确认的仍未收回信息:以十进制字符串表示、单一币种的剩余余额,到期日或明确的未知,以及跟进状态(open 待收、partial 部分结清、settled 已结清、disputed 有争议)。每次确认追加新版本并保留全部历史;已结清要求余额为零,待收/部分结清要求余额为正。此处仅记录跟进依据:不记账,也不修改来源交易或发票。
会话路由:GET /crm/customers/:id/engagement/receivables 列出各事项及其最新观察;GET /crm/customers/:id/engagement/receivables/:caseId 返回单个事项及全部历史(最新在前);POST /crm/customers/:id/engagement/receivables 开立事项并确认首条观察({source_kind, source_id, remaining_balance, currency, due_on, state});POST /crm/customers/:id/engagement/receivables/:caseId/observations 确认下一条观察并要求 expected_revision——不匹配返回 409 stale_revision,不写入任何数据。校验拒绝(invalid_balance、invalid_state、currency_mismatch、来源跨客户或不存在时返回 invalid_source、不存在的日历日期返回 invalid_due_on、未来时间戳返回 invalid_observed_at)一律 400,且不改动既有观察。token 调用方通过受治理操作 crm.engagement.receivable.list、crm.engagement.receivable.read 与两步确认的 crm.engagement.receivable.confirm 使用同一能力。使用 DELETE /crm/transactions/:tid 删除来源交易时,会在同一操作中关闭对应事项:追加一条余额为零的 settled 观察,备注为 transaction deleted,历史观察保持不变。该删除在存在关联事项时返回 {"deleted": true, "receivable_case_closed": true},否则 receivable_case_closed: false,并同时删除该交易自身的时间线记录。
文件识别记账——从文档记录一笔交易
销售通过文档标签页的“记录交易 ▸ 从文件”(或助手已持有的一份就绪文件)交出一张发票、采购单或收据;Atlas 只读一次,并提出一份交易提案,每个值都引用到具体摘录——文档未声明的币种对销售留空,绝不猜测为人民币。在销售确认之前不写入任何数据。
会话路由:POST /crm/customers/:id/documents 新增多部分字段 propose("1" 表示读取本次上传——不带该字段的普通上传只存储、不读取)与 engagement_id(归档到的商机,在该客户下校验);可读格式(pdf/jpg/jpeg/png/webp)且不超过 10 MiB 时,返回的 proposal.state 变为 reading,其余情况立即变为 unsupported。POST /crm/customers/:id/documents/from-artifact 将一个已就绪的 intake 附件——{artifact_id, propose?, engagement_id?, kind?, note?}——归档为该客户的文档,而不必重新上传,适用于文件已存在于另一个接入入口的情形(例如助手的文件识别交接);对同一附件、同一客户的重复调用返回 200 并给出已归档的文档而非重复项,409 artifact_already_assigned 表示归属于另一个客户或上下文,404 source_unavailable 表示附件缺失或尚未就绪。
GET /crm/documents/:id/proposal 返回 {proposal}——文档从未被提案时为 null,否则返回完整提案:关联信息(客户、商机、类型)、已在档匹配到的值、交易各字段(各自带来源——doc 引用摘录、atlas 来自 Atlas 自身记录推断、you 留给销售——以及说明与标记)、联系人更新、事实与建议动作的其余摘录,以及标记指向的编号阅读块。POST /crm/documents/:id/proposal/retry 重新读取卡在 failed/unreadable/unsupported 的文档(202 {state:"reading"};已在读取中返回 409)。POST /crm/documents/:id/proposal/accept 接收销售确认后的提交体,是本功能唯一的写入:一笔交易(携带 source_document_id)、已确认的事实、联系人字段更新(联系人邮箱为空时才写入,绝不覆盖——不同的引用邮箱改存为一条事实)、任务,以及文档的商机/备注——一次调用,按组织隔离;提案尚未 ready 时返回 409 proposal_not_ready,文档已关联交易后返回 409 document_already_recorded(一份文档最多关联一笔交易)。POST /crm/documents/:id/proposal/dismiss 返回 {state:"dismissed"},不写入任何数据。
GET /crm/customers/:id/documents 各行新增 engagement_id、proposal_state(从未提案的普通上传为 null)与 recorded_transaction({id, amount_cents, currency} 或 null)。GET /crm/customers/:id/transactions 各行新增 source_document({id, filename} 或 null);POST /crm/customers/:id/transactions 接受 source_document_id(文档不属于该客户时返回 404,已关联交易时返回 409 document_already_recorded)。
每个客户文件都必须回答属于哪个业务(#4266)。customer_documents.engagement_id 为 NULL 时既可能表示“归在客户层级”,也可能表示“从未有人问过”,因此迁移 0115_document_deal_answer.sql 新增 deal_answer(deal | not_a_deal | pre_atlas,NULL 表示尚未回答)与 pre_atlas_ref(选择 pre_atlas 时可填的参考号,最多 80 个字符)。既有数据不做回填:有商机而无答案的行读作业务,两者皆无的行读作“待回答”,系统绝不自动指派。写入方保证 deal_answer = 'deal' 当且仅当 engagement_id 有值。
三个入口写入该答案。POST /crm/customers/:id/documents 新增多部分字段 deal_answer 与 pre_atlas_ref;既未给出答案也未给出 engagement_id 的上传按“未回答”存储,绝不猜测。POST /crm/documents/:id/proposal/accept 现在要求请求体必须包含 deal_answer(答案为 deal 时须带 opportunity_id,pre_atlas_ref 选填);同时会把仍存为 other 的文档类型更正为识别结果的类型(po/invoice/quote),但人工选定的类型绝不覆盖。提交体还可携带 document_kind(文档类型之一:quote、contract、spec、po、sample、invoice、other),即审核人在“这是什么”下选定的类型;它会写入文档类型,若由此成为开放业务上的采购订单,则与其他采购订单一样触发赢单核实。未知值返回 400 invalid_document_kind。新增 PATCH /crm/documents/:id/deal-answer,接受 {deal_answer, engagement_id?, pre_atlas_ref?},返回 200 {document}——这是规则生效前上传的旧文件唯一的回答途径。三个入口的拒绝方式一致:未给出答案返回 400 deal_answer_required,答案取值非法返回 400 invalid_deal_answer,答案为 deal 但缺少商机返回 400 engagement_required,商机属于其他客户或其他组织返回 400 invalid_engagement,调用方无权查看的文档返回 404 not_found。GET /crm/customers/:id/documents 与 GET /crm/documents/mine 各行新增 deal_answer 与 pre_atlas_ref,客户文件列表另增 engagement_title,该连接同时匹配文档自身的客户,任何一行都无法借用其他客户的业务名称。
#4276 —— 符合自动读取条件的上传文件在归档前会检查客户与业务,文件也可以两者都不属于。POST /crm/customers/:id/documents/from-artifact 现在同样接受业务答案字段 deal_answer、engagement_id 与 pre_atlas_ref,校验规则与多部分上传完全一致:以答案为准,因此与 not_a_deal 一同发送的商机 id 不会被存储;只发 engagement_id 仍表示 deal;完全不带答案的归档存为“未回答”,而不是默认某个答案——助手是先归档、随后在卡片上询问。商机属于其他客户或其他组织返回 400 invalid_engagement,且在附件归档之前就拒绝,因此不会留下任何文档行。
助手的文件移交不再拒绝一次拖入多个文件。拖入一个或多个文件时,每个文件在发送的同时就会存入该成员的「我的文件」——与「知识库 › 我的文件」相同的个人范围 POST /knowledge/files,使用该成员本人的会话——并为其中符合自动读取条件的文件创建一个审核批次,回复只有两行加一个「查看 N 份文件」按钮,点击进入审核页面;对话中不再提问。超过 50 MiB 的文件仅保存在「我的文件」,不会创建自动审核任务;未处理状态不表示读取失败。符合条件文件的轻量阅读在回复之后进行,由持久化任务异步、可续跑地执行,而不是在回复内完成,因此按钮会在几秒内出现,而 60–90 秒的阅读仍在排队;审核页面显示每份文件的阅读状态,阅读失败或始终未完成的文件仍可完整审核,所有文档字段均留给业务员填写。
报价单阅读结果(#4591)。 新读取的提案还带有可选的 document_fields:occurred_at、reference、amount、currency 和 payment_terms,各字段与交易字段一样包含值、来源、置信度、引用证据、说明与标记。报价单类型仍为 opportunity,且 transaction: null;读取这些值不会创建交易或推进商机阶段。文件审核的 GET /crm/document-review/batches/:id 优先使用这些通用阅读结果;缺少新属性时,回退到旧提案的 transaction.fields。只有引用有效且无歧义的值才显示;引用无法解析或文档未写明的值保持空白。报价单的 status 与 received_amount 保持空白。缺少这些阅读结果的旧报价单提案需要重新读取;本次变更不会回填旧提案,也不会保存审核表单中输入的修改。
上传关联检查
每个上传入口背后的轻量阅读(#4276)。POST /agent/documents/association-check 接受 {source: {kind, id}, customer_id?},其中 kind 为 intake_artifact 或 library_file,返回 200 {read, customer, deal}。它只提出建议,从不替人决定:调用本身不写入任何数据——不建客户、不建文档、不建关联——调用方把结果渲染为预填并标注“已读”的字段,由人按“上传”“确认”或“保存”确认。
read 包含 party_name、party_role、kind_hint、document_kind、total_hint、delivery_date、line_count 与 customer_type_hint——只有结构化字段,绝不返回模型正文。customer.match 取值为 exact、close、none 或 no_company:no_company 表示成功读取了一份未写明公司的文件(即“与客户无关”的情形),而 none 表示文件写明的公司不是现有客户。customer.candidate 与 customer.target 分别给出匹配到的客户与调用方所问的客户。deal 包含 candidates——仅该一个客户的进行中业务,文件写明的排在最前——以及 proposed_id 与 how;文件写明两个业务时属于提问而非猜测,proposed_id 为 null,how 为 names two deals。
组织只来自已验证会话,其他来源一概不认;每个 id 都通过转发调用方自身会话的绑定解析。拒绝方式:未登录 401;source 不是上述两种返回 400 invalid_request;调用方无权读取的附件、知识库文件或客户返回 404 not_found——与 id 不存在的回答完全一致;文件没有可读文本返回 422 unreadable;受治理的阅读无法执行返回 503 read_unavailable,绝不以“未写明公司”作答,否则界面会把它显示成关于该文件的事实。
library_file 的存储状态为 extracting 或 failed 时,也会在调用模型前返回 503 read_unavailable。尚未完成或失败的提取不会产生确定的关联答案。未存储状态的旧文件保留现有行为。
知识库文件的客户与业务
记录知识库文件的客户与业务,但不移动文件(#4286)。在知识库行内保存客户后,文件仍留在知识库,并在该客户档案的“文件”页“来自知识库”分区中显示——一个文件,一处存放,两种视角。答案存放在由 CRM 拥有的关联行中,以 EKB 文件的稳定显示 id 为键;不建立外键,因为生产环境的 EKB 是独立数据库(ADR 06);也不存储文件名、扩展名与大小,这些在读取时通过 EKB 服务绑定解析。
PUT /crm/library-files/:ekbFileId/association 接受 {state, customer_id?, deal_answer?, engagement_id?, pre_atlas_ref?, read?},返回 200 {link}。返回的 link 携带联结出的 customer_name 与 engagement_title——仅为本次响应解析,绝不存储——因此保存后立即渲染的行显示名称而非 id;not_customer_related 答案两者均为 null。state 为 customer 或 not_customer_related;not_customer_related 本身就是完整答案,无论请求里还带了什么,都不携带客户与业务,并且会被记住,该行不再重复询问。文件先经 EKB 解析,因此调用方无权查看的文件返回 404 not_found 且不会产生任何答案;EKB 不可用返回 503 upstream_unavailable 且不写入。业务答案的校验与其他入口完全一致:400 invalid_state、400 customer_required、400 deal_answer_required、400 invalid_deal_answer、400 engagement_required,商机属于其他客户或其他组织返回 400 invalid_engagement。再次保存会替换原答案——每个文件只有一行,不保留历史。
GET /crm/library-files/associations?ids=a,b,c 返回 200 {items},即本组织对这些 id 已有的答案,单次最多 100 个;若本组织对这些 id 都没有答案,返回 200 {items: []}。GET /crm/customers/:id/library-files 返回 200 {items},每项为 {ekb_file_id, name, ext, size_bytes, deal_answer, engagement_id, engagement_title, pre_atlas_ref, decided_at}。该成员已无权查看的文件会被剔除,而不是列出名称;调用方无权查看的客户返回 404——所有 /crm/customers/:id/** 路由都要经过同一道可见性闸门,这是唯一完全不泄露信息的答复。
迁移 0126_library_file_associations.sql 创建 customer_library_file_links,可增量且可重复执行。
只有全部请求批次成功后,关联查询才能证明没有已存关联。浏览器每批最多查询 100 个 ID 并覆盖全部所选文件;任一批失败时整个读取保持不可用。数据库或权限查询失败返回 503 association_read_unavailable,不会伪装成成功的空结果。确实为空或无权访问的集合仍返回 200 {items: []}。关联保存响应从原子写入结果返回有权查看的 customer_name 与 engagement_title,不泄露调用者权限外的名称。
邮件附件(STORY-EMAIL-FILES)。在收件箱中把收到的邮件关联到客户(POST /crm/inbox/links/confirm、POST /crm/inbox/links/confirm-exact,或对已由人关联的会话中回复的自动关联)后,邮件里的每个真实文档都会以草稿形式存入执行关联的成员的“我的文档”,并为该客户写入 customer_library_file_links 记录。请求本身只把邮件加入队列,由 CRM 定时任务在几分钟内完成存档。带 Content-ID 的内嵌图片、小于 30 KB 的图片、签名(.p7s)、日程邀请(.ics)、winmail.dat、转发的邮件(.eml)、资料库不接受的类型以及超过 10 MB 的部分都会被跳过。已有答案的文件保留原答案。清除邮件的关联或把它改关联到其他客户时,文件会从原客户撤下(只删除存档本身写入的关联,绝不删除人设置的答案),并为新客户存档。每个组织每天最多存档 200 个文件,其余顺延到第二天。以此方式存档的 GET /crm/customers/:id/library-files 行带有 from_email: true、comm_item_id(所来自的邮件)与 download_path(与下文相同的邮件副本下载路径,所有者同样可用)。既能查看该客户、又能读取该邮件所在邮箱(与其他客户往来视图相同的邮箱授权)、且其角色可读取资料库的人,都能看到这类文件,即使它位于另一位成员的“我的文档”中;对他们而言,该行还带有 content_path,GET /crm/customers/:id/library-files/:ekbFileId/content 会下载邮件中的副本(200,始终为 Content-Disposition: attachment;不是为此客户从邮件存档的文件或已被所有者删除的文件返回 404 not_found;无法询问 EKB 时返回 503 upstream_unavailable)。迁移 0151_comm_attachment_filings.sql 创建 comm_attachment_filings 队列,可增量且可重复执行。 在文件内容路径后加 ?inline=1 可在浏览器中直接显示而不是下载;仅当文件本身的字节是 PDF 或 PNG、JPEG、WEBP、GIF 图片时才会这样处理,其他文件仍以下载方式提供。
订单交期跟进
订单交期事项(order-delivery case)是单个订单交期跟进的稳定标识,锚定于同一客户下的一份采购订单或合同文档(po 或 contract)作为佐证单据。其下的带日期观察记录销售确认的交期信息:交期结果(on_track 按计划、at_risk 有风险、delayed 已延迟、delivered 已交付)、预计交期日或明确的未知、最多五条佐证单据引用,以及不超过 500 字符的可选备注。每次确认追加新版本并保留全部历史。此处仅记录跟进依据:不履行、不重排、不派发任何订单,也绝不写入 ERP 记录。delivered 观察只停止本事项的跟进——其锚定的计划以 case_delivered 为由取消。交期未知只会产生数据缺口问题,绝不虚构紧迫性;预计交期变化会将已接受计划的到期依据标记为过期(原因 source_changed),直到人工重新接受。每条路由都按组织隔离,跨客户的单据一律拒绝。
会话路由:GET /crm/customers/:id/engagement/order-deliveries 列出各事项的 order_reference、source_document_id、source_label、最新观察与观察次数;GET /crm/customers/:id/engagement/order-deliveries/:caseId 返回单个事项及全部历史(最新观察在前);POST /crm/customers/:id/engagement/order-deliveries 开立事项并确认首条观察({order_reference, source_document_id, timing_result, expected_on}),或对既有事项追加第 N+1 版(case_id 加 expected_revision——不匹配返回 409 stale_revision,不写入任何数据);POST /crm/customers/:id/engagement/order-deliveries/:caseId/observations 确认该事项的下一条观察。已交付事项拒绝继续观察,返回 409 case_delivered。校验拒绝(invalid_timing_result、不存在的日历日期返回 invalid_expected_on、超过五条引用或引用不属于本客户返回 invalid_source_refs、invalid_order_reference、来源跨客户或不存在返回 invalid_source)一律 400,且不改动既有观察。token 调用方通过受治理操作 crm.engagement.order-delivery.list、crm.engagement.order-delivery.read 与两步确认的 crm.engagement.order-delivery.confirm 使用同一能力。
样品测试结果
样品测试案例是一件样品的稳定标识:由客户关联的样品文件加一个明确的样品标识派生出唯一的 sample_key。案例之下,按日期记录的观察承载业务确认的答复:结果(pass 通过、fail 未通过、inconclusive 结论不明、retest_required 需重测)、确认日期、证据引用和人工反馈。每次确认追加一个新版本并保留全部历史。最终的 pass/fail 将关闭该案例,并仅停止该样品自身的样品测试跟进计划;无关的客户活动和另一项选型决定都不会解决它,retest_required 的结果只提议一份新计划供人工接受——绝不自动创建。
会话路由:GET /crm/customers/:id/engagement/sample-test 列出案例及其最新观察;GET /crm/customers/:id/engagement/sample-test/:caseId 返回单个案例及其完整历史(新版本在前);POST /crm/customers/:id/engagement/sample-test 开案例并确认第一次观察({document_id, sample_label, result});POST /crm/customers/:id/engagement/sample-test/:caseId/observations 确认下一次观察并要求 expected_revision——不匹配返回 409 stale_revision,不写入任何内容。校验拒绝(invalid_document 跨客户、已删除或非样品文件,invalid_result,invalid_observed_at 未来时间,invalid_expected_revision,case_not_found)返回 400,且绝不会关闭跟进。令牌可通过受治理动作 crm.engagement.sample-test.read 与两步式 crm.engagement.sample-test.confirm 使用同一界面。
年度合作依据
GET /crm/customers/:id/engagement/annual-evidence?year=YYYY 返回单个客户一个自然年(UTC 边界,见 timezone)的可复核年度合作依据:transaction_groups——按状态 × 币种分列的组,含 count 与 amount_minor,按交易 id 去重(同一发票编号下的两笔不同付款都计入);paid_minor_by_currency/outstanding_minor_by_currency 不含取消金额、绝不跨币种合并;该年度的 activity_timeline(最多 50 条,activity_total 为真实总数,超出时 coverage.timeline 为 partial);以及 outstanding_as_of——来自最新确认观察的应收跟进余额,按确认时点单列,绝不计入交易合计。该读取确定性且无写入:依据不变则 digest 相同。token 通过受治理读取 crm.engagement.annual-evidence.read 使用同一能力。
治理依赖健康检查
GET /governance/health 是无需登录的只读运维探测,返回 HTTP 200、标准 Governance 构建字段及 memory: {reachable, ready, mode, error}。请检查 status:Memory 无法确认存储与信任就绪时,状态为 degraded。请求与响应体读取共用两秒期限,响应体最多 64 KiB(按字节计算)。
可选的 memory.identity 仅包含绑定 Memory 的 /health 响应中的 {commit,ref,environment}:上游 service 必须为 atlas-memory,commit 必须恰好是 40 个小写十六进制字符,ref 只能是 main 或 staging,environment 只能是 development、staging 或 production。任一身份字段缺失或格式无效时,整组身份字段均省略。有效但与预期不同的 ref/environment 会保留,不会用 Governance 的身份替代。
验收内部 Memory 发布时,应将这三个身份值与已记录的目标 Memory 构建逐一比较,并单独检查 memory.ready。缺少身份字段时仍可能就绪,但不能据此证明发布身份匹配。有效的 Memory 未就绪 HTTP 503 响应可以保留身份;响应格式错误、超时或其他上游失败均不能提供身份。Memory 仍只通过私有 Service Binding 访问,不新增公共 Memory 路由。
失败类别仍为 binding_missing、probe_timeout、probe_failed 或 memory_not_ready。可选的 detail 仅包含已知信任错误类别,不透传任意上游诊断文本、调用方记录、密钥标识及密钥。/healthz 仍为独立的存活探测。
当前已发布版本
使用会话登录的成员可通过 GET /knowledge/releases?kind=policy|workflow&q=…&limit=1..50&cursor=… 列表或词法检索组织当前已发布的制度与工作流版本;GET /knowledge/releases/<releaseId> 返回一个当前版本。
每个条目为 {citation,release}。引用包含带所有者的 policy/procedure reference、不可变资产标题和稳定的章节或步骤定位符。列表和单条读取的 locator 为 null,quote 为空;检索会引用发布快照中匹配的标题、标题栏、正文或步骤,最多 200 个 Unicode 码点。已撤回、被替代、跨组织和未知版本 ID 都返回 404 not_found;无效参数、kind、查询、limit 或 cursor 返回 400 invalid_query;存储不可用返回 503 storage_unavailable。该 REST 会话读取不对委托 token、MCP、CLI 或 agent adapter 开放。
沙盒运营知识范围
登录成员可在 POST /knowledge/sandbox/ask 中添加 operational_kinds。省略或传入 [] 时保持纯文档行为。非空值仅接受 ["policy"]、["procedure"]、["policy","procedure"]。现有全部、文件夹、所选文件 scope 始终限制文档范围;运营知识不会伪装为文件 ID。
{"question":"报价条件","scope":{"kind":"all"},"operational_kinds":["policy","procedure"]}仅使用当前可回答的版本,每类最多选取最新 25 个,总计最多 50 个。新增的 operational 包含 requested_kinds、release_count、capped_kinds 和 ceiling_per_kind:25,明确标示受限范围。release_citations 保留不可变标题、准确版本、章节或步骤及原文引用;文档引用保持原样,两类合计最多三条。答案仍为确定性原文摘录,不是自动生成的合规判断。
非法类型返回 400 invalid_request。最后检查发现版本变化时返回 409 source_invalidated,不附答案,可重新提问。存储不可用或权威数据损坏返回 503 storage_unavailable,不能伪装为空结果。沙盒显示所选类型,恢复运营知识引用时重新检查,并在跳转来源前再次验证。
尚未生效的制度规则
仅拥有知识管理写入权限的普通租户会话经理或管理员可为 tasks.profile_gap_task.create 的 "1" 版本编写规则。平台管理(包括角色预览)、委派令牌和断言凭证均不可管理规则,即使同时提供 Cookie。规则不可执行;预览不创建任务、不授予权限、不生成综合决策。
PUT /knowledge/policies/:id/rules 接受 {base_version,rules:{schemaVersion:1,rules:[…]}},对草稿版本进行原子比较后替换整组规则,返回 {item:PolicyDetail}。每条规则为 {id,action,action_version,condition:{field,op,value?},outcome,section_id}:新建 prule_<UUID> 并在编辑时保留,使用上述唯一操作及版本,结果为 warn|require-confirmation|block,关联已保存的章节 ID。详情新增 rules、与章节顺序一致的 section_ids 和服务器计算的 can_manage_rules;普通 PATCH 不能修改规则。
支持字段 customer_id,profile_gap_code,assignee_email,suggestion_id,action_id,current_role,due_at,均支持 eq,ne,in,exists;仅截止时间支持 lt,lte,gt,gte。存在条件不带值;多值条件需要 1–20 个不重复且类型正确的标量,其余条件使用一个标量。字符串精确比较;客户 ID 不得为空,负责人和建议字符串允许为空;缺项代码和角色遵循操作目录及租户角色表。截止时间为 0 至 253402300799 的整数 Unix 秒。拒绝未知字段、额外键和不支持的版本;最多 20 条规则、32 KiB,允许空规则集。
POST /knowledge/policies/:id/rules/preview 接受 {base_version,cases:[{id,args}]},需要 1–10 个唯一示例 ID,请求体不超过 32 KiB。参数仅允许任务的六个键 customer_id,profile_gap_code,title,due_at,assignee_email,suggestion_id。为测试未知状态,允许错误类型,不做转换或补默认值。服务器提供 action_id 和 current_role,客户端不得传入;标题不参与判断。响应为 {preview_only:true,base_version,results:[{case_id,rules:[{rule_id,result,reason_code,section_id}]}]}。结果 match|non_match|unknown,原因为 matched|not_matched|missing_value|invalid_type。缺失、null 或错误类型在所有条件(包括不等于和存在)下均为未知。客户等于 customer-a 的条件:customer-a 匹配,customer-b 不匹配,缺失则未知。
发布时校验章节关联,将规范化规则与文字和摘要固定在不可变版本中。制度版本摘要新增 has_rules,内容新增 rules。恢复保留来源字节、创建新版本,不改草稿;旧版缺少规则时视为空,损坏快照或失效章节关联则拒绝。当所选来源、草稿或当前版本任一包含规则时,发布、恢复、撤回和删除均要求规则管理权限。删除与发布原子比较草稿版本和当前版本指针;无规则的旧流程权限保持原样。
错误:400 invalid_request、401 unauthorized、403 forbidden、404 not_found(外部组织与不存在的 ID 相同)、409 version_conflict、503 storage_unavailable。版本冲突请重新加载并核对修改,存储故障恢复后再试。这些接口不增加 Agent、Task、MCP、CLI 或实际执行能力。
待审核草稿的有效性
已登录应用使用的 GET /agent/prepared 会在返回前检查草稿是否仍然有效,包括未指定筛选条件的请求。关联客户已删除或关联优先事项已处理的草稿会被标记过期并从列表中移除。组织范围和决策来源标识保持不变。若同一组织的同一客户已有待审核草稿,定时扫描不会再插入草稿,包括并发扫描已提交的草稿。定时扫描日志保留经过格式校验的组织标识用于运维关联,不输出无效标识或业务原文。
客户公海
组织可以将客户放入公海,使其从现任负责人的名下移出,同时保留完整历史。POST /customer-pool/:id/release(现任有效负责人、管理者或管理员)将该行标记为 ownership_state=pooled,在 owner_email 与 previous_owner_email 中保留原负责人,并记录原因。POST /customer-pool/:id/claim 允许任何调用者为自己认领公海客户;POST /customer-pool/:id/assign 仅限管理者/管理员,并需指定 assignee_email。认领与分配会将客户重置为 assigned 并归属新负责人。
每次变更都必须携带 expected_revision 与 request_id。过期的 expected_revision 返回 409 revision_conflict,不产生任何部分写入。对同一客户重复相同的 request_id 会返回已记录的结果(200 且 replayed:true),绝不写入第二条审计记录。每次生效都会在流转审计中写入一条不可变记录(release、claim、assign 或 patch_assign)——不存在任何更新或删除路径。
GET /customer-pool 对所有角色开放,仅以受限投影列出本组织的公海客户:id、name、country、industry、previous_owner_email、pooled_at、pool_reason 以及并发控制字段 ownership_revision(认领所需的乐观锁守卫)。可选的 q 参数(不超过 100 个字符)仅对这些字段做不区分大小写的匹配,在 SQL 中、分页之前生效,并同时决定 items 与 total;分页使用 limit(1–100,默认 25)与 offset。GET /customer-pool/:id 返回同一投影,并附上来自 CRM 自身记录的 open_engagements 与 open_quotations 计数;由于任务支柱未提供本支柱可调用的按客户计数接口,open_tasks 恒为 null。
公海中的客户会从原负责人的名册以及该销售可见的每一个客户维度读取中消失——响应与不存在的 id 完全相同的 404。管理者与管理员的组织级可见性不变。流转从不会重新分配独立的业务、报价与任务。每一项新的所有权生效——认领、分配,以及更改 owner_email 的 PATCH /customers/:id——都会在写入前立即经过 Auth 的实时成员资格检查;绑定缺失、调用失败或成员非活跃都会以失败关闭方式拒绝。PATCH 更换负责人仍需 Auth 签发的资格证明,但证明永远不能替代实时检查。销售 PATCH 会在事务实际生效时复核客户仍为该销售的已分配客户;同时修改字段和负责人的 PATCH 会将客户字段、去规范化记录和不可变流转审计放入同一受版本保护的事务。竞争失败时销售收到 404,管理者收到 409 revision_conflict,不会留下部分写入。同负责人的 PATCH 与客户创建行为保持不变。
客户公海策略与自动回收
每个组织可以自行配置:分配到销售名下的客户在何种条件下自动掉入客户公海。GET /customer-pool/policy(仅限管理员)返回当前策略:在管理员启用之前为 enabled:false,默认 inactivity_days:60、warning_days:7、assignment_grace_days:60。设置均有边界——不活跃与分配宽限为 1–3650 天,警告为 1–30 天且必须小于不活跃天数——且每个组织的策略相互独立。
POST /customer-pool/policy/preview(管理员)在不做任何变更的前提下模拟提议的设置:返回 qualifying_count(已过不活跃阈值且未被保护的已分配客户)、excluded 计数(protected_count、recent_contact_count)、每个客户的 open_engagements / open_quotations 计数、各自的 warning_due_at 与 projected_release_at,以及当前 policy_revision。只有规范的人工记录的客户沟通——人工记录的通话与会面、人工确认的来信、经人审批的发送——才会推迟截止日期;编辑、内部备注、上传、任务完成以及无人参与的自动活动永远不会推迟。
POST /customer-pool/policy/activate(管理员)保存设置并启用自动回收。它必须携带 expected_revision——即预览返回的版本号——过期的值会返回 409 policy_revision_conflict,因此两名管理员不可能基于不同的假设启用策略。停用使用 POST /customer-pool/policy/deactivate,附带同样的版本守卫;停用的策略不会执行任何自动动作。启用会重新锚定激活安全宽限:自激活起经过完整的警告周期之前,没有任何客户会进入可回收状态,因此历史导入的老客户绝不会立即被回收。
管理者或管理员可以通过 POST /customer-pool/protections(customer_id、至多 365 天后的 protected_until、5–500 字的 reason)将某个客户排除在自动回收之外,并通过 POST /customer-pool/protections/:customer_id/remove 解除保护。两次变更各写入一条审计时间线记录。GET /customer-pool/protections 列出当前有效的保护。
策略启用后,当客户到达警告截止点时,会通过任务支柱向负责人以及每一位管理者和管理员发送一条应用内警告(绝不发送任何外部消息)。警告按所有权版本与截止日期去重并重试直至送达;随后回收会等到所有条件全部满足——不活跃、分配宽限(以最后一次分配与激活时间中较晚者锚定)、激活安全宽限、无有效保护、警告已送达且自送达起经过完整的警告周期。自动回收在生效前以原子方式复核当前沟通记录、所有权版本、策略、保护和警告,因此发生冲突的变更不会产生流转或审计效果。受限的扫描保留持久化的组织和客户游标并公平地循环,避免早期繁忙页面使后续符合条件的记录长期得不到处理。它在不可变流转审计中记录策略版本与原因;停用的策略不执行任何流转。
客户归属批量交接
批量交接让组织所有者一次把多个客户移交给同一位接手人。POST /customer-handover/preview 接收 assignee_email 和 1–100 个客户 id,按客户返回现任与拟任负责人、归属状态与版本号、允许查看的在途工作计数(来自 CRM 自身记录的在途业务与报价;open_tasks 恒为 null),以及接手人的实时资格答案和所需的原因长度限制。预览绝不写入:不改归属、不动版本号、不留审计行。缺失的 id 会以 missing:true 明确报告——既不会悄悄丢弃,也不会虚构。
POST /customer-handover/confirm 接收 assignee_email、5–500 字符的 reason,以及 1–100 条 {id, expected_revision, request_id}。两个路由仅限组织所有者使用;管理者和管理员仍使用 #2363 的单个公海客户分配。确认会按客户逐个套用与单个 PATCH 换负责人相同的原子化带版本流转,并始终逐客户给出结果:transferred(附结果版本与状态)、conflict(revision_conflict、ineligible_recipient 或 already_owned_by_recipient)或 failed(not_found)。一个客户的拒绝从不会撤销无关客户的成功。每次生效前都会对接手人做实时资格检查,批次中途资格变化只会影响其后的客户。重复提交相同 request_id 会先返回已记录的结果,再谈任何拒绝——部分完成的批次可以安全重发而不会重复转移;未解决的客户则会重新执行。公海客户可直接交接——成交接即重置为 assigned 并清除公海状态。每次生效写入同一条不可变审计行(操作人、原因、原与现归属);独立的业务、报价与任务从不会随之转移,客户上原负责人的 AI 权限随归属一并撤销,绝不由接手人继承。
交接接手人建议
POST /agent/handover/recipients 仅向组织所有者开放(会话的 owner 标志;其他任何主体都会得到 403 owner_only 且无数据)。请求体携带 customers:1–100 条含 id 的客户;旧客户端可额外携带展示字段,但服务端会忽略它们。智能体从 Auth 获取实时有效成员资格,并通过所有者已获授权的 CRM 连续性路由重新读取每位客户的名称、当前负责人、记录作者及时间戳,同时转发调用者 Cookie 与角色预览。它将该成员名单与 CRM 已记录的工作量相交,只有选中客户的关系记录和工作量证据都存在时,才最多推荐三名成员。私有或外部工作量缺失时保持未知,绝不按零处理。不存在或跨组织客户均返回相同的 404 customer_not_found;Auth 或 CRM 证据不可用时返回 503,且不会调用模型。建议仅供参考:它们不会变更归属,交接预览与确认无论如何都会重新执行实时资格校验。响应会保留可读的 gaps、rationale 以及证据的 label/value 字段,并可额外提供用于本地化渲染的 gap_codes、rationale_code 和每条证据的 code、count 或 at。客户端只翻译已知代码;遇到未知代码必须显示其附带的可读字段。
交接完成
在负责人的批量交接移动客户(POST /customer-handover/confirm)之后,每个被交接的客户在拥有合格的下一步之前保持待完成状态。本节所有接口仅向组织所有者开放(会话的 owner 标志,而非租户角色);其他任何人都会得到 403 owner_only 且无任何数据,组织始终取自已验证的会话。
GET /customer-handover/completions 仅按已验证组织中当前有效的交接流转列出客户:请求、原负责人与新负责人、原因、时间、当前负责人,以及该流转对应的 pending 或 complete 记录(已记录的下一步、任务创建失败;完成时的操作者与时间)。历史收据保持不可变,只能携带明确的 handover_transition_id 重放;它们绝不会把后续交接标记为完成。没有当前合格交接的客户不出现在此列表中。
POST /customer-handover/next-action 为被交接客户记录下一步,并通过签名的 CRM→TASKS 服务绑定恰好创建一个受治理任务。请求体包含 customer_id、1–200 个字符的 title、明确的 epoch 秒 due_at、可选的 assignee_email(默认为客户当前负责人)以及 request_id。每次本地及任务效果前,已记录的交接流转必须仍与客户当前的 assigned 负责人和归属版本相符;没有当前审计交接的客户返回 409 no_current_handover,已被后续归属变更取代的待完成交接返回 409 handover_not_current。负责人必须在效果发生前通过实时的 Auth 成员资格检查(fail-closed:400 ineligible_assignee)。以相同 request_id 重试会先按绑定到交接的来源身份协调已接受或响应不明的创建,再返回该任务或安全重试效果;绝不重复创建任务,也不把失败视为最终结果。如果 TASKS 接受创建时归属已变更,CRM 保留持久化清理目标,并在本次或后续重试中仅取消刚创建的受治理任务;既有任务绝不变更。创建失败返回 502 task_create_failed,并作为 next_action.error 在交接记录上保持可见,直到重试成功。
POST /customer-handover/complete 仅在门槛通过时将单个客户的交接标记为完成,并在效果发生时实时重新验证:其持久化交接流转仍匹配客户当前的 assigned 负责人和归属版本、已记录的下一步仍然存在且仍指向该客户、其任务仍为 open、带有明确的到期日,且其负责人在写入前立即通过实时资格检查。已存在且调用者可见、关联该客户并符合条件的未完成任务可以被记录为下一步;不会创建重复任务,也绝不改变该任务的负责人或可见性。TASKS 未授权调用者读取的私有任务不会被选择或命名。每次拒绝都会指明缺失项(no_current_handover、handover_not_current、no_next_action、next_action_unavailable、task_customer_mismatch、task_not_open、missing_due_date、assignee_ineligible),交接保持待完成、所有权不受影响。完成记录按交接流转不可变:不带流转标识的请求只处理当前交接;授权的历史收据可携带 handover_transition_id 重放,并在后续交接之后仍返回 200 replayed:true。已完成的当前交接之后的下一步请求以 409 already_complete 拒绝。本节任何路由都不会重新分配既有任务或独立业务记录。
客户健康信号
成功的客户列表和详情响应包含 customer_health:healthy(健康)、at_risk(需关注)或 critical(紧急)。健康状态由最近联系、进行中业务和符合条件的逾期任务实时计算,不改变档案完整度状态。
私人任务不参与计算。仅未完成且已逾期、并且客户现任负责人是执行人或创建人的团队任务参与。负责人和管理者看到相同健康状态;他人的受限任务不会影响该状态。
任务信号无法核实时,读取返回 HTTP 503,错误码为 customer_health_unavailable,请稍后重试。不可用信号不会被当作零逾期任务。内部任务汇总接口不是公开 API。
沟通审核访问边界
GET /crm/communications/queue?view=pending|discarded 与 GET /crm/communications/pending 先按权限筛选,再应用数量上限。尚无匹配建议的记录会在返回列表前由共享匹配器生成建议——每条记录仅一次;扫描受限的记录保持「尚未匹配」,不会导致整个列表失败。业务员可分流未关联消息,并读取关联至其当前负责客户的消息。不可访问的关联消息不出现在列表中;空列表返回 200 {"items":[]}。view=discarded 按时间倒序列出本组织的丢弃决定,每条含 discarded_by、discarded_at 与 reason,适用同一边界;未知 view 返回 400 invalid_view。
消息详情、匹配、候选查询、确认、丢弃接口以及 GET /crm/communications/quarantine 使用相同的当前负责人边界。不可访问、跨组织及不存在的消息统一返回 404 item_not_found。重复确认在返回已保存结果前,重新校验原客户的当前访问权限,包括负责人变更后的情况;修改请求中的客户不能读取其他负责人的回执。本修复不增加权限或对外发送能力。
确认为仅审核模式:在事务内校验当前负责人后,一并提交审核决定、客户关联、一条客户时间线记录及最近联系时间更新和队列状态。系统绝不写入任何推导事实,请求体中遗留的 proposals 字段会被忽略。并发重试仅在当前有权限时返回先成功提交的原始结果,不能替换其客户。原 POST /communications/:id/extract 预览接口已移除,请求按通用回退应答,不会返回任何消息相关信息。丢弃操作也在同一负责人校验下,一并提交决定和队列状态。事务失败不会留下新决定或部分 CRM 变更。
旧版本留下的空结果确认,仅允许原操作人在当前仍有消息和客户访问权限时重试;已有的客户/联系人关联仍是依据,已关闭的消息拒绝再次匹配或关联。其他操作人不能通过替换请求内容完成该旧决定。
邮箱内容同时受该连接的具名账户授权约束,并在每次请求读取任何内容前重新校验。个人连接仅对其拥有者应答;共享连接仅对持有当前「读取/审核」授权及 CRM 读取权限的调用者应答——仅发送授权不等于读取权限。设置支持人员永远无法读取邮箱内容,无授权的管理员同样无权;撤销授权后,下一次详情、列表、匹配和确认调用即被拒绝;没有账户身份记录的连接对任何人都不可读。当授权服务不可用时边界失效关闭:列表返回筛空结果,消息与未知 id 一样返回 404 item_not_found。列表筛选在数量上限之前的 SQL 层生效,因此计数、分页和匹配候选永远不会暴露隐藏消息;已确认消息的源副本同样保持私密。任何响应都不会出现附件存储键或 URL,也不存在公开的附件下载路由。
GET /crm/communications/:id 的消息记录现额外携带 body_text:来自对存储的原始 rfc822 消息同一次有界解析得到的正文——发布流程解析附件清单时已经运行过这次解析,一次 R2 读取同时服务两者,绝不携带邮箱侧存储键。该字段上限 20,000 字符;当附件缺失、无法读取或解析结果为空文本时为 null——绝不会悄悄回填 summary 的内容。审核证据面板会依次回退到该消息自身存储的 summary,再回退到准确的空状态提示,而不再是此前那句「未存储任何文本」——即便消息的完整 rfc822 内容其实就存放在 R2 中。同一读取返回的已确认 link 对象现也携带 engagement_id——关联会话时选择的商机,未选择时为 null——与其原有的 confirmed_by/confirmed_at 记录并列,阅读端即可在客户归属授权范围内一并展示关联商机。该字段为新增返回值;存储的关联自商机选择列引入起即保留该值,此读取只是不再丢弃它。
带格式的邮件源内容。同一受权限保护的 GET /crm/communications/:id 消息记录也可携带可空的 body_html:第一个非附件 HTML MIME 正文,包含 multipart alternative 中的 HTML 版本,解码上限为 250,000 字节。缺失、为空、不支持、超出上限或无法读取时为 null。这是不可信 HTML,不能作为应用界面的安全标记直接使用。浏览器客户端必须在显示前使用仅允许 HTML 的白名单清理内容,再放入禁用脚本、阻止资源加载的独立框架;不得插入应用自身的 DOM。收件箱保留支持的文字与表格样式,阻止自动加载外部资源;仅在用户主动操作时打开经过校验的链接,且不传递来源信息或窗口控制权。body_text、附件权限与字节、源内容存储、分类和发送行为不变。列表响应不包含 body_html。
在收件箱阅读。打开邮件可查看支持的格式和表格,较宽的表格可在邮件内部横向滚动。选择查看纯文本可使用原有文本阅读器并展开引用内容,再选择查看邮件格式返回。不支持或过大的格式正文会使用文本版本。回复、转发及附件操作仍位于邮件旁边。
邮件分类问题(#3565)
GET /crm/communications/questions 返回本组织当前生效的问题集:{"version","questions"}。双语文本始终是一个对象 {"en","zh_cn"},而不是 _en/_zh 两个独立字段。每个问题的形状为 {"key","kind","prompt"} 加上按类型而定的字段,服务端校验器会拒绝对象上出现的任何其他字段——没有 active 标记,也没有 sort_order:数组顺序就是问题集唯一的顺序。kind 为 boolean(无额外字段;无论哪种类型,每个答案都带有 high|medium|low 把握程度,并非仅限是/否题)、choice(options:2–12 个 {"key","label"} 条目,label 为双语)或 scale(levels:2–10 个 {"level","label"} 条目,level 是从 1 开始的顺序位置而非自由数值,label 为双语,由低到高)。「发件人是否精确匹配现有客户」与「是否已 14 天以上未联系」这两项是数据库事实,不属于该结构,也从不作为问题出现。
PUT /crm/communications/questions 携带 {"expected_version","questions"} 发布新版本,返回 200 {"version"}。竞态落败的保存不写入任何内容,拒绝为 409 version_conflict——与报价规则、报价路由相同的乐观并发形状:调用方需重新读取当前问题集并重新应用编辑,而非盲目合并。已开启 inbox-intelligence 的组织中,每封同步邮件都会通过一次受管控的模型调用(用途 inbox_triage)依据当前版本分类;保存新版本不会重新分类历史邮件,只影响此后同步的邮件。
POST /crm/communications/questions/preview 携带 {"question","limit"}(limit 上限为 6),对最近至多该数量的邮件试运行一个问题——无论已保存还是仍为草稿,本调用都不会保存它——返回 {"items":[{"comm_item_id","subject_preview","answer","error?"}]}。answer 与分类存储的形状完全一致,为 {"value","confidence"};当某一封邮件的实时模型调用失败时为 null,该条目会在 error 中给出失败原因,而不会导致整次试运行失败。试运行不会改变已保存的问题集或任何已存的分类结果。
POST /crm/communications/triage/eval 携带 {"items":[{"id","subject","summary","participants","direction"}]}(1–10 封;direction 为 inbound 或 outbound),按双语默认问题集(而非组织已保存的问题集)对所提供的邮件分类,每封邮件一次受管控的模型调用(用途 inbox_triage),以调用者本人的会话计费,返回 200 {"run_id","model","items":[{"id","answers","error?"}]}。它不读取任何已存邮件,也不写入任何内容。额度被拒时整批停止:该邮件及其后每一封都返回 error: "credit_refused",不再发起调用。仅限组织管理员(否则为 403 forbidden);inbox-intelligence 关闭时隐藏为 404 not_found;请求体格式错误为 400 invalid_items。该端点用于黄金集准确率测评 bun run inbox-eval --live。
分类对 CRM 数据只读:本能力面的任何环节都不会写入客户、联系人、往来、任务或时间线记录;每个问题集与分类记录的组织范围均来自已认证的连接,绝不取自请求体。该功能受 inbox-intelligence 开关控制,默认 developers_only;邮箱一旦同步,分类即依据组织的双语默认问题运行,早于任何人打开设置页——该页面从不是必经的设置关卡。
POST /crm/communications/triage/run 请求对调用者所在组织执行一次有界的、按用户归因的分类(至多 10 封,约 8 秒预算),立即返回 202 {"started"}:分类批次在 Worker 自身的生命周期内运行,关闭或离开页面都不会中断已认领的处理,几秒后从分类概览端点读取最新计数。浏览器在打开审核队列或点击刷新时调用;它以调用者本人的会话计费模型额度,只遍历调用者的组织,且幂等——当前问题版本下已分类的邮件不会被再次认领。设置该端点的原因是:定时任务不携带用户归因,而受管网关在无归因时安全拒绝;待网关提供系统服务归因路径后,定时清扫将接管,此端点退化为无操作的加速器。
POST /crm/communications/triage/resort 仅限已验证的平台超级管理员会话调用(Forth 内部员工,绝非组织管理员或代表)——未获授权的调用者得到与其他隐藏平台入口相同的 404 not_found 形状,从不暴露该入口的存在。发布新的问题集版本从不会重新分类已按旧版本评分过的邮件(见上文 PUT /crm/communications/questions 段落);这些记录会因此永远滞留在旧版本上,在 triage-summary 仅统计当前版本的计数中变得不可见。本端点就是刻意保持非自动化的显式修复手段:每次调用最多认领 25 条 question_set_version 严格低于组织当前版本的记录,为每条记录执行一次与全新分类完全相同的受管控模型调用重新分类,然后返回 202 {"started"}——批次在 Worker 自身生命周期内运行,与上文的 /triage/run 相同。已处于当前版本的记录,或旧版本下处于 failed/仍在 generating 的记录(这两类本就会被常规清扫自身的重试环节触达),本端点从不触碰,常规清扫本身也绝不会自行重新打开历史记录。由于这会花费与一次全新分类相同的单条模型额度,平台专属的准入门槛正是为了让该花费无法被任何组织普通成员触达的路径触发;积压较大时需要多次调用,因为每次调用的认领都有上限,且可安全幂等重试。
POST /crm/communications/triage/rerun-open-replies 与 /triage/resort 使用相同的平台超级管理员专属门槛、相同的可选 {"org_id"} 目标组织和相同的隐藏 404 not_found 形状。它只重新分类每个「待回复」且尚未回复的会话(即工作台显示为回复行的会话)中最新的一封收到的邮件,且仅限其已存分类读取自较早的分类输入,使这些邮件连同正文重新读取并获得客户要求的截止日期——绝不触及其余历史邮件。保存正文摘录之前存入的邮件,会在此处读取一次已存的原始邮件并保存其正文摘录;常规清扫从不读取已存的原始邮件。可选 "item_ids" 为 1–25 个邮件条目 id(否则为 400 invalid_item_ids):改为重新读取这些邮件的当前分类,而不是待回复会话——例如工作台回复行的触发 id;只在目标组织内匹配,其他组织的 id 不会命中任何记录。人工分类的邮件只重新读取其截止日期:其答案、状态和分类人从不改变。可选 "cap" 为 1–25 的整数(默认 25,否则为 400 invalid_cap);每封邮件一次受管控的模型调用(用途 inbox_triage),以调用者本人的会话计费。它同步返回 200 {"candidates","attempted","updated","skipped","creditRefused","remaining"};重复调用直到 remaining 为 0。每封重新读取的邮件都会标记当前输入版本,因此重复调用是幂等的。重新分类失败,或期间被人工分类抢先时,已存分类保持原样,不会因重跑失败而使会话离开「待回复」;模型分类的邮件若重新读取的结果不够确定,保留已存分类。
收件箱分类概览(#3566)
GET /crm/communications/triage-summary 返回 /communications 总览页以及 Today 中 inbox_triage 决策行背后的数量统计、按问题的分布和排序后的注意力列表:{"connected","last_sync_at","question_set_version","counts","by_question","attention"}。仅当调用者完全没有可读的邮箱连接时(#2830 具名授权集合)connected 才为 false,此时其余字段均返回各自的空结果,不会执行查询。last_sync_at 是最近一次连接同步的时间(epoch 毫秒),首次同步前为 null。counts 是对调用者可读、范围内连接下 comm_items 的统计:{"found","ready","unsorted","generating","failed"}。页面上显示的「尚未分类」数字并不是这五个统计字段之一——它由客户端计算得出,公式为 found - ready - failed,将低置信度的 unsorted、正在进行中的 generating 以及在当前生效问题集版本下尚无分类记录的邮件合并为一个中性数字,绝不显示为琥珀色或红色:诚实的未完成不是警告。by_question 以组织当前生效的问题键为键,先为每个生效问题清零初始化,再仅对 ready 状态的答案进行汇总:{"kind":"boolean","true","false"}、{"kind":"choice","options"}(以选项键为键的对象)或 {"kind":"scale","levels"}(以字符串形式的级别数字为键的对象)。
attention 是 services/crm/src/services/inbox-attention.ts 在每次读取时即时计算出的确定性、有上限、已排序的列表——不存储任何相关状态。每一项为 {"comm_item_id","source":"inbox_triage","occurred_at","expires_at":null,"decided_at":null,"subject_preview","score","factors","customer"}。subject_preview 是原始主题截取的前 120 个字符,与队列自身的预览接口使用相同的上限。factors 是 {"key","weight","evidence"} 的列表——整数权重加结构化证据,整个排序过程中不涉及任何模型调用:needs_reply_urgent(40 分,needs_reply 与 urgency 两个答案同时命中)、purchase_order(30 分)、quotation(20 分,对应 quotation_discussed 答案)以及 silence_14d(10 分,已匹配客户但 14 天以上无记录活动,或从未有过记录)。score 为各已触发因素权重之和;只有至少触发一项因素的邮件才会出现,按分数降序、occurred_at 降序、comm_item_id 升序排列,每次读取最多返回 10 条。只有 ready 状态的分类结果才会产生因素——unsorted 的低置信度答案不会被当作已确定的注意力理由展示,这与分类本身拒绝猜测的立场保持一致,只是又高了一层。当邮件已确认关联客户时,customer 为 {"id","name"};未匹配的邮件则为 null(在组织范围内仍然可见,因为并不存在「那个客户」可供判断是否已沉寂)。Today 的 inbox_triage 分组渲染的正是同一批行,未作任何改动:本阶段中 expires_at 与 decided_at 始终为 null(S3 才拥有可决策的 CRM 写入审核流程),因此约束队列拥挤程度的是该列表自身 10 条的上限,而非过期机制。
该接口应用与此能力面其他所有通讯读取相同的 #2830 具名授权内容边界与 #1509 负责人范围:凡确认关联到调用者范围之外客户的邮件,在统计、分布和注意力列表中均被完全排除,而不仅仅是隐去姓名。分类结果只针对组织当前生效的问题集版本读取,与队列自身的分类字段完全一致。该接口受 inbox-intelligence 开关控制;返回 404 即代表组织未开启该开关,总览页与 Today 的信息流都将其视为「开关未开启」而非故障——不会读取任何客户端功能开关来决定渲染内容。
#3595 S4 为同一读取新增了一个附加字段 trend:{"weeks":[{"weeks_ago","window_start","window_end","classified","by_question"}]},按从旧到新排列(数组最后一项为 weeks_ago:0,即截至当前的滚动 7 天窗口——这里的「每周」指滚动 7 天窗口,而非日历周,因此不涉及任何时区边界的选择)。classified 是该窗口内结算为 ready 的分类数量,所有问题共用同一个分母(一次受管调用同时回答整个生效问题集)。by_question 仅以组织当前生效的问题键为键——针对已删除或改名问题键记录的纠正永远不会出现在这里。每个条目为 {"corrections","rate"},其中 rate 为当周 corrections / classified,当 classified 为 0 时为 null(表示「暂无数据」,绝不会渲染成平淡的 0%)。这是一个纠正率趋势,而非经过校验的准确率声明:没有人对这些答案与真实结果进行过校验,下降的纠正率是这一阶段所提供的准确率代理指标。该计算是组织范围的——不会按调用者自身的 #2830 可读连接集合或 #1509 负责人范围过滤,这与 inbox-corrections-retrieval.ts 自身少样本检索所使用的组织级样本池保持一致——但始终只隔离到这一个组织自身的记录。
通讯页按邮箱过滤(#3733)
GET /crm/communications/queue?view=pending|discarded、GET /crm/communications/quarantine 与 GET /crm/communications/triage-summary 均新增一个可选的 connection_id 查询参数,将结果收窄到某一个已连接的邮箱——即 /communications 页面的按邮箱过滤器。省略该参数则保持本次改动之前的行为不变:返回调用者可读的每一个邮箱的数据。
该参数只会与调用者自身已解析出的 #2830 具名授权可读连接集合进行比对,绝不直接查询数据库,也绝不会比对到其他组织的行:只有当 connection_id 已经出现在调用者自身的可读集合中时,结果才会收窄为该单一连接——这与上述每个读取接口原本就在使用的集合完全一致。属于其他组织的连接 id、该成员自身授权未覆盖的 id,以及根本不存在的 id,三者的回答完全相同,都是 200 {"items":[]}(在 triage-summary 上则是调用者本身没有任何可读连接时返回的同一空结果结构)——与既有的「没有可读连接」情形使用同一套回答。遇到无效 id 时绝不会回退到未过滤的完整集合,也绝不会区分「不属于你」与「根本不存在」。在 triage-summary 上,只有统计数字、分布和注意力列表会收窄到被过滤的邮箱;附加的 trend 字段仍然是组织范围的,其门槛取决于调用者是否拥有任意可读连接,而非该过滤器,与其自身文档中「组织范围」的既有立场一致。
GET /crm/communications/mailboxes 提供该过滤器自身的选项列表:{"items":[{"id","channel","label","external_account_id"}]},按存储顺序排列,范围正是调用者自身的 #2830 可读连接集合。它比 GET /crm/communications/connections(要求在旧版共享策略下具备受管理员限制的 inspect_setup 操作权限)更窄:仅拥有共享邮箱内容只读授权、但没有 integrations 管理权限的代表,依然会出现在这里,从而可以按自己可读的邮箱进行过滤,即便同一个连接不会出现在「设置 → 集成」的管理列表中。可读集合为空时返回200 {"items":[]}。
收件箱阅读面(#4269)
GET /crm/inbox/accounts 列出调用者可以阅读邮件的关联账号:{"items":[{"id","channel","label","external_account_id","status","pending_count","unread","spam","sync"}]},按存储顺序返回。这些行取自调用者自身的 #2830 具名授权可读连接集合——与队列、隔离区、邮箱列表等读取接口原本就使用的集合完全一致,刻意窄于受管理员限制的 GET /crm/communications/connections 列表——unread 即侧栏角标:按该集合在 SQL 层收窄后的一次分组计数,统计调用者尚未打开的、来自真人的收件(direction='inbound',正常邮件,不含自动邮件与发件人校验失败),状态为 pending 或 confirmed,负责人可见性与邮件列表一致;它等于该账号 GET /crm/inbox/items?view=emails&unread=true&connection_id= 的 counts.unread,他人的已读不会降低它。pending_count(待审核数)为兼容保留。每个条目附加携带 sync——{"state":"never|running|idle|failed","last_success_at"}——即连接列表发布的同一份逐账户同步状态,使仍在首次拉取中的邮箱可以如实说明,而不是显示为空;旧版 CRM 滚动升级期间该字段缺席,按未知处理。另附加 spam:每个账号收到的自动邮件数(direction='inbound'、mail_kind='automated',状态为 pending 或 confirmed,无论已读与否,且未被调用者的个人状态移出),范围与负责人可见性同上;它等于该账号 GET /crm/inbox/items?view=emails&connection_id= 的 hidden_counts.automated,让只收到自动邮件的新邮箱也能看出邮件已到达。旧版 CRM 不返回该字段,按 0 处理。不可读账号的邮件永远不会计入任何角标。没有可读连接的调用者得到 200 {"items":[]},绝不是 403。
GET /crm/inbox/items?connection_id=&limit=&cursor= 返回一页邮件快照,最新优先:{"items":[…],"next_cursor"},其中每一行都是队列读取所用的同一 publicItem 形状(participants 与 metadata 已从 JSON 解析回来;任何行都不会携带附件存储键)。状态只包含 pending 与 confirmed——被放弃的邮件是一个已记录的决定,不再是待阅读的邮件——排序为 occurred_at DESC, id DESC。limit 收敛到 1–200(默认 50)。next_cursor 仅在返回了完整一页时出现,且是不透明值;格式错误的游标会被当作不存在(即第一页),绝不会报错。connection_id 与队列读取的按邮箱过滤是同一条规则,只与调用者自身的可读集合比对:属于其他组织的连接 id、该成员授权未覆盖的 id,以及根本不存在的 id,三者的回答完全相同,都是 200 {"items":[]}——绝不回退到未过滤集合,也绝不用 403 去区分「不属于你」与「根本不存在」。
两个读取接口均受 inbox 注册表开关控制(默认 developers_only):开关关闭期间,两者对任何调用者都回答 404 not_found——包括平台超级管理员;隐藏就是隐藏,绝不会用 403 去证实该接口的存在。所有查询的组织范围均来自已验证的会话。阅读窗格的逐封邮件内容来自既有的 GET /crm/communications/:id(含其 body_text 与附件清单),遵循其自身文档所述的具名授权边界;本能力面不新增任何写入、发送或撰写能力。
个人 Atlas 邮件状态
星标、已读、归档和可恢复废纸篓只影响当前登录者自己的 Atlas 视图,不会改动邮件服务商的标签、删除已保存邮件、影响同事视图,或把 CRM 的丢弃审核决定当成删除邮件。操作仅作用于显示的邮件 ID;对话列表中仅作用于最新符合筛选条件的邮件,不会移动整个会话或未来邮件。垃圾邮件仍采用 Atlas 现有的自动邮件分类。
GET /crm/inbox/items/:id/personal-state 返回 {"item_id","state":{"starred","placement","revision","unread"}}。没有个人状态记录时,默认为无星标、收件箱、版本零;未读状态仍由现有 comm_item_reads 决定。接口先校验邮件访问权限,再返回版本。读取和写入均使用登录者的组织与身份、当前已连接邮箱的可读授权及客户负责人可见范围。未知、其他组织、已丢弃、非邮件或无权读取的条目均返回 404 item_not_found;收件箱功能不可见时返回 404 not_found。
PATCH /crm/inbox/items/:id/personal-state 仅接受 {"action":"star","expected_revision":0} 这种结构(示例)。操作包括 star、unstar、read、unread、archive、inbox、trash、recover。请求格式无效,或将已发送邮件标为未读,返回 400 invalid_action。成功后返回保存的状态及递增版本;旧版本返回 409 state_changed,不改动个人状态或已读记录。请先读取最新状态,再明确决定操作,切勿猜测版本自动重试。移入废纸篓会记住原来的收件箱或归档位置;重复移入不会覆盖恢复位置,恢复后回到原位置。此接口不支持永久删除。
GET /crm/inbox/items?view=emails&folder=starred、folder=archive、folder=trash 读取个人文件夹,包含收件、发件和垃圾邮件。星标文件夹包含收件箱与归档中的星标邮件,不包含废纸篓。省略文件夹时为收件箱;计数和分页游标采用同一位置筛选。个人文件夹不能同时指定 direction 或 kind,组合请求返回 400 invalid_filter。已归档或移入废纸篓的已发回复仍用于判断会话是否已回复。旧版 POST /crm/inbox/items/:id/read 可标记尚未有个人状态的邮件,也可重复读取真实已读记录;有明确个人状态且无已读记录时返回 409 state_changed,不会覆盖手动未读。新版收件箱自动已读使用带版本的 PATCH。断开账号和邮件服务商变更不在此接口范围内。
在 Atlas 中标为垃圾邮件
“标为垃圾邮件 · 仅限 Atlas”更改该邮箱获授权阅读者共用的 Atlas 分类,不会移动邮件服务商的文件夹、删除原始邮件,或改动个人已读、星标、归档状态。操作只作用于显示的收件邮件;若会话中还有正常邮件或已发回复,会话可继续显示在收件箱。邮件服务商同步行为不变。
POST /crm/inbox/items/:id/junk 接受无请求体或精确的 {},返回 200 {item_id,moved:0|1}。重复标记已是垃圾邮件的邮件返回 moved:0。未知、其他组织、已断开、无阅读权限或超出客户负责人可见范围的邮件均返回 404 item_not_found;收件箱功能不可见时返回 404 not_found。有阅读权限但属于发件、非邮件或已丢弃审核的条目返回 400 not_markable_junk;无效 JSON 或未知字段返回 400 invalid_action。接口先读取客户端请求体,再检查当前组织、身份、已连接邮箱的阅读权限及客户负责人可见范围。
获授权的发件人校验失败收件邮件也可标为垃圾邮件。原来的失败证据若仅保存在 mail_kind=failed_check 中,分类改为 automated 时会将同等证据保留为 sender_check=failed。现有失败标记、校验证据、审核状态与已完成分类答案不变。标记及逆操作都保持 sender_check_failed 的禁止草稿/发送限制,且不会自动写入 CRM 联系记录。
POST /crm/inbox/items/:id/not-junk 仍是明确的逆操作:同一组织、邮箱、会话中获授权且状态为 pending 或 confirmed 的自动收件邮件恢复为正常邮件;发件、非邮件及已丢弃条目保持不变。已完成的答案保留,未分类收件邮件等待人工分类。先完成分类问题集读取,再校验当前邮箱连接、阅读权限与客户负责人可见范围。操作前校验每封将被更改的邮件,任何隐藏目标都会使整批返回 404 item_not_found 且不发生更改;新到达且不在授权目标快照中的邮件不会加入批次。对已经正常的邮件仍返回现有 400 not_junk。界面立即禁用等待中的操作,拒绝后恢复,并忽略旧身份、邮箱或邮件的过期结果。操作已保存但刷新失败时明确说明列表过期,请重新加载查看真实结果;不会自动发起逆操作或邮件服务商变更。
询问邮件是否真实
对于发件人校验失败、回复地址(Reply-To)与发件人不同,或被分类标记为可疑的邮件,Atlas 不会起草回复。收件箱会询问阅读者这封邮件是否真实,而不是在工作台生成回复任务(那样会显示为逾期)。在有人回答之前,不会为它生成回复任务,已打开的任务会让位(以 superseded 关闭)。“是垃圾邮件”使用现有的 POST /crm/inbox/items/:id/junk。
每个 GET /crm/inbox/items 读取(view= 读取和 thread_key 读取)都会为需要询问的收件附加 genuine_check:{"reason":"sender_check_failed|reply_to_mismatch|suspicious|payment_change","confirmed":false|true}。其他行不包含该字段。
POST /crm/inbox/items/:id/genuine 为整个组织记录“是真的”(这是关于邮件本身的事实,而不是某个人的视图),返回 200 {item_id,genuine_check},其中 confirmed:true;重复确认保留第一次的记录。对同一路径的 DELETE 即撤销,返回 confirmed:false。确认后,下一次任务扫描(每五分钟一次)会生成回复任务,Atlas 照常起草;涉及更改付款信息的邮件(payment_change)除外,Atlas 永不起草。无需核实的收件或已发送邮件返回 409 not_applicable。未知、其他组织、已断开、无阅读权限或超出客户负责人可见范围的邮件返回 404 item_not_found;收件箱功能不可见时返回 404 not_found。组织、身份、已连接邮箱的阅读权限和客户负责人可见范围的检查与垃圾邮件接口完全相同。
收件箱智能分拣 CRM 录入建议(#3567)
POST /crm/communications/:id/proposal/build 为一条通讯记录生成——或幂等地直接返回已存在的、不会重新抽取——唯一一条建议,返回 {"proposal"}。404 item_not_found 覆盖未知、跨租户或内容边界拒绝的记录;该接口受 inbox-intelligence 开关控制,组织未开启时返回 404。当没有任何建议适用时,返回的是 {"proposal":null,"reason"}——绝不是 HTTP 错误——reason 为以下之一:classification_not_ready(该邮件在组织当前生效问题集版本下的分类尚未 ready)、no_signal(未匹配的邮件未被分类为新业务询盘,或已匹配的邮件分类结果未命中 purchase_order/quotation_discussed/samples_discussed 中任何一个)、或 duplicate_open_engagement_no_stage_match(已匹配客户已有一个进行中的业务,且组织的实时销售阶段目录中没有与触发信号匹配的阶段——此时若仍提出一个通用的新业务建议,会悄悄制造出第二笔重复交易的风险,因此宁可不给出建议)。一封未匹配、被分类为新询盘的邮件会生成 kind:"lead" 建议:邮件的主题/参与者/摘要被转换为带引文出处的合成证据片段,送入与 `customer-staging` 完全相同的受管抽取流水线(EXTRACT → NORMALISE → MATCH),因此 payload 的 proposal 字段就是一个完整的 `StagedCandidateProposal`(参见前文的暂存客户候选相关小节)——每个有值的字段都带有 `high|medium|low` 把握程度与出处证据,或者是 {"state":"conflict","alternatives"},或者是 {"state":"blank"};绝不猜测。完整的 lead payload 为 {"proposal","matches","suggested_lead_source":"website","entry_stage","opportunity_title","source":{"comm_item_id","subject","occurred_at"}}。已匹配客户的分类若命中 purchase_order、quotation_discussed 或 samples_discussed,则会生成 kind:"opportunity"(尚无进行中业务时)或 kind:"stage"(推动已有进行中业务变更阶段)建议,payload 为 {"customer_id","customer_name","engagement_id","from_stage","to_stage","signal","rationale","evidence":{"answer_keys"}}——from_stage/to_stage 是取自组织自身实时销售阶段目录(GET /crm/settings/pipeline)的阶段键,绝非写死的列表;rationale 为 matched_catalogue_key、highest_probability_open_stage 或 no_catalogue_stage_match_used_entry_stage 之一。同一 (org, comm_item, kind) 组合下的建议行唯一——建议处于待确认或已确认状态时,再次打开同一封邮件的审核页面绝不会重新触发受管模型调用,且只有未匹配线索的生成路径才会真正花费一次模型调用。已放弃的建议是唯一例外:对其再次生成会重新打开同一行——重新执行一次抽取(线索类会花费一次明确的、按用户归因的模型调用),新 payload 替换旧内容,状态回到 pending,而此前的放弃记录仍保留在纠正审计线索中。放弃是一个决定,不是墓碑。
GET /crm/communications/proposals?status=pending|confirmed|discarded(默认 pending;其他取值为 400 invalid_status)返回 {"items":[proposal]},其组织与代表负责人范围与审核队列本身完全一致,按最新优先排序。每个 proposal 行为 {"id","org_id","comm_item_id","kind","payload","staging_id","status","discard_reason","confirmed_by","confirmed_at","created_at","updated_at"}——只有 lead 建议才会设置 staging_id,指向同一次抽取写入的不可变 `customer_staging_candidates` 审计记录。
两个可选筛选参数可将列表收窄到一封邮件及其会话:comm_item_id(该邮件)和 thread_key(其会话)。两者任给其一即按其匹配,同时给出时满足任一即可;都不给时列表与以前一样不做筛选。组织、可读邮箱和负责人范围的限制与不带筛选时完全相同,因此筛选只会让结果变少:调用方无权读取的邮箱中的邮件,或其他组织的 ID,都返回 200 {"items":[]}。工作台的回复任务就是这样读取商机建议的:GET /crm/communications/proposals?status=pending&comm_item_id=&thread_key=。
商机建议会自动生成。当组织的 opportunity.update_from_inquiry 权限为 with_permission(默认值)时,定时收件箱任务会为最近 14 天内满足以下条件的每封来信生成一条待处理的 kind:"stage" 建议:分拣结果为 ready 且命中 purchase_order、quotation_discussed 或 samples_discussed,已确认关联到客户,且该客户有一个规划器会向前推进的进行中商机。该任务从不自行确认:只有当有人通过 POST /crm/communications/proposals/:id/confirm 确认后,商机才会变化。每封邮件最多生成一次建议;已放弃的建议不会被定时任务重新生成;建议从不让商机倒退,也不会推进到商机已处于的阶段;没有进行中商机的邮件不会得到建议(该任务从不提议新建商机)。权限为 by_itself 时,同一任务照旧直接应用建议;为 never 时不生成任何建议。
POST /crm/communications/proposals/:id/confirm 可携带可选的 {"corrections":[{"field","prior","corrected"}],"accepted_fields":["company.name","contacts[0].email",…]}:其中每一条 `corrections` 都会在确认生效之前写入一条 `comm_triage_corrections` 审计记录,供 AC8/S4 少样本学习闭环使用——代表当下做出的决定是一个既定事实,与这次确认本身是否成功无关——并且现在这些修正会真正应用到写入本身:被拒绝的字段不会写入,被编辑过的字段会写入编辑后的文本,冲突字段会写入所选的那个候选值。accepted_fields 列出代表在某个开放字段(低/中把握度的建议值,或已解决为某个候选值的冲突字段——绝不包括高把握度、已预先勾选的字段,那类字段无需在此列出,服务端会自行重新计算该默认规则)上明确接受、且未作任何修改的字段路径。之所以需要它,是因为 `corrections` 只有在字段的原值与最终值不同时才会产生一条记录:原样接受一个开放字段时,原值与最终值相同,这在报文层面与代表从未碰过该字段完全无法区分——而这两种情况服务端的处理方式恰好相反(写入被接受的值,还是按同一条安全规则将未触碰的开放字段当作留空排除)。company.lead_source 现在也遵循同一套「普通可审核字段」规则:只有当确认请求中完全没有为它提供任何值时,才会回退到该建议自带的 suggested_lead_source。确认通过一条已存储的回执做到恰好一次(与本能力面其他确认使用的同一套 `0035` 模式一致):首次调用会执行写入——对 kind:"lead" 创建客户(线索类)、其主联系人与一个处于入口阶段的业务;对 kind:"opportunity" 在 to_stage 新建一个业务;对 kind:"stage" 执行阶段变更(历史、时间线、变更事件,适用时的赢单自动升级)——返回 201 {"proposal_id","created":true,"kind","result"};此后每次重放(包括 `409 confirm_conflict` 之后的重试)都返回 200,created:false 并携带最初存储的 result,绝不会重新执行写入,也不会重新读取某次重试请求携带的 `corrections`/`accepted_fields`。拒绝情形:404 not_found(未知或跨租户 id);409 already_discarded(已被他人放弃——已无可确认的内容);409 confirm_conflict(存在并发的确认尝试正在进行,或该行处于"已确认但无回执"的不一致状态)——可安全地重试同一次确认调用,它要么重放出真实的已存储结果,要么再次失败;重试时应省略第一次已经记录过的 `corrections`(它只是一次没有重放保护的普通审计写入),而 `accepted_fields` 则可以安全地重复携带(写入本身由已存储的回执把关,而非由这个字段把关)。
POST /crm/communications/proposals/:id/discard 携带 {"reason"}(1–500 字符;为空、缺失或超长均为 400 reason_required),不改动任何记录——与本能力面其他决策相同的零写入放弃姿态——首次调用返回 201 {"proposal_id","created":true,"status":"discarded","discard_reason"},重放时返回 200,created:false 并携带最初的原因。404 not_found 覆盖未知或跨租户 id;409 already_confirmed 表示该建议已被确认(无论是本人还是他人)——此时的放弃请求已无意义,而不是需要重新争辩的错误。
以上四个接口均受 inbox-intelligence 开关控制(未开启时返回 404),并应用与本能力面其余部分相同的组织与负责人范围边界:`lead` 建议对任何有权读取其来源邮件的人可见;`opportunity`/`stage` 建议还要求调用者的负责人范围包含所匹配的客户。
GET /crm/communications/won-verifications?thread_key=(或 ?comm_item_id=;二者必须且只能提供一个,否则 400 invalid_request)按时间从新到旧列出由采购订单邮件开启的赢单核实,形状为 {"items":[{"id","engagement_id","deal_title","customer_id","comm_item_id","status","filename","created_at"}]}。status 取值为 pending、confirmed、confirmed_partial、declined 或 superseded;filename 是邮件中的采购订单文件名,没有则为 null。结果为空或调用者不可见时返回 200 {"items":[]}。与提议列表一样,组织未开启收件箱智能时返回 404 not_found。采购订单邮件若对应客户唯一的进行中商机(或其关联指定的商机),会把附件中的采购订单归档到该商机,并开启与上传采购订单相同的待核实;文件不会被自动读取。工作台的回复任务读取这一接口,把销售引导到商机的"赢单·待核实"核对。采购订单邮件不再产生 stage 建议;是否将商机标记为赢单,始终只由人在商机页面决定。
仅限内部服务绑定调用,任何浏览器调用方均不可达:GET /internal/pipeline-stages?org=(HMAC 签名的内部主体请求,否则 403 forbidden)返回请求组织当前生效的销售阶段目录,形状为 {"items":[{"key","name","type","sort_order"}]},不做缓存,因此管理员在同一会话中对阶段的修改能立即体现在下一次调用中。Agent Worker 的 `draft_stage`/`draft_opportunity` 工具读取的正是这一接口,而不是写死的阶段列表——与本建议生成器自身遵循的"始终读取实时目录"规则一致。
收件箱邮件与会话
GET /crm/inbox/items 按时间倒序返回调用者可读邮箱中的邮件:{"items":[...],"next_cursor"}。该接口受 inbox 功能开关控制:返回 404 表示本组织未开启。只返回调用者有权读取的邮箱中的邮件(与其他通讯读取相同的具名授权边界),已丢弃的邮件不会出现。结果为空、connection_id 不可读或不存在、调用者无权限,均返回 200 {"items":[]}。
分页:limit(默认 50,最大 200)与 cursor(传入上一次响应的 next_cursor;格式错误的游标按第一页处理)。connection_id 限定为某个可读邮箱。可选过滤条件之间为 AND 关系:group=thread 每个会话返回一行(该会话中最新的可读邮件),并增加 thread_count(该会话中可读邮件的数量);thread_key=<key> 按从旧到新返回单个会话(不能与 group 同时使用);email_type(分类标签值,例如 quotation);needs_reply=true;urgency_min(1 到 5 的整数);customer_id(关联到该客户的邮件)。在平铺列表中,每封邮件自身必须满足所有过滤条件。使用 group=thread 时,会话中只要有任一封邮件属于该 email_type 即匹配,只要有任一封关联该 customer_id 即匹配;而 needs_reply 与 urgency_min 则以该会话中最新一封已分类的收到邮件为准。任何过滤参数的值无效都会返回 400 {"error":"invalid_filter"},过滤条件绝不会被悄悄忽略。
每一行除常规邮件字段外,还包含 thread_key(所属会话)、thread_basis 与 classification。thread_basis 为 headers 表示会话由邮件的回复头确定,single 表示该邮件独立成会话。一个会话就是一条邮件往来链:不会按主题合并邮件(2026-10-11 之前按主题合并的邮件会按其自身邮件头重新归入会话;在此完成前某些行仍可能显示 inferred)。classification 为该邮件在本组织当前问题集下的分类答案,没有则为 null。GET /crm/communications/pending 的每一行同样带有 thread_key 与 thread_basis。
会话与邮件视图。加上 view=conversations 或 view=emails,即按页面的方式读取收件箱;不带 view 时上述读取保持不变。响应变为 {"items":[...],"next_cursor","counts","hidden_counts"}。kind=person|automated|failed_check 选择列出哪类邮件(默认 person)。needs_reply=true 与 urgent=true(紧急度 4 或 5)同时收窄行和所有计数。view=conversations 还可使用 latest_topic=inquiry|quotation|sample|purchase_order|invoice_payment|logistics|after_sales|supplier|promotions|job_application|notice|other|not_sorted;view=emails 还可使用 covers=(前七个主题的逗号分隔列表),所列主题必须全部涉及(AND)。view=emails 只列出收到的邮件:发出的邮件不会成为该视图的行或计数,而是显示在其所在的邮件串中 (view=conversations)。参数与视图不匹配、值未知,或与旧的 group、email_type、urgency_min、customer_id 过滤同时使用,都会返回 400 {"error":"invalid_filter"};thread_key 仍是原来的单会话读取。没有任何可读邮箱的调用者,或 connection_id 不可读,返回 200,列表为空且所有计数为零。
会话的各项事实取自该会话中最新一封已分类(状态 ready)的收到邮件:latest_topic 为其 email_type(前六种以外的值记为 other;没有已分类的收到邮件则为 not_sorted),needs_reply 与 urgent 为其答案。covers 为所有已分类收到邮件的 email_type 与询盘、报价、样品、订单、发票或付款标记的并集。在邮件视图中每封邮件独立计算:使用自身的类型与标记,以及自身的 needs_reply 与 urgent;已发送邮件没有这些。
counts.all 为所选 kind 中符合上述收窄条件的会话(或邮件)数量。会话视图中 counts.latest_topic 之和始终等于 counts.all,选择某个 latest_topic 只改变行,不改变任何计数。邮件视图中 counts.covers.T 是在再勾选主题 T 时会匹配的邮件数,因此各 covers 计数相互重叠,不会相加。hidden_counts 为相同收窄条件(包括已勾选的 covers)下 automated 与 failed_check 的数量,所以显示的加上隐藏的等于全部匹配项。会话中只要有一封邮件属于 person,该会话即为 person,真实客户的会话不会因其中一封自动邮件而被隐藏。
每一行增加 thread_key、covers(数组)、needs_reply、urgent、mail_kind、sender_check 与 not_sorted_reason;会话行另有 thread_count 与 latest_topic。mail_kind 在邮件到达时按邮件头确定:已知发件人(本组织的联系人,或我们已发过邮件的会话)始终为 person,若邮件服务商的验证失败则 sender_check 为 failed;否则 DMARC 失败、Microsoft 365 复合验证失败(compauth=fail),或 SPF 失败/软失败且没有与 From 域名对齐的 DKIM 通过,为 failed_check;只读取最上面的 Authentication-Results 头(收件服务商会把自己的加在最上面,其下方的内容可能由发件人伪造);否则群发、邮件列表、自动提交、投递报告或 no-reply 邮件为 automated;其余邮件以及尚未分类的邮件均为 person。已分类时 not_sorted_reason 为 null,否则为以下之一:queued、older_than_30_days、low_confidence(存有两个候选时附带 candidates)、retrying(附带 attempt 与 max_attempts)、failed、credit_paused、sorting_off,或 only_sent(会话中没有收到的邮件;view=conversations 不列出也不计入这类会话,它们在收到回复前属于“已发送”)。
把会话关联到客户与商机。两个视图中的每个 person 行都带有 link:{"state","customer_id","customer_name","deal_id","deal_title","how","new_contact","deals","reason"},按会话判断(没有会话线索的邮件自成一个会话)。state 为 suggested、confirmed、not_customer 或 none。建议在读取列表时仅根据本组织已保存的联系人计算,不调用模型,也不会写入任何数据。按强弱排序:收到的邮件的发件人地址恰好等于调用者可见的某一个客户的联系人邮箱时为 how=contact;否则发件人域名恰好是某一个这样的客户的联系人邮箱域名时为 how=domain(gmail.com、qq.com 等公共邮箱域名不计入),并把 new_contact 设为该发件人地址;否则 state=none。两个客户共用同一地址或域名时不给出建议。若该客户恰好有一个进行中的商机(未赢单也未输单),则以 deal_id 与 deal_title 给出建议;否则 deals 列出可选的进行中商机(id、title、stage)。会话中只要有一封邮件未通过发件人验证(mail_kind=failed_check 或 sender_check=failed),或任一邮件的 Reply-To 域名与 From 不同,就绝不建议客户:该会话为 state=none 且 reason=sender_unverified。销售代表只会看到自己名下客户的建议;经理和管理员可看到本组织全部客户。confirmed 表示会话中每封可读邮件都已关联到同一客户,此时若调用者能看到该客户,则返回 customer_id、customer_name 以及所选商机。needs_linking=true(仅可与 view 同用)只保留状态为 suggested 或 none 的 person 会话(自动邮件和未通过验证的会话无需关联,因此此时 hidden_counts 为 0),并像 needs_reply 一样收窄行、counts 与 hidden_counts。counts.exact_matches 是此刻 POST /crm/inbox/links/confirm-exact 会关联的会话数(见下);它不受主题和待回复筛选影响,遵循 connection_id,只检查最新的 500 个未关联会话。该值仅在第一页计算:带 cursor 的读取不返回此字段,请沿用第一页的值。已有任何关联或已标记为非客户的会话不会被计入。
以下四个写入接口都需要人的明确操作。除此之外,Atlas 只在下一段所述的两种情况下自行关联会话(对已由人关联的会话中的回复,以及来自已保存联系人的邮件)。对于未知的邮件、其他组织的邮件,或调用者无权读取的邮箱中的邮件,一律返回 404 {"error":"item_not_found"}(与 POST /crm/communications/:id/confirm 相同的命名授权与归属范围规则);inbox 功能关闭时返回 404 not_found。组织始终取自会话。POST /crm/inbox/links/confirm,请求体 {"item_id","customer_id","deal_id"?,"add_contact"?},把该邮件所在会话的所有可读邮件关联到该客户,并设置所选商机(null 或省略表示不关联商机),返回 200 {"confirmed":n,"contact_added":bool}。客户必须存在于本组织且调用者可见(否则 404 customer_not_found;缺少时为 400 customer_id_required),商机必须属于该客户(否则 400 deal_customer_mismatch)。审核确认建立的关联不会被替换;从收件箱建立的、指向其他客户的关联只在本会话内被替换。add_contact=true 还会把该会话的发件人(最新收到邮件的发件人,姓名取自 From 头)保存为该客户的联系人,无论该客户是否已有联系人或域名是否匹配;但会话中存在未验证、验证失败、自动发送或 Reply-To 不一致的邮件时不保存,该邮箱已是该客户的联系人时也不保存(contact_added:false);地址由服务器重新计算,绝不读取请求中的值。POST /crm/contacts 的邮箱若已存在于同一客户下(去除首尾空格、不区分大小写),返回 200 {item, existing:true} 即已有联系人,不会创建重复记录。POST /crm/inbox/links/confirm-exact,请求体 {"connection_id"?},确认所有建议为 how=contact 且客户至多有一个进行中商机的会话(关联该商机,或不关联),该会话中每封收到的邮件都必须发件人已验证(最上面的 Authentication-Results 显示 dmarc=pass,或与 From 域名对齐的 DKIM/SPF 通过;出现多个此类头则视为未验证),或完全没有 Authentication-Results 头(服务商不加盖该头)才会被纳入。发件人未验证的建议仍会显示并带 unsure:true,可由人逐个确认,但绝不会进入 confirm-exact,也不会因它保存联系人(add_contact)。返回 200 {"confirmed":n,"remaining":m}(按会话计数)。每次调用最多关联 50 个会话;remaining 为之后仍待确认的精确匹配数,重复调用直到为 0。已有任何关联的会话(例如已确认会话中新到的回复)绝不会被纳入。POST /crm/inbox/links/not-customer,请求体 {"item_id"},记录该会话不是客户(200 {"marked":true};仍有关联时返回 409 already_linked)。POST /crm/inbox/links/clear,请求体 {"item_id"},撤销该决定及该会话在收件箱中建立的关联(200 {"cleared":n}),不会删除审核确认建立的关联;被撤销的会话 Atlas 不会再自行关联,只能由人关联。
按已保存联系人自动关联。当会话发件人的完整邮箱地址恰好是本组织一个客户的已保存联系人时,该会话无需人确认即关联到该客户。只有当客户的负责人能读取该邮件所在的邮箱时才会关联:其本人的个人邮箱,或其拥有读取授权的共享邮箱(因此个人邮箱只会关联到邮箱所有者本人的客户,与其角色无关);否则保留建议。新邮件到达时即执行(仅在组织的 inbox 功能开启时;较长的会话保留建议);对收件箱中已有的邮件,则在 POST /crm/inbox/links/confirm 或带邮箱的 POST /crm/contacts 之后立即执行:该客户来自其已保存联系人的其他会话会被关联,范围限于调用者可读取的邮箱和可见的客户。响应本身不变,关联在响应之后进行。在此期间由人关联、撤销或标记为非客户的会话保留人的决定。此类关联的 confirmed_by 为 auto:contact,与其他关联一样显示为 state: "confirmed";客户恰好有一个进行中商机时同时关联该商机。以下会话绝不会被自动关联:已有任何关联、已标记为非客户或已被撤销;任一邮件发件人验证失败、属于验证失败类或 Reply-To 指向其他域名;任一收到邮件的发件人既未验证通过、也不属于未加盖验证头的情况(与 confirm-exact 相同的规则:验证结论不能证明发件人时不关联,服务商不添加 Authentication-Results 时可以关联);该地址是两个或以上客户的联系人(或会话中的发件人分属不同客户);或仅邮箱域名匹配。这些会话保留建议,由人确认。
立即分类(Sort now)。POST /crm/communications/triage/run 可选请求体 {"item_ids":["comm_..."]},含 1 到 25 个 id(否则 400 invalid_item_ids)。带 id 时只分类这些邮件,且仅限调用者可读邮箱中的邮件:未分类(含超过 30 天的)或仍有重试次数的失败邮件(手动立即分类比自动重试多允许三次)会被领取;其他 id 被忽略,响应没有任何差异,始终为 202 {"started":true}。无论是否带 id,自动邮件与未通过发件人验证的邮件都不会发送给模型。不带请求体时行为不变。
分类进度与个人主题。view=emails 还可使用 not_sorted_reason=,值为以逗号分隔的 low_confidence、retrying、failed、credit_paused、queued、older_than_30_days、sorting_off;只保留因所列原因尚未分类的收到的邮件,可与其他过滤组合(用于其他视图或未知取值时返回 400 {"error":"invalid_filter"})。第一页(不带 cursor)还会返回 sorting:{"total","sorted","waiting":{"low_confidence","retrying","failed","credit_paused","queued","older_than_30_days","sorting_off"}},除 connection_id 外不受任何过滤影响。它只统计收到的真人邮件(已发送、自动邮件和未通过发件人验证的邮件不会被分类,因此不计入),且 sorted 加上各 waiting 计数等于 total。每次读取还会返回 personal_topics:[{"id":"p:<id>","label","label_zh"}],即调用者自己的主题,按创建时间从早到晚,没有时为 []。latest_topic 与 covers 接受调用者自己某个个人主题的键 p:<id>,counts.latest_topic 与 counts.covers 为每个个人主题增加一个键;对创建者而言,归入个人主题的邮件计入该键而不是 other,因此各计数之和仍等于 counts.all。每一行以及 thread_key 读取的每个条目都带有 personal_topic("p:<id>" 或 null)。当邮件由人工分类时,classification.model 为 person。
手动分类一封邮件。POST /crm/inbox/items/:id/sort,请求体 {"topic" | "new_topic":{"label","label_zh"?},"covers"?:[...],"needs_reply":bool,"urgent":bool}。topic(内置键 inquiry、quotation、sample、purchase_order、invoice_payment、logistics、after_sales、supplier、promotions、job_application、notice、other,或调用者自己的 p:<id>)与 new_topic(label 去除首尾空白后 1 到 60 个字符,label_zh 可选,最多 60 个字符)必须二选一。covers 取除 other 外的内置键,默认 []。new_topic 的名称若与某个内置主题的英文或中文名称相同(不区分大小写),则归入该内置主题;与调用者自己的主题相同则复用,不会重复创建。该答案作为这封邮件的共享分类写入,同事能看到主题、涵盖项、需回复与紧急标记;个人主题对外共享为 other,只有创建者能看到该主题本身,同事看到的是“其他”。该答案同时被记录为一次纠正,Atlas 会将其作为相似邮件的示例进行学习。会话分组不变。200 {"item_id","topic","personal_topic":{"id","label","label_zh","created":true}|null,"sort_id"}。分类、个人归档、纠正示例、新主题及撤销快照会一并提交。若提交前状态已被其他请求更改,返回 409 sort_changed,不会写入任何部分;请重新加载后再分类。错误:400 invalid_request(请求体格式错误,或 topic 与 new_topic 同时给出或都未给出)、400 topic_name_required(new_topic.label 为空或过长)、400 unknown_topic(既不是内置键也不是调用者自己的主题)、400 not_sortable(已发送邮件,或自动邮件、未通过发件人验证的邮件)、404 item_not_found(未知、其他组织的,或在调用者无权读取的邮箱中);inbox 功能开关关闭时为 404 not_found。
撤销一次分类。POST /crm/inbox/items/:id/sort/undo,请求体 {"sort_id"},即该次分类返回的 sort_id。inbox 功能开关与邮件权限规则与分类接口相同。只有做出该分类的人可以撤销,且仅当它仍是这封邮件最新的一次分类时。邮件恢复为分类之前的原样:之前的分类,或没有分类(与之前一样等待分类),以及调用者自己的主题归档。该次分类记录的纠正会被删除,Atlas 不再以此学习;该次分类新建的个人主题,在没有其他邮件归入时也会被删除。200 {"item_id","undone":true};重复调用,或 sort_id 未知、属于他人、已被更新的分类取代时,返回 200 {"item_id","undone":false},不做任何更改。错误:400 sort_id_required、409 sort_changed(此后邮件的分类或个人归档已改变,或保存的撤销状态不完整;不会写入任何内容)、404 item_not_found,以及 404 not_found(inbox 功能开关关闭时)。
Atlas 对未分类邮件的最佳猜测。GET /crm/inbox/items/:id/sort-assist 与 POST /crm/inbox/items/:id/sort-assist 使用与上面分类接口相同的 inbox 功能开关和邮件权限规则。GET 从不调用 AI,也不产生费用:返回已保存的答案,尚无保存时返回 assist_status: "none"。POST 仅在没有与调用者可读的相关邮件相匹配的已保存答案时,才运行一次受治理的 AI 调用,费用记在组织名下,并以调用者自己的会话执行;否则直接返回已保存的答案。已保存的答案会一直沿用,直到同一会话或同一发件人有新邮件到达。两个接口都不会对邮件分类:只有当人通过 POST /crm/inbox/items/:id/sort 保存后,邮件才会被分类。两者均返回 200 {"item_id","assist_status","related":[...],"assist":null|{...}}。
related 最多 8 封,按时间从早到晚,只来自调用者可读的邮箱,不包含自动邮件和未通过发件人验证的邮件。每一项包含 id、subject、direction(inbound 或 outbound)、occurred_at、from({address,name})、to({address,name} 或 null)、relation(same_conversation、same_sender、same_company、same_reference 或 duplicate,后者指 10 分钟内再次收到的同一封邮件)、position(相对这封邮件的 before 或 after)、summary、topic(它的分类;未分类或已发送时为 null)和 thread_key。
assist 存在时包含 intent({en,zh},发件人想要什么)、best_guess({topic,confidence};Atlas 没有猜测时 topic 为 null;confidence 为 high、medium 或 low)、alternative、evidence(原文引用)和 evidence_from(与 evidence 顺序一致的数组:引文来自这封邮件本身时为 null,否则为所引相关邮件的 id)、related_notes([{id,why:{en,zh}}])、recommendation(sort_as、covers、needs_reply、new_topic 为 {label,label_zh} 或 null、next 为 link_customer 或 null、text 为 {en,zh})、model 和 created_at。在这封邮件或相关邮件中找不到的引文会被丢弃;没有引文支撑的猜测显示为没有猜测(best_guess.topic 为 null)。主题仅限本组织提供的主题。
assist_status | 含义 |
|---|---|
ready | 存在 assist。 |
none | 仅 GET:尚无保存的答案;请调用一次 POST。 |
unavailable | AI 无法运行或回答不合格;仍返回 related。 |
credit_paused | 本组织的 AI 额度已用完;仍返回 related。 |
sorting_off | 本组织已关闭分类;仍返回 related。 |
not_needed | 邮件已分类、已发送,或为自动邮件、未通过发件人验证的邮件;仍返回 related。 |
错误:404 item_not_found(未知、其他组织的,或在调用者无权读取的邮箱中);inbox 功能开关关闭时为 404 not_found。
邮件来自哪位客户。GET /crm/inbox/items/:id/customer-match 使用与上面分类接口相同的 inbox 功能开关和邮件权限规则。它不调用 AI,也不写入任何数据。返回 200 {"item_id","candidates":[...],"new_customer":{...},"deal":{...}}。candidates 最多 3 位该邮件可能来自的现有客户,按匹配强度排序,每项含 customer_id、name、owner_email、open_deals、how 和 evidence。how 为 contact(已保存的联系人)、domain(公司邮箱域名)或 name(邮件中出现该客户名称);evidence 是匹配到的原文。只会推荐调用者有权查看的客户。new_customer 是“新客户”的预填内容:name、name_evidence、contact_name、contact_email(均可为 null)以及 contact_addable。deal 含 suggested(询价、报价和采购订单邮件为 true)、去掉回复和转发前缀的 title、入口阶段 stage 以及本组织的开放阶段 stages。未通过邮件服务商发件人验证的发件人不会匹配到任何人:candidates 为空,contact_addable 为 false。错误:404 item_not_found(未知、其他组织的,或在调用者无权读取的邮箱中);inbox 功能开关关闭时为 404 not_found。界面通过现有的 POST /crm/inbox/links/confirm、创建客户和创建商机来保存;此读取接口不改变任何数据。
待办与"不是客户"。第一页(不带 cursor)时两种视图都会返回 todo:{"needs_sorting":N,"needs_linking":N,"not_customer":N}。这些数字只取决于调用者可读的邮箱(以及 connection_id),不受其他筛选影响。needs_sorting 是需要人工分类的收到邮件数:Atlas 不确定、无法分类,或分类功能已关闭(low_confidence 加 failed 加 sorting_off);needs_linking 是未关联客户且未标记为"不是客户"的普通邮件会话数,与 needs_linking=true 列出的完全一致;not_customer 是已标记为"不是客户"的会话数。not_customer=true 只列出被标记为"不是客户"的会话(邮件视图中为这些会话的邮件);其他取值返回 400 invalid_filter。
未读。已读标记按人记录:同事打开同一封邮件,不会替其他任何人标记为已读。POST /crm/inbox/items/:id/read(无需请求体)记录调用者已打开该邮件。它与上文的分类接口使用相同的 inbox 功能开关和条目规则,且只写入调用者自己的已读标记:邮件本身、它的分类以及其他人看到的内容都不变。该接口是幂等的,返回 200 {"item_id","read_at"},其中 read_at(毫秒)为首次标记已读的时间;重复调用返回相同的值。错误:404 {"error":"item_not_found"}(条目不存在、属于其他组织,或位于调用者无权读取的邮箱中),inbox 功能开关关闭时为 404 not_found。收到的邮件在调用者标记已读之前都是未读;发出的邮件永远不算未读。会话的最新一封邮件是调用者尚未读过的收到邮件时,该会话为未读,因此我们自己的回复会让会话变为已读。带 view 时,unread=true 只保留未读的行,并像 urgent=true 一样同时收窄行、counts 和 hidden_counts;其他取值,或不带 view 使用 unread,均返回 400 {"error":"invalid_filter"}。两种视图的每一行都带有 unread(true 或 false),两种视图也都返回 counts.unread:所选 kind 中未读的会话(或邮件)数,收窄条件与 counts.all 相同但不含 unread 本身,因此开启 unread=true 时这个数字不变。两种视图还返回 counts.needs_reply:所选 kind 中需要回复的会话(或邮件)数,收窄条件与 counts.all 相同但不含 needs_reply 本身,因此开启 needs_reply=true 时这个数字不变。direction=outbound 即收件箱的“已发送”文件夹:配合 view=emails 列出已发送的邮件而非收到的邮件;配合 view=conversations 列出所有包含我方发出邮件的会话。latest_topic 及其他仅适用于收到邮件的过滤条件不能与其同时使用。它只包含 Atlas 记录为已发送的邮件(从 Atlas 发出的回复);暂不读取邮箱自身的已发送文件夹。它不能与 needs_reply、urgent、unread、covers、needs_linking、not_customer、not_sorted_reason 或 person 以外的 kind 组合;其他 direction 取值,或在会话视图、未指定视图时使用 direction,均返回 400 invalid_filter。
不是垃圾邮件。POST /crm/inbox/items/:id/not-junk(无需请求体)把一封垃圾邮件(mail_kind=automated)及其所在邮件串中的其他垃圾邮件移回普通邮件:每封都变为 mail_kind=person,其中尚未分类的收到邮件等待调用者分类。这些邮件的 not_sorted_reason 为 low_confidence,classification.cause 为 moved_from_junk,计入 todo.needs_sorting,Atlas 不会自动为它们分类。系统会记录谁在何时移出了哪封邮件。功能开关和条目规则与上文的分类接口相同:返回 200 {"item_id","moved":n};邮件不是垃圾邮件时(包括未通过发件人验证的邮件,它们单独处理)返回 400 {"error":"not_junk"};条目不存在、属于其他组织或位于不可读邮箱时返回 404 {"error":"item_not_found"}。垃圾邮件和未通过发件人验证的邮件的 not_sorted_reason 始终为 null:Atlas 从不为它们分类。
待回复直到已答复。收到邮件的 needs_reply 只在得到答复前有效:同一邮件串中较晚的发出邮件,或其后的“标记为已回复”,都会清除它;客户再次来信时会重新出现。urgent 是 needs_reply 的一个等级:只有 needs_reply 为 true 时才可能为 true,并随之清除。两者都作用于行、needs_reply=true / urgent=true 以及所有计数。POST /crm/inbox/items/:id/replied(无需请求体)记录调用者已在 Atlas 之外答复了该邮件所在的邮件串(Atlas 不读取已发送文件夹):返回 200 {"item_id","thread_key","marked_at"};重复调用会把 marked_at 更新为最新时间。它只写入该标记:不发送任务事件,也不记录联系(没有 email_sent),因为并未发送任何邮件。收件箱的“归档”使用此接口,并自行关闭该邮件串的回复任务。功能开关和条目规则与分类接口相同:条目不存在、属于其他组织或位于不可读邮箱时返回 404 {"error":"item_not_found"}。
收件箱智能分拣纠正闭环(#3595)
POST /crm/communications/:id/classification/correct 携带 {"question_key","corrected_value"},纠正一条分类答案并立即可见地重新分类该邮件——这正是 AC1 在不做任何微调的前提下要求闭环的方式。corrected_value 必须已经符合该问题自身的形状(boolean 问题为布尔值,choice 为 options[].key 之一,scale 为 levels[].level 之一)——与校验模型自身答案完全相同的分类型规则。受 inbox-intelligence 开关控制(未开启时 404 not_found),随后应用与本能力面其他任意单条操作相同的组织与代表负责人范围校验(#2830 具名授权内容边界,再加上针对该邮件本身的 #1509 负责人范围检查)。请求形状层面的拒绝——400 question_key_required、400 unknown_question_key(不在组织当前生效的问题集中)、400 invalid_corrected_value(附带 reason)——都在可见性校验之前执行,因为它们只依赖组织自身的问题集,不会泄露该邮件是否存在;调用者无权读取的邮件仍返回 404 item_not_found,而从未被分类过的邮件返回 400 not_classified。
成功时,这条纠正会在尝试重新分类之前,先写入一条 comm_triage_corrections 审计记录(scope 为 classification,prior 为该邮件此前对该问题键存储的值或 null,corrected 为新值)——代表所做的决定是一个既定事实,与随后的重新分类本身是否成功无关。重新分类复用与 POST /crm/communications/triage/run 完全相同的受管分类流程,只针对这一条邮件执行,并以调用者自身的 cookie 归因,同时应用该组织自身的检索少样本示例(见下文)——随后,无论这次全新模型调用实际返回了什么,都会将被纠正问题的答案强制设为代表本人提供的值,并标记为 high 置信度:一次模型调用并不保证会重现人类刚刚给出的值,这样处理让「展示已改变的存储答案」这一承诺变成确定的,而不是概率性的。其余每一个问题的答案,只要这次全新模型调用成功,就反映这次重新分类的结果;一旦调用失败(信用额度拒绝、网关不可用、模型输出无效),则会以该邮件此前完整的答案集合作为兜底,只改变被纠正的那一个键——失败的重新分类绝不会让此前已确定的答案倒退,也绝不会让这次纠正看起来什么都没改变。响应为 {"comm_item_id","question_key","classification":{"status","question_set_version","answers","cause","model"},"rerun":{"ok","cause"}}——只有当一次真实的模型调用确实执行、且其(被强制覆盖后的)答案被存储时,rerun.ok 才为 true;为 false 时 rerun.cause 会说明原因(例如 model_unavailable、credit_refused:<kind>、model_output_invalid),但无论哪种情况,classification.answers[question_key] 始终等于提交的值。若该邮件此时已有另一次分类进行中(另一条纠正,或恰好赶上的一次定时扫描),则返回 409 reclassify_conflict——此时纠正审计记录已经落盘,调用者只需重试这次重新分类即可。
检索(AC2,实现见 inbox-corrections-retrieval.ts):从现在起,这个组织发起的每一次分类调用——定时扫描、临时的、以用户身份发起的 /triage/run,以及这个纠正接口自身的重新分类——都会附带最多 3 条该组织自身过往分类纠正记录作为少样本示例,选取方式是确定性的、基于词法的(不使用任何向量检索服务):只有当一条纠正记录的问题键仍在当前生效的问题集中时,它才有资格成为候选,候选之间按目标邮件主题/参与者与候选自身主题/参与者之间的纯词汇重合度排序,重合度相同时按最近时间优先,再相同则按邮件 id 排序。重合度为零的候选会被直接剔除,而不是被排在低位。被选中的示例会被注入到与邮件正文本身完全相同的不可信内容包裹约定中——一个被纠正的值终究也是进入提示词的、由用户提供的内容。隔离是绝对的:检索只读取发起调用的这个组织自身的 comm_triage_corrections 记录,且一条邮件永远不会把它自己过往的纠正记录当作自身的示例提供给自己。这一整套闭环中的任何环节都不会进行任何微调——纠正记录始终只是可审计的行记录与提示词侧的示例(AC5)。
沟通匹配证据
已保存的匹配快照保留为组织范围审计记录。所有返回的关联和确认回执,均按客户当前负责人及联系人当前所属客户筛选嵌套候选,包括旧版保存的证据和负责人变更后的重复请求。已提交的客户与时间线保持不变;无权访问的辅助候选不会出现在响应中。
匹配确认在写入关联时校验当前负责人和联系人归属。并发请求不能返回其他负责人的关联,也不能把另一客户/联系人组合报告为已成功选择。已保留的确认回执还会阻止将缺失关联重建到替代目标。
邮件发布到 CRM
POST /crm/communications/:id/publish 在审核人确认确切的目标客户、邮件和附件后,仅将明确选定的邮件正文和所选附件复制到一个客户记录。每次尝试都会实时校验两项权限:来源邮箱的具名阅读/审阅授权(实时 Auth 成员身份与作用域,加上存储的 comm_account_grants 行,并在提交内部复查——共享账户无授权或个人账户非本人时返回 403 mailbox_not_authorized),以及目标客户的现有写入权限(#1509 负责人范围;范围外客户与不存在客户同样返回 404)。请求体包含 customer_id、可选 contact_id、明确的 attachment_ids、idempotency_key(1–128 字符)和可选 expected_account_version(账户配置已变更时返回 409 account_version_changed)。清单之外的附件 id 返回 400 unknown_attachment。
提交后的快照归 CRM 所有:一条时间线记录(ref_type comm_publication)带限定长度的正文快照,以及仅为所选附件单独存储的副本,此后仅按现有 CRM 权限提供访问。快照只标注来源账户,不携带任何邮箱侧标识——无外部邮件 id、无源存储键、无邮箱 URL——并在日后断开连接或撤销授权后仍然保留;其删除遵循现有 CRM 政策。首次确认返回 201;重复请求返回 200 和原始发布结果;同一 key(或同一邮件)以不同选择或目标重试时返回 409 idempotency_conflict 并指明原始记录。对象副本在提交前写入暂存;复制失败或权限丢失时不产生任何发布,清理暂存键,私有来源邮件保持可重试(503 storage_failed)。
DELETE /crm/communications/publications/:publicationId 移除该副本:删除时间线记录与附件副本、记录转为墓碑,并在连接审计事件中写入移除记录。撤销发布是 CRM 删除操作——遵循客户的现有写入权限而非邮箱授权——客户范围之外的代表与未知发布同样读到 404。GET /crm/communications/publications/:publicationId/attachments/:partId 向当前可见该客户的调用者提供单个已发布副本。
脱敏的账号诊断与操作记录
GET /crm/communications/connections/:id/diagnostics 从既有存储投射单个账号的运行状态:收信与发信检测状态及最近检测时间、邮箱同步状态与最近成功/尝试时间和已同步条数、发送台账中最近被接受的消息与最近尝试结果,以及管理状态(生命周期、配置状态、是否已记录定时同步的人类授权人)。每个失败面至多出现一次,并附带一个封闭原因和一个下一步操作。原因收敛到固定词表 credential_rejected, access_denied, address_unknown, provider_unreachable, configuration_invalid, storage_unavailable, sync_failed, outcome_unknown,另加至多一个数字服务商代码——绝不出现主机名、用户名、密码或服务商原文。空时间戳显示为「从未/未知」,零计数显示为零。
GET /crm/communications/connections/:id/activity?cursor=&limit= 返回该账号有归属的配置、检测、授权、同步、发送、发布与导出事件,按最新在前排列。不透明游标为键集对(事件时间戳、事件 id);每页默认 25 条且不超过 100 条,同一毫秒的事件保持稳定顺序,非法游标或数量返回 400。发送事件来自既有发送台账,不另建第二事件存储。每条记录只包含白名单字段——id、类型、操作者类型、操作者、时间戳、封闭原因、可选数字代码、发送结果,以及授权变更中的被授权人字段。原始存储详情、消息正文与收件人列表永远不会离开服务。
两个读取都要求该账号的实时 inspect_setup 授权(与所有同级账号读取同一闸门);未被授权服务的调用者得到与未知 id 相同的 404,且所有查询都限定在已验证会话的组织内。GET /crm/communications/connections/:id/diagnostics/export 是显式的账号管理员操作:先重新校验实时 manage 授权,以下载形式返回 atlas-account-diagnostics-export/1——仅含账号/版本/时间/原因/事件元数据,不含操作者标识与授权对象——并在任何字节离开前,在该连接自己的事件存储中写入一条 export 审计事件。无法审计的导出不会发生。
手动连接恢复
POST /crm/communications/connections/:id/test 仍是只读的连接检查,现在附加恢复速率约束:每个账户同时至多一项受限操作。检查在发起任何提供方访问前先认领账户;当任一受限操作(检查或同步)正在进行时,路由以 409 {error:"operation_running", operation} 回报正在进行的操作,而不是再启动一项。其请求体、响应结构、实时 manage 授权、归因的 check 审计事件与只读保证均不变。
POST /crm/communications/connections/:id/sync 是新增的版本化「立即同步」:对邮箱账户的一次显式、受限的同步重试。请求体必须携带 idempotency_key(1–128 个字符)与 expected_version(账户当前配置版本)。执行复用定时扫描所用的同一把按连接租约、同一个已记录人类授权门 —— 不新增 Worker 或计划任务、不重置游标、不自动恢复,也绝不重发邮件。同一幂等键返回同一操作 —— 相同的稳定操作 ID 与相同的中途或已闭环结果 —— 不会第二次执行;完成后可用新键启动又一次受限运行;过期的 running 行是崩溃残留,新键可以越过它,历史从不清除或重写。
成功以 200 {connection, account, operation} 回应,其中 operation 为 {id, operation:"sync", state, result, requested_by, actor_type, created_at, updated_at},state 为 running、completed 或 refused,result 仅携带闭环令牌与计数 —— {outcome:"passed"|"failed"|"completed"|"refused"|"skipped", cause, delivered?, duplicate?, quarantined?} —— 绝无服务器原文、主机名、凭据或游标值。并发以 409 {error:"operation_running", operation} 拒绝,并回报定时同步持有的新租约或进行中的手动操作;过期的 expected_version 为 409 {error:"version_conflict", config_version};缺失或非法的键或版本为 400;未知 ID 与未获实时授权的调用者与所有同级账户操作一样只得到相同的 404。
拒绝会记录为可检视的 refused 操作并附带闭环原因(account_paused、account_disconnected、authority_missing、authority_lost、no_credential、inbound_unavailable、access_revoked、generation_changed、lease_lost、sync_failed),且不改变任何状态 —— 不改生命周期、不改凭据、不动游标。每一笔被受理的同步请求都会向连接自身的审计存储追加一条归因、脱敏的 sync_requested 事件;证据只增不删。发送结果未知的邮件仍可通过诊断读取(send.last_outcome: "outcome_unknown")与活动记录检视;恢复路径绝不写入发送命令或尝试记录,也不存在「重试发送」操作 —— 再次发送始终需要单独的明确批准。在组织的账户模型激活(见 账户模型激活)之前,个人账户在这些路由上保持不可用:所有调用者都得到相同的 404。
账户模型激活
完整的账户模型——个人邮箱、具名授权、私密导入、明确的发送权限与恢复——默认处于关闭状态,且只能按组织通过激活闸门启用。GET /crm/integrations/account-model 报告开关及其就绪状态:{activated, revision, matrix_revision, activated_at, activated_by, deactivated_at, deactivated_by, readiness:{ready, stale, unverifiable}}。就绪指已记录的授权回填在每次读取时都与实时 Auth 成员/权限修订号重新校验:确认发生变更时报告 stale 并清除就绪(需重新回填),实时修订号不可读时报告 unverifiable。同一状态还馈送 Integrations 页面的 can_create_personal 投影,决定连接向导是否提供个人邮箱选项。
POST /crm/integrations/account-model/activate 与 POST /crm/integrations/account-model/deactivate 是由协调者控制的发布操作,绝非浏览器开关。请求体必须携带 expected_revision(开关当前的 CAS 修订号),否则返回 400 expected_revision_required 或 409 {error:"version_conflict", config_version};调用者必须持有实时 manage 权限,其他调用者得到统一的 404。缺少已记录证明时激活拒绝 409 {error:"not_ready"};回填后成员发生变动时拒绝 409 {error:"stale_membership"}(同时清除就绪);实时修订号不可读时拒绝 503 {error:"unverifiable"}——模型从不在未知状态下开启,被拒绝的激活不写入任何内容。已接受的防护矩阵修订号会记录在行上,发布记录因此指明组织所启用候选的版本。停用是遏制而非回滚:个人账户的新建、同步与发送停止,脱敏审计的读取限制与全部历史保留,也不会退回旧的全组织读取——隐私防护读取的是账户元数据,而非这个开关。
内部会话知识读取准入
尚未启用消费者的 Auth 服务绑定前置接口 POST /internal/session-read-assertions 要求已配置 AUTH_SECRET、绑定请求正文的内部 HMAC,以及普通组织成员的会话 Cookie。拒绝 Bearer 令牌、传入主体凭证和平台会话。此接口不是公共 REST API,也不会启用聊天、界面或任何目录适配器。
请求正文仅包含 {audience:"ekb",surface:"agent",method:"GET",target,requestId,action,scope:"ekb:read"}。请求标识为 1–128 个字符,目标为不超过 4096 个字符的规范本地路径,不允许主机名、片段或路径归一化。允许的动作与目标为 ekb.knowledge.retrieve → /search、ekb.library.search → /library/search、ekb.releases.list → /releases 和 ekb.releases.read → /releases/{encoded_release_id}。查询字符串按字节绑定并由所属服务验证;版本详情不接受查询参数。
Auth 每次调用都重新读取会话、组织、暂停状态、成员关系、角色与权限;缺失或格式异常的权限来源不会回退到旧会话声明。即使销售代表没有知识管理权限,仍可读取知识;角色预览只能降低权限。成功返回 {principal,assertion},使用现有凭证请求头绑定本次精确请求,有效期为 30 秒,不返回 Cookie 或令牌文本。失败为 401 unauthorized、403 organisation_suspended 或 503 credential_store_unavailable;所有签发响应均为 no-store,开发环境同样要求真实配置。
EKB 仅从普通会话主体确定组织,在查询知识前拒绝混合凭证及签名、路径、方法、动作、查询、受众、接口来源、权限范围或请求标识不匹配的请求。现有文档排序、当前版本资格与引用保持原状,仅供参考的版本仍仅供参考。未来消费者必须在每次分发和最终检查时重新申请凭证。此前置能力不授予修改、发布、生产交付或助手启用权限。
知识检索匹配方式
GET /search(即 ekb.knowledge.retrieve 的目标)接受可选的 match 查询参数。默认 match=all 以 AND 连接查询词,长短语按短语查找匹配;match=any 改以 OR 连接同样的引号词元,返回共享任一词元的 BM25 排序并集,供调用方自行用配对步骤保证精度。其他取值返回 400 invalid_request;未知参数仍然被拒绝。排序、当前版本资格、组织隔离及其余参数均不变。
内部完整操作规范段落
尚未启用消费者的服务绑定读取 GET /operational/passages 要求 Auth 新签发的普通成员严格会话凭证:动作 ekb.releases.passages、受众 ekb、来源 agent、权限 ekb:read。拒绝仅 Cookie、混合凭证及其他主体类型。本能力不增加公共 REST/MCP/CLI 动作、不启用助手回答,也不修改沙盒行为。
参数:必填 q(NFC、去除首尾空白及合并空白后为 2–100 个 JavaScript 字符单元),可选 kind=policy|workflow、limit=1..8(默认 8)。拒绝未知、重复参数及游标。使用规范化小写子串匹配,完整正文优先于政策章节标题和版本标题;标题命中仍返回完整候选内容。
每种请求类型读取最新 25 个当前且允许回答的版本。第 26 行仅设置 coverage.capped_kinds,不提供证据。正文及定位快照合计在传输前限制为 128 KiB;展开正文及稳定标识文档合计最多 4096 个 JSON 值节点,深度最多 32。每个完整段落最多 16 KiB UTF-8;超限内容明确计数,不会静默截断。响应最多 256 KiB。
响应为 {schemaVersion:1,items,coverage:{capped_kinds,oversized_snapshots,oversized_passages,hit_limit,examined_releases}}。每项包含所属服务签发的 reference、asset、持久化 locator.stable_id、从 1 开始的 locator.occurrence:{group,index}、完整 text、text_format、release_digest、passage_digest、passage_key、complete:true 与 answerable:true。相同内容可共享稳定标识,出现位置及段落键用于区分。政策正文与字符串步骤为纯文本;其他步骤保留完整 JSON,包含条件、键、数值、布尔及 null。消费者必须保留流程结构,不能从孤立叶节点推断规则。
仅本组织当前且允许回答的版本可提供证据,返回前再次检查选中版本。已验证的无匹配、受限的部分覆盖与存储不可用明确区分。错误不包含来源内容:400 invalid_query、401 unauthorized、409 source_changed 或 503 evidence_unavailable;所有响应均为 no-store。Auth 保留 403 organisation_suspended 与 503 credential_store_unavailable。现有短引用、文档读取及沙盒行为保持原状。消费者启用前必须另行完成严格权限及预发布环境验收。
配对方法诊断
现有已授权的 agent.team.inspect 详情读取可为当前客户经理方法候选返回 candidates[].methodComparison。这是原始不可变实验报告的只读投影,仅供 staging/development 环境中当前有权的人类管理员读取。它不设置 evaluationId,不改变候选状态,也不改变发布、回滚或黄金基线门槛。
status 区分 absent(不存在方法证据,包括仅有完整旧式评估的候选)、pending(实验尚在运行)、incomplete(终态报告缺失)、stale(当前来源、聘用、候选、技能、配置或策略已变化)、incompatible(关联、准入或报告完整性失败)和 unavailable(无法验证权限或服务)。即使投影为 complete,仍须检查 comparison.compatible;不兼容原因会明确列出,并停止返回受影响的差值。
投影包含精确的实验清单摘要、报告 ID/摘要及对照和提议指令摘要。冻结身份明确返回为 control.versionId 和 proposed.candidateId、candidateRevision、candidateDigest,直接来自已验证的原始关联,不从当前或上一启用版本推断。验证集和审计集分别保留三个试验序号、逐用例改善/退步/不变/混合结果、首次通过与三次全通过计数。每次原始调用及回退尝试均显示用途、请求与返回的提供商/模型、实际请求推理力度及其来源。执行身份未知或清理未验证时,质量和成本比较均被拒绝。
usage.control 与 usage.proposed 分别按 account_review 和 follow_up_draft 展示预期、已观察、token 完整、已计价、额度完整和待结算尝试数。金额缺失或未计价时,保留 knownCostByCurrency、knownTokens 和 knownCredits,但完整总额保持未知,不返回 costDelta。明确的 zeroModelTrials 与 unknownTrials 不混同。可比较成本差为同币种的提议减对照,不代表胜者或自动启用决定。
summary.latencyMs 先对每个原始试验的精确计费提供商尝试耗时(含回退)求和,再求试验均值。basis 为 mean_per_trial_sum_provider_attempt_latency,并分别返回已验证/总试验覆盖数。耗时缺失保持 null。这是提供商耗时,不是 Agent 端到端墙钟耗时。结果仅为抽样诊断证据,不证明因果关系、真实模型效用、客户验收或发布资格。
任务开始时间与提前提醒
POST /tasks、PATCH /tasks/:id 支持可空字段 start_at(UTC 毫秒时间戳)、start_timezone(有开始时间时必填的 IANA 时区)、reminder_minutes(5、15、30、60;null 关闭)。任务响应返回这三个字段,截止日期 due_at 独立。设置 start_at: null 同时清除时区和提醒。
CRM 受治理的交接补偿只能向 PATCH /tasks/:id 发送 {status:"cancelled",expected_identity:{customer_id,source:"system",source_id}}。仅当三个身份字段仍完全匹配时 TASKS 才原子取消;任务已变更、缺失或调用者不可访问时返回 409 task_identity_mismatch,且不修改任务。其他 PATCH 调用方不发送 expected_identity。
校验合并后的日程:提醒必须有开始时间;新改动的提醒时刻不可已过。历史日程未变时可编辑其他字段。并发日程修改返回 HTTP 409 schedule_changed;请重新加载任务,核对日程后再重试。无效输入返回 HTTP 400,包括 start_at_invalid、start_timezone_invalid、reminder_requires_start_invalid、reminder_time_past_invalid。客户端将本地时间转换为固定 UTC 时刻,拒绝夏令时不存在的时间,并显示重复时间所选的偏移量。
GET /tasks/reminders 原子生成已到时的开始提醒,按任务、开始时刻、提前分钟和负责人去重;window_key 以 start: 开头,嵌套任务包含日程。?feed=all 包含现有 SLA 提醒。POST /tasks/reminders/:id/seen 标记当前有权负责人的提醒已读,刷新后保留。改期、关闭提醒、重新分配、完成、取消或删除后,旧开始提醒不可读取或确认。改期或清除任务截止时间时,尚未读出的 overdue/due_soon 提醒行也会在同一次写入中删除,因此不会送达过期的截止提醒;新日程下次进入提醒窗口时会生成自己的提醒(STORY-V2-ERP-04)。私人开始提醒要求调用者同时为创建者和负责人;经理及其他组织无权读取或确认。空列表返回 200 {items:[]};不可访问的确认 ID 返回 404。
今日和我的任务在页面可见且在线时每 30 秒刷新,返回或重连时刷新。受浏览器与网络影响,通常一分钟内送达;关闭浏览器或离线期间不保证提醒,恢复在线后显示仍有效的未读提醒。现有截止日期和 SLA 行为不变。本功能通过上述 REST 接口提供,不新增 CLI 命令或 MCP 工具。
任务到期日汇总(首页与任务页)
GET /tasks/due-summary?timezone=Asia%2FSingapore 返回 {timezone, now, dayStart, dayEnd, overdue, today, upcoming, undated},统计当前登录用户自己的待办任务:dayStart/dayEnd 是所请求 IANA 时区中当天与次日本地零点(边界按该时区计算,含夏令时切换),overdue 统计今天开始前到期的任务,today 统计今天余下时间到期的任务,upcoming 统计之后的任务,undated 统计无截止时间的待办任务。已保存的截止时间不会被改写;只有展示分组随时区变化。桌面端首页汇总与任务页分组使用同一授权范围,两个页面不会出现不一致的状态。未知或格式错误的时区返回 400 timezone_invalid;未登录返回 401。正在并入其他客户的任务会被排除,并像列表接口一样通过 merge_pending 报告。这是一个只读接口;日程安排仍在现有任务接口上进行,不新增 CLI 命令或 MCP 工具(STORY-V2-ERP-04)。
任务辅导文案的语言(#2665)
POST /tasks 在 title/description 之外,支持可选的结构化辅导字段:coaching_key(规则 ID 或画像缺口要求代码,最多 80 字符)与 coaching_params(最多 20 个调用方提供的字符串/数字值组成的对象)。任务响应(列表与单条读取)都包含这两个字段,缺省为 null。仅提供 coaching_params 而没有 coaching_key 时返回 HTTP 400 coaching_key。
CRM 的建议接受路径(POST /crm/suggestions/:id/decide,action: "accept")会同时设置这两个字段:按接受者当时的语言(读取 Web 客户端已写入的同一个 atlas_locale Cookie)渲染新建任务存储的 title/description,并把来源建议 evidence.reason 的键与参数存为 coaching_key/coaching_params,使 Web 端可以按当前查看者的语言重新渲染该行——查看时的语言可能与接受时不同。此功能上线前写入的任务,或由其他调用方写入的任务,coaching_key 为 null,按原样显示其存储文案。
文档 PATCH:名称、文件夹与关键词
PATCH /knowledge/files/:id 可修改文档的名称、所在文件夹,以及(自 #4077 起)关键词列表。请求体决定修改内容:只应用其提及的键——{"name":"…"} 重命名,{"folder_id":"…"} 移动(null 表示资料库根目录),{"keywords":["…","…"]} 整体替换该文档存储的关键词列表。响应是文档的完整详情,即 Worker 实际保留的内容。
keywords 是对用户可见关键词的整体替换:逐项去除首尾空白、丢弃空项、大小写不敏感的去重(保留第一种写法)、每个关键词 1–40 个字符、最多保留 32 个;超出即 400 invalid_request。只有 ready 状态的文档接受关键词(否则 409 wrong_status)。写权限遵循记录自身的作用域:公司文档需要知识管理权限,个人文档仅限所有者,其余为 403 forbidden。关键词写入会递增文档 version,并发的重新提取会因守卫写入失败而让路,不会复活已删除的关键词。该写入与响应都不改变提取、段落、搜索行为或语料统计——这些仍只来自提取器。
PDF 理解
经当前 document_pdf_extract 技能处理的 PDF,在 GET /knowledge/files/:id 中会新增 pdf_extraction;旧记录和非 PDF 返回 null。该对象记录准确的技能 ID、版本和摘要、可空的文档元数据、带页码的内容,以及相互独立的文字、图像和 AI 覆盖范围。partial 状态及其 gaps 是权威依据:客户端不得暗示未覆盖页面、矢量图或识别失败的图像已经被理解。
POST /knowledge/files/:id/re-extract 会重新读取当前存储的原始字节。只有同一文件版本仍然有效时,成功刷新才会原子替换 PDF 结果及其可搜索段落;处理期间旧结果继续可见。技能检查前 200 页,对选定的位图最多发出一次受限的多模态请求,并通过受治理网关在发布结果前记录用量。
可以安全重试的终态提取错误会返回 retry_execution_id。用户下一次明确重试时,仅重放一次同一操作,并发送 x-atlas-pdf-retry-of: <retry_execution_id>;同一重试意图的传输层重试必须复用此值。未知、跨组织或属于另一提取依据的身份会返回 409 invalid_retry_execution。已经受理过的重试身份始终复用其既有子执行,绝不再次推进;尚无子执行的旧身份只能推进当前最新且已确认失败的执行。execution_in_progress 不返回重试身份,因为结果未确定的工作绝不重复执行。
文档详情中段落的来源信息
GET /knowledge/files/:id 返回的 passages 中,每一段都带有可选的来源字段(snake_case 命名):page(PDF 的 1 起始页码)、kind(PDF 段落为 text、scan 或 visual;图片段落为 transcription 或 description;表格段落为 table)、image_ordinal(页内第几张图,1 起始;纯文字段落为 null)、part(Office 文档的第几个部分,1 起始:DOCX 依次为正文、页眉、页脚;整个 PPTX 视为第 1 部分)、line(文本或 Markdown 段落窗口的起始行号,1 起始)以及 spreadsheet({sheet, range, row}:工作表名称、段落覆盖的 A1 样式单元格范围、1 起始行号)。所有字段均为可选——缺失即未知:在此变更之前提取的文档和旧数据都会如此,直到通过 POST /knowledge/files/:id/re-extract 重新提取。失败的文档仍返回空的 passages 数组。
资料库工作区与 AI 整理
GET /crm/knowledge-links?limit=1..200&offset=… 是资料库工作区使用的有界关联索引。它只返回 customer_id 与 source_id;经理和管理员可读取组织内可见客户,销售代表只能读取自己负责的客户。该接口不会从 EKB 复制文档元数据。GET /crm/documents?limit=1..200&offset=… 是客户资料中上传文件的对应索引(#4231):按相同的可见性规则只返回 customer_id 与文档 id,不含已删除的文件和已归档的客户,客户文档页据此把资料中的文件与关联文档一起计数。文件本身仍是 CRM 文档,通过 GET /crm/customers/:id/documents 读取。GET /crm/documents/mine?limit=1..200&offset=… 以上传者视角读取同一批记录:当前登录成员在客户资料中上传的文件,按时间倒序,供资料库「我的文档」使用。一份文件,两种视角——这与客户文件页展示的是同一条 CRM 文档记录,不会复制到知识库。每行包含 id、filename、kind、content_type、size_bytes、created_at、customer_id、customer_name、engagement_id 与 engagement_name(文件归属的业务;客户级文件时两者均为 null)。业务名称只从该文档所属客户名下的业务解析,因此 engagement_id 指向别处的记录只返回该 id 与 null 名称,绝不返回其他客户的业务标题。limit 或 offset 超出范围或非整数时回退为默认值(100 与 0),不报错,并在 items 之外返回 total。该接口按组织与 uploaded_by 限定;销售代表还只能看到自己负责的客户,已删除的文件和已归档的客户一律排除。存储键绝不返回。GET /crm/documents/:id(#4418)以同样的方式返回单份文档自身的元数据——按组织限定、绝不返回存储键——供共享文档查看器(我的文档、客户文件页、销售记录、审核页)在只拿到一个 id 时打开该文档,而无需列表读取已带出的整行记录。其他组织的 id 或不存在的 id 一律返回 404 not_found,两者无法区分。
POST /knowledge/library/organize-plan 接受 1–50 个不重复资料库文档 ID 的 {"file_ids":[...]},并要求知识写入权限。它仅通过受治理的 Workers AI 绑定发送当前文件元数据与现有文件夹 ID,并返回带理由的只读方案。未知文件、虚构文件夹 ID 与格式错误的模型输出均会被拒绝。该接口绝不移动文档;应用仅通过现有批量移动接口执行用户明确批准的目标。此界面流程不提供 MCP、CLI 或智能体适配器。
客户档案缺口
档案缺口是"每个客户每个待补字段一条"的属主记录——记下备注没有回答的内容(kind: "missing"),或用户答应稍后提供的内容(kind: "promised",附简短 promise_note)。/today 摘要、确认队列和客户页缺项标签都只是这些记录的视图,绝不构成第二份任务清单。状态为 open、asked(助手对每个缺口最多主动问一次)、answered 与 skipped。
GET /crm/customers/:id/profile-gaps 列出单个客户的缺口,未解决的排在前面。POST /crm/customers/:id/profile-gaps 每次最多 upsert 12 条({"gaps":[{"field_key","kind","promise_note?","payoff_key?"}]});每个客户每个字段键只保留一条,同一请求内重复键返回 400 duplicate_field_key。两者都遵循客户可见性关卡:不可见客户的任何子资源一律 404。
GET /crm/profile-gaps?status=live|open|asked|all&limit=1..500 是跨客户界面背后的有界组织级索引(默认 status=live = open + asked,默认 limit 100)。经理和管理员可见组织内可见客户,销售代表只能看到自己负责的客户;每条记录以 customer_name 附带客户显示名称,来自可见性过滤所用的同一行。空结果为 200 {"items":[]}。
PATCH /crm/profile-gaps/:id 以 {"status","value?"} 变更状态。answered 只有五个真实客户列(main_markets、brands、products、background、customer_type)可以携带值;值经由普通的客户更新写入——同一套校验、唯一路径——且必须是登录用户会话:助手的主体断言路径会被 403 value_write_requires_session 拒绝,因此智能体只能记录"已问过",永远不能自己写入答案。没有客户列的字段键携带值返回 400 field_not_writable;已被他人处理的缺口返回 409 gap_already_resolved。
图片理解
知识库接受 JPEG、PNG、WebP、GIF、BMP 和安全 SVG 文件,限制为 8 MiB 和 400 万像素。经 document_image_extract 处理的图片,GET /knowledge/files/:id 会返回 image_extraction,其中包含准确技能身份、格式、尺寸、检测语言、存在时的原始拍摄元数据、段落和覆盖范围。GIF 覆盖范围会明确说明仅读取首帧。GET /knowledge/files/:id/content 会为 JPEG、PNG、WebP、GIF 和 BMP 返回允许的位图 MIME 类型,供资料库内联预览;SVG 始终作为纯文本返回,浏览器绝不执行。
确认和重新提取都读取当前存储的原始字节。成功的受保护结果会原子替换可搜索段落和 image_result;刷新失败时此前的就绪结果保持可见。读取器会在模型调用前拒绝无效签名、不安全 SVG、超限字节或像素。一次禁用缓存的 Gemma 请求通过受治理网关,并在发布前记录已计价用量。图片文字属于不可信证据,不会识别个人或授予权限。
仅限固定样本的质量比较
平台超级管理员可在预发布质量运行器中使用 POST /knowledge/skill-quality/image-benchmark,请求为 {fixtureId, arm:"crm_same_model"|"image_skill", batchId, repeatId, skill?}。只有编译进运行器的六个样本 ID 有效。image_skill 分支要求当前图片技能的 id/version/digest;crm_same_model 拒绝 skill,并在同一受治理 Gemma 路由上使用现有 CRM 转录提示词。响应返回 fixtureId、样本摘要、分支、运行 ID、延迟和结果。重复相同身份会重放缓存结果,绝不再次调用模型。此端点不是普通用户功能,不接受字节或提示词,也不发布资料库证据。
完整表格知识索引
查询额外限制:所选工作簿合计最多 16000 个单元格;完整答案及引用最多 20000 字符,沙盒与助手采用相同限制。发布时,存储段落与保留窗口合计不超过 1.8 MB。超出会明确拒绝,请缩小问题或拆分文件。spreadsheet.evaluated_plan 记录实际用于计算的已校验操作与筛选条件。
确认或重新提取 XLSX 时,会索引安全范围内所有工作表的已占用单元格。文件详情新增 xlsx_index: {state, source_version, coverage: {cells, rows, sheets, semantic}},状态包含 complete、partial、failed 和 legacy_stale。单元格与语义覆盖分别报告;旧版文字仍可检索,但回填前不可计算。
POST /knowledge/library/spreadsheets/query 接受 {"question":"计算 Sales 表中 Amount 列的合计","file_ids":["文件编号"]},限制 1–20 个文件、问题最多 2000 字符,沿用文档库读取权限。沙盒和助手共用此 EKB 结果。受治理的模型只查看表结构;确定性计算遍历表头第 1 行之后的全部匹配数据行。支持精确查找、AND 筛选、行计数、合计、平均、最值及两组结果比较。字段歧义、公式缓存缺失、数字列空值或非数字、陈旧索引及模型不可用均明确拒绝,不猜测。不执行公式、宏、SQL 或跨工作簿联接。
响应新增 answer_kind: "spreadsheet"、spreadsheet: {status, result, matched_rows, reason?};引用包含 file_id, file_name, source_version, sheet, sheet_index, range,定位所有计算值及筛选条件的范围。查找或筛选超过 200 行、证据超过 1000 个不连续范围时会要求缩小问题,不截断结果。表结构超过 60000 字符或可检索文字超过 1.5 MB 时需拆分工作簿。
管理员调用 POST /knowledge/library/spreadsheets/backfill,请求 {"limit":2}(每次 1–5 个文件),返回 {updated, partial, conflicts, failed, remaining}。已完整索引及失败或部分完成的源文件/索引记录会跳过;每个终止性回填失败都会隔离,需修正后明确重新提取,因此不会阻塞后续文件。发布先保存派生对象,再原子切换当前索引,并在同一 D1 批次同步已迁移的兼容记录;失败保留之前的完整版本。部署更新后的 EKB 前需应用增量迁移 0026_library_xlsx_index.sql。
XLSX 读取与客户准备
GET /knowledge/files/:id/xlsx 通过已认证的知识库所有者读取保留的原始工作簿,沿用组织与资料库权限。响应为 {"extraction": ...};读取不会替换原始文件,也不表示 CRM 导入已完成。
| 查询参数 | 含义 |
|---|---|
sheetIndex | 从 0 开始的工作表序号,默认 0。 |
startRow、startColumn | 从 1 开始的坐标,默认 1。 |
maxRows、maxColumns | 每次最多 200 行、40 列,也是默认值。 |
结果包含固定的技能 ID、版本及摘要、文档元数据、有序工作表名称、原始单元格坐标、类型值与显示值、超链接标签及目标、合并区域,以及与缓存值分开的公式。缺失缓存会明确标记;公式与外部链接不会执行。coverage.covered、coverage.remaining 和 continuation 给出精确范围。用返回的续读参数再次读取;表头保留原始坐标。图表优先返回缓存序列,位图描述通过受治理 AI 生成;不可读取的视觉内容会产生警告及部分覆盖状态。有效空工作簿可成功返回;缺失元数据为 not provided。
具名解析错误包括 xlsx_corrupt、xlsx_encrypted、xlsx_legacy_binary 和 xlsx_unsafe_archive。资源上限为 400 个 ZIP 条目、总解压 32 MiB、每条目 8 MiB;额外分配限制为 16,000 个单元格、64,000 个 XML 标记、总 XML 8 MiB、XML 文本 2 MiB。超限明确拒绝,不会静默截断。技能身份或遥测故障单独报告。重新提取失败时保留上次有效结果及原始字节。
附件与助手读取范围
GET /crm/intake-artifacts/:id/reading 与助手的 read_artifact 接受 sheet(名称)、row_from/row_to 和 col_from/col_to。行列从 1 开始,包含首尾。每次最多读取 200 行、40 列;依据返回的完整性及剩余范围继续读取,不应把首个窗口视为整个工作簿。引用或准备客户时保留来源位置。这些是现有附件与助手操作,并非新增 MCP 工具名称。
客户画像将同一公司的重复行保留为独立联系人,包括空序号单元格及来源网站;不会按邮箱域名合并公司。单客户准备发现多家公司时返回 422 multiple_customers,助手转入现有批量审核。422 source_range_required 表示应为歧义输入选择更小的来源范围后重试。使用现有明确提交操作前,审核公司、每位联系人及网站。读取或准备成功不表示客户已经保存。
平台组织模板入驻
获授权的平台操作员可调用 POST /governance/platform/organisations,提交 {orgId, name, adminEmail, templateKey?}。模板来自 GET /auth/organisations/templates;省略时使用 manufacturing-oem。Auth 记录来源,通过签名的 CRM 服务绑定复制默认配置,再发送首位管理员邀请。登录域名与成员权限规则不变。
配置复制或邀请失败时,组织 ID 与原资料保持保留,组织暂不可访问。新的平台入驻流程以此替代原来的“发送失败即删除”。目录返回 onboardingStatus(pending_seed 或 pending_invitation)、onboardingAdminEmail 和 onboardingTemplateKey。用相同资料重试同一 POST,或在平台 → 组织选择“重试设置”。更改资料会冲突;执行中的请求返回 409 onboarding_in_progress。不要改选模板或删除后重建。已完成请求重放不会重新复制配置;邀请重试可能重复发送同一邀请。
seed_failed 与 send_failed 表示已保留的失败步骤。无效或停用模板、无效输入、登录准入拒绝及其他已有组织 ID 会被拒绝。已完成配置复制的邀请重试保留原模板,即使模板随后停用。成功响应单独报告 delivered;false 不代表邮件已送达。待完成组织无法登录、切换、启用成员或通过普通生命周期操作绕过设置。已有组织及其自定义值不会重新复制。
DOCX/PPTX 共享技能
资料库提取返回 office_extraction,包含元数据、带出处内容、图片描述、覆盖范围、续读位置与固定技能版本。GET /knowledge/files/:id/office 从原始文件读取下一部分:DOCX 使用 startBlock(从 1 开始)、maxBlocks(1–200);PPTX 使用 slideIndex(从 0 开始)、maxSlides(1–20)。返回 {extraction},不覆盖已保存证据;原有资料库权限继续适用,重提取失败时保留原结果。
助手附件新增 PPTX。GET /crm/intake-artifacts/:id/reading 对 DOCX 接受 block_from/block_to,对 PPTX 接受 slide_from/slide_to(均从 1 开始)。首块的 office 包含元数据、覆盖范围、续读与技能身份。可选 skill_version/skill_digest 必须与已保存证据一致;版本不匹配或已停用时拒绝读取。CRM 保持附件权限与存储所有权;内部签名 POST /ekb/internal/office-extract 重新验证会话,仅进行临时共享提取,不是公开上传接口。
DOCX 使用最终修订视图并分别标注页眉页脚;无法确定页码,文件声明的页数未经验证。PPTX 优先读取保存的图表数据,不增加渲染基础设施。不支持或无法读取的内容明确标为部分覆盖。任务根据已实现目录与已激活版本发现技能,不授予额外权限、不执行上传代码。新版本仍由管理员激活或停用。客户经理读取同一份已授权证据并记录 account_manager 技能使用。
联系人标签与备注
设置 → 标签分为公司和联系人。联系人页可管理共享和私人标签集。备注中选择或新建标签会同时记录备注上下文和长期个人特点。删除备注不会删除特点;移除特点不会修改旧备注。私人标签和备注中的私人标签上下文仅本人可见。
GET /crm/label-sets?entity_type=contact返回联系人标签集;省略类型时默认返回公司标签。POST /crm/label-sets接受不可变的entity_type(company或contact)。沿用现有标签集编辑和删除权限。POST /crm/label-sets/:setId/categories/:categoryId/values接受{"name":"偏好邮件"},在有管理权限的联系人分类中创建标签;如已有同名可见标签,复用该标签。返回{item}。GET /crm/contacts/:id/labels返回{items}。POST接受{"value_id":"…"},添加长期特点。DELETE /crm/contacts/:id/labels/:valueId移除指定特点。POST /crm/contacts/:id/notes和PATCH /crm/contact-notes/:id接受body和可选的label_value_ids。PATCH 省略标签数组时保留上下文;显式数组替换该备注的标签上下文,并追加长期特点。备注返回值包含labels。无效、隐藏、无权限或类型错误的标签不会导致备注部分写入。
每个请求均校验组织、所属客户可见性、私人标签所有权及备注作者权限。标签字段包含 value_id、value_name、category_id、category_name、set_id、set_name 和 visibility。公司指标与行业标签仍仅用于公司。
GET /governance/platform/skills/:id/quality?orgId=…&version=…&from=…&to=… 将汇总和已封存报告限定为所选组织、技能版本及时间范围。版本筛选先于 2,000 份报告上限执行,避免新版本挤掉历史比较证据。超出上限时会明确提示;不同币种保持独立,CSV 成本保留精确纳单位。
沟通策略方法:执行说明、草稿与撤回(#3734)
POST /crm/settings/outreach-playbooks 将新策略保存为尚未发布的草稿,返回 current_version: 0、draft 和 draft_saved_at。此时没有发布历史,不会匹配客户,也不会出现在成员指引中。确认后选择发布,携带 expected_version: 0 创建不可变的第 1 版;后续发布沿用现有版本序列。明确采用示例的操作仍会发布示例策略。
策略编辑采用草稿流程。PUT /crm/settings/outreach-playbooks/:id/draft(管理员)校验完整的策略内容——名称、条件、触达次数、周期、活动组合、主题,以及最长 4000 字符的 instructions 执行说明——并把它存为该策略的草稿,返回 draft_saved_at;已发布版本、current_version 和每一个历史版本都保持原样。POST /crm/settings/outreach-playbooks/:id/publish 携带 {"expected_version"},在一个带并发保护的批次里把已保存的草稿发布为下一个不可变版本(记录 published_by),并使其成为新计划使用的版本,同时清空草稿;并发竞争失败时不写入任何内容并返回携带当前 current_version 的 409 version_conflict,没有草稿时返回 409 no_draft。POST /crm/settings/outreach-playbooks/:id/withdraw 写入 withdrawn_at 并清空草稿:该策略不再匹配任何客户,历史仍可查看,草稿、发布与恢复一律拒绝 409 playbook_withdrawn(重复撤回是无变化的成功)。#3734 之前“保存即发布”的 PUT /crm/settings/outreach-playbooks/:id 已移除。所有读写都限定在调用者所属组织;列表保留已撤回的策略以便查看历史。
计划会记录匹配依据。GET /crm/engagement-plan?customer_id=… 新增返回 match(计划所用策略锁定的匹配属性:tier、customer_class、country、industry)与 guidance_changed——当计划记录的版本已不再是当前版本(发布了新版本,或策略被撤回)时为真。已接受的计划始终保持在各自记录的版本;接受时锁定的是建议所依据的版本。POST /crm/engagement-plan/refresh 携带 {"customer_id"} 是对该标记的明确回应:按当前匹配客户的策略重新生成建议(保留已完成的触达与最近接受记录),并记录新版本与匹配依据;没有可用策略时不写入任何内容,响应中给出 unmatched。
成员可阅读已发布的指引。GET /crm/engagement-guidance/playbook?customer_id=…(组织任意成员,与 /engagement-plan 使用同一客户可见性门禁)返回 {"guidance" 或 null, "unmatched"}:客户计划锁定的版本——或没有计划时当前匹配的策略——及其 instructions、条件、节奏、活动组合、主题与 match。答案永远来自不可变的已发布版本,草稿绝不可能出现。内部服务使用以共享内部 HMAC 主体签名的 GET /crm/internal/engagement-guidance?org=…&customer_id=…。内部投影只回答已接受计划所遵循的内容:返回已接受计划锁定的精确版本及其执行说明、主题、节奏与匹配依据,或返回明确的 none 并给出 reason——no_plan(没有已接受的计划)、no_playbook(锁定的版本无法解析),或 withdrawn(已在 withdrawn 字段中注明策略与版本,且绝不返回已撤回版本的执行说明)。内部投影没有“当前匹配”兜底。他组织的客户 id 得到与成员读取相同的 404。
触达草稿遵循计划锁定的策略版本(#3741)
POST /agent/engagement-plan/draft 携带 {"customer_id","touch_seq"} 在落笔之前先通过 CRM 的内部锁定指引投影读取客户的策略指引,并在草稿上标注所遵循的版本。行数据与响应新增 guidance:草稿遵循已接受计划锁定的精确版本时为 {"state":"followed","playbook_id","name","version"}——策略重新发布后,仍引用旧版本的计划下的草稿不受影响;草稿未使用策略指引时为 {"state":"none","reason","name?","version?"}:withdrawn 会注明策略与版本(X 已撤回 — 请刷新计划),no_plan 与 no_playbook 显示没有适用的策略。策略执行说明只进入受治理提示词的非受信内容信封,绝不写入行数据;草稿的模型调用记录在提示词版本 touch-draft/2 之下。若指引无法读取,请求返回 503 guidance_unavailable,不生成也不存储任何草稿——业务员会被提示重试,绝不会拿到悄悄遵循其他指引的草稿。Today 与客户详情的审阅界面都会渲染该标注行;#3741 之前写入的旧行没有该字段,界面相应不显示。
受治理的人类团队操作
所有接口复用 Auth 团队函数。新增委托权限 auth:teams:read 与 auth:teams:write 相互独立:写权限只允许提出和确认已审核变更,不包含读取工具。既有令牌不会自动获得权限,须通过 POST /auth/tokens 显式申请。签发以及每次读取、提案、确认、恢复均检查当前有效的所有者/管理员成员身份,以及对应实时 members:read/members:write 能力。撤销、过期、角色降级及不支持的平台/计划任务/同步身份均不能执行。
MCP 工具:auth_teams_read、auth_teams_change_propose、auth_teams_change_confirm、auth_teams_change_reject。提案参数包含变更字段和 idempotency_key;确认/拒绝使用 proposal_id 与 idempotency_key。CLI:atlas organisation teams read '<json>';atlas organisation teams propose '<json>' --idempotency-key KEY;atlas organisation teams confirm PROPOSAL_ID --idempotency-key KEY;atlas organisation teams reject PROPOSAL_ID --idempotency-key KEY。
REST 动作入口:POST /assistant/v1/actions/auth.teams.read、POST /assistant/v1/actions/auth.teams.change,及现有 POST /assistant/v1/proposals/:id/confirm 或 /reject。提案和决定须提供 Idempotency-Key。结果使用现有 atlas.action.v1 信封,数据与 Auth 团队/分配结果一致。团队变更属于管理类提案,执行前仍受政策暂停、到期、拒绝及实时权限检查约束。提案绑定原始主体、组织、规范化输入、版本依据和幂等键。委托令牌不能绕过政策;所有者决定沿用现有治理流程。
{"operation":"list","status":"active","limit":50}
{"operation":"team","team_id":"TEAM_ID","limit":50}
{"operation":"assignment","email":"rep@example.com"}
{"operation":"create","name":"销售团队"}
{"operation":"rename","team_id":"TEAM_ID","name":"客户成功","expected_revision":1}
{"operation":"archive","team_id":"TEAM_ID","expected_revision":2}
{"operation":"assign","email":"rep@example.com","team_id":"TEAM_ID","expected_assignment_revision":0,"expected_team_revision":1}
{"operation":"assign","email":"rep@example.com","team_id":null,"expected_assignment_revision":1}
列表状态支持 active(默认)、archived、all,按 ID/邮箱稳定排序,使用不透明 cursor,默认 50 条、最多 100 条;原样传回 next_cursor。名称去除首尾空白后不能为空,最多 80 个 Unicode 字符。拒绝未知/多余字段。重命名和归档须提供当前 expected_revision;分配须提供 expected_assignment_revision,非空目标另须 expected_team_revision;移除分配必须省略目标版本。仅能归档无成员的有效团队,不提供角色或全局成员变更。
Auth 将变更、审计和操作回执原子提交。超时或响应丢失后,以相同提案和决定键重试:恢复经过实时准入,仅返回匹配的原始结果,不会再次执行;已执行提案随后过期仍可恢复。缺少回执不能绕过有效期。重复键对应不同输入或版本过期返回冲突;权限撤销返回拒绝;服务/存储不可用返回可重试失败,不能伪装为空成功。所有响应均为 no-store。先部署 Auth 回执迁移 0023_organisation_team_operation_receipts.sql、权限和生产接口,再部署 Agent 适配器;公开会话团队 REST 保持兼容。仅服务绑定可访问 /internal/teams/actions/read 和 /change/prepare、/change/execute、/change/recover,要求精确绑定请求及正文的断言,没有公开别名。
若确认请求超时,操作可能仍在提交。方案保持待定;在结果未确定时,拒绝与过期处理返回冲突。再次确认只查询原操作回执,不重复执行;若始终没有回执,方案保持待定,不能据此假定操作失败。
GET /governance/platform/skills/:id/quality?orgId=…&version=…&from=…&to=… 将汇总和已封存报告限定为所选组织、技能版本及时间范围。版本筛选先于 2,000 份报告上限执行,避免新版本挤掉历史比较证据。超出上限时会明确提示;不同币种保持独立,CSV 成本保留精确纳单位。
通过 MCP 与 CLI 读取工作台客户分析
只读操作 crm.workbench.customer_summary.read 与 crm.workbench.customer_contributors.read 沿用 crm:read 权限。普通租户会话与持有该授权的委托令牌,会在 CRM 查询前及返回前由 Auth 重新确定公司、已分配团队、本人或空范围。未分配有效团队的经理获得明确的空范围;无需团队管理权限,也无需确认操作。
MCP 提供只读工具 crm_workbench_customer_summary_read 与 crm_workbench_customer_contributors_read。概览输入严格为 {};明细输入为 {"metric":"customers","scope_revision":"<scope_revision>","limit":50},可选不透明的 cursor。指标包括 customers、active、at_risk、open_pipeline;每页默认50条,允许1–100条。额外字段及调用者提供的组织、负责人或范围提示均被拒绝。
atlas workbench customers summary
atlas workbench customers contributors '{"metric":"open_pipeline","scope_revision":"<revision>","limit":50}'
CLI 调用 POST /assistant/v1/actions/crm.workbench.customer_summary.read 与 POST /assistant/v1/actions/crm.workbench.customer_contributors.read,保留现有操作信封。游标字节须原样传递,并使用匹配的范围版本。每页明细与四项总数来自同一次当前 CRM 快照;分页不保证历史快照不变。
结果保留 customer-current-book-v1、公开范围元数据及 evaluated_at。商机 amount_by_currency 使用完整货币单位并按币种分组;unknown_amount_count 统计缺失金额。商机明细同时提供 customer_id/customer_name 与跟进项目名称及ID,不返回内部负责人名单。
响应均为 Cache-Control: no-store。输入或游标无效返回400;凭证与权限拒绝保留401/403;范围变化返回409 scope_changed,需重新读取概览;Auth/CRM 不可用返回503,不伪造零值。平台、定时及同步调用者不受支持。公开 CRM 路由仍仅接受会话,令牌通过受治理操作读取。先部署 Auth 精确允许列表与 CRM 断言入口,再部署 Agent 适配器;不增加迁移或权限范围。
GET /knowledge/policies/<id>/releases 按最新在前列出历史;GET /knowledge/policies/<id>/releases/<releaseId> 返回单个版本——被替代或已撤回的版本只能在此作为授权历史证据读取。撤回只对当前版本有效:POST /knowledge/policies/<id>/releases/<releaseId>/withdraw 对已撤回或已非当前的版本返回 409 release_not_current,并在同一事务中移除当前版本指针。
冲突会被明确报告。当 base_version 与草稿存储版本不一致时,发布返回 409 {"service":"atlas-knowledge","error":"version_conflict"} 且零部分写入;两个并发的发布会通过版本表的唯一约束串行化,失败方收到同样的 409 且什么都没有写入。条目读取(GET /knowledge/policies/<id>、GET /knowledge/workflows/<id>)新增可加性字段 current_release 摘要,从未发布时为 null;旧数据的 Published 标签并不等于发布版本。删除带当前版本的资产时,该版本会在同一事务中被撤回。读取面向所有角色;发布与撤回要求知识写权限(管理员与经理);所有路由均按组织隔离,外部组织的 ID 一律返回无法区分的 404。所有版本发布路由均拒绝委托令牌。
应收跟进
应收事项(receivable case)是同一笔来源欠款的稳定标识——来源是一笔已记录的交易或一份已上传的发票文档。其下的带日期观察记录销售确认的仍未收回信息:以十进制字符串表示、单一币种的剩余余额,到期日或明确的未知,以及跟进状态(open 待收、partial 部分结清、settled 已结清、disputed 有争议)。每次确认追加新版本并保留全部历史;已结清要求余额为零,待收/部分结清要求余额为正。此处仅记录跟进依据:不记账,也不修改来源交易或发票。
会话路由:GET /crm/customers/:id/engagement/receivables 列出各事项及其最新观察;GET /crm/customers/:id/engagement/receivables/:caseId 返回单个事项及全部历史(最新在前);POST /crm/customers/:id/engagement/receivables 开立事项并确认首条观察({source_kind, source_id, remaining_balance, currency, due_on, state});POST /crm/customers/:id/engagement/receivables/:caseId/observations 确认下一条观察并要求 expected_revision——不匹配返回 409 stale_revision,不写入任何数据。校验拒绝(invalid_balance、invalid_state、currency_mismatch、来源跨客户或不存在时返回 invalid_source、不存在的日历日期返回 invalid_due_on、未来时间戳返回 invalid_observed_at)一律 400,且不改动既有观察。token 调用方通过受治理操作 crm.engagement.receivable.list、crm.engagement.receivable.read 与两步确认的 crm.engagement.receivable.confirm 使用同一能力。使用 DELETE /crm/transactions/:tid 删除来源交易时,会在同一操作中关闭对应事项:追加一条余额为零的 settled 观察,备注为 transaction deleted,历史观察保持不变。该删除在存在关联事项时返回 {"deleted": true, "receivable_case_closed": true},否则 receivable_case_closed: false,并同时删除该交易自身的时间线记录。
年度合作依据
GET /crm/customers/:id/engagement/annual-evidence?year=YYYY 返回单个客户一个自然年(UTC 边界,见 timezone)的可复核年度合作依据:transaction_groups——按状态 × 币种分列的组,含 count 与 amount_minor,按交易 id 去重(同一发票编号下的两笔不同付款都计入);paid_minor_by_currency/outstanding_minor_by_currency 不含取消金额、绝不跨币种合并;该年度的 activity_timeline(最多 50 条,activity_total 为真实总数,超出时 coverage.timeline 为 partial);以及 outstanding_as_of——来自最新确认观察的应收跟进余额,按确认时点单列,绝不计入交易合计。该读取确定性且无写入:依据不变则 digest 相同。token 通过受治理读取 crm.engagement.annual-evidence.read 使用同一能力。
展会背景
销售为一个客户展会明确核实的展会事实——后续推广与邀请只使用该记录,不猜测展会安排:展会名称、起止时间(epoch 毫秒)、IANA 时区、地点、已核实的参展方(participation.customer / participation.company,各为 confirmed、not_confirmed 或 unknown)与展位归属(booth.party 为 customer、company、明确的 none 或 unknown,可带 booth.label)。未知状态保持未知——绝不呈现为已核实的展会安排。每次写入都会先在该客户名下解析 1-10 条引用来源({kind: document|fact, id},CRM 所有)后才落库;每次读取都携带解析后的 sources、confirmed_by 与 confirmed_at。
会话路由:GET /crm/customers/:id/engagement/exhibition-context 列出该客户的记录(无记录时 200 {items:[]});GET /crm/customers/:id/engagement/exhibition-context/:eventId 返回单条记录;POST /crm/customers/:id/engagement/exhibition-context 核实一个新展会的记录(重复展会返回 409 event_exists);PUT /crm/customers/:id/engagement/exhibition-context/:eventId 重新核实已有记录(不能更改展会标识);DELETE /crm/customers/:id/engagement/exhibition-context/:eventId 撤回该记录。拒绝情形——结束早于开始(invalid_timeframe)、非 IANA 时区如 Invalid/Zone(invalid_timezone)、引用来源在该客户下无法解析(跨组织或已删除文档,invalid_source)、无展位/未知展位却带展位号(invalid_booth_label)——一律返回 400,且不改变既有记录。令牌通过受治理读取 crm.engagement.exhibition-context.read 与 propose-confirm-reject 效果 crm.engagement.exhibition-context.create、.update、.delete 访问同一表面。
投标简报
销售为一位客户确认的投标简报——投标推广据此工作而不猜测要求:调用方选择的 tender_reference(每个客户内唯一)、title、以 IANA timezone 读取的 deadline_at 时刻、始终为 confirmed 的 status 及其 confirmed_by/confirmed_at,以及 1-12 条每条不超过 500 字符的已确认要求。每条要求引用 1-5 个已解析的已确认 EvidenceRef(owner:kind/id@version——本客户名下按固定摘要解析的 CRM 记录,或通过 Service Binding 以调用方自身凭据解引用的 EKB Library 源);无法自证的要求会拒绝整个写入,模型绝不能添加或静默确认任何要求。更新是一次显式的再次确认,追加修订 N+1——原修订在 /revisions 下保持可读,新修订成为新建推广草稿的唯一依据。所引用证据过期或撤回绝不删除任何内容:读取返回 eligible_for_new_drafts: false 及机器可读的 ineligible_reasons,直至人工再次确认。这里绝不会创建报价、样品、价格、投标文件或递标;要求文字中的报价或样品标识仅为不透明文本。
会话路由:GET /crm/customers/:id/engagement/tender-briefs 列出该客户的简报(无则 200 {items:[]});GET /crm/customers/:id/engagement/tender-briefs/:reference 返回一条简报;GET /crm/customers/:id/engagement/tender-briefs/:reference/revisions 按新到旧返回全部保留修订(含当前);POST /crm/customers/:id/engagement/tender-briefs 确认一条新简报(重复编号返回 409 tender_exists);PUT /crm/customers/:id/engagement/tender-briefs/:reference 再次确认(不可改名编号);DELETE /crm/customers/:id/engagement/tender-briefs/:reference 连同其修订一并撤回。拒绝——invalid_timezone(非 IANA 时区如 Asia/Bad)、invalid_deadline_at(非 epoch 毫秒的截止时间)、invalid_requirement(超界或未引用的要求)、invalid_requirement_evidence(引用无法在本客户下解析为当前已确认证据)、evidence_unavailable(知识服务暂时无法应答)——都不会改变已存储的简报。令牌通过受治理读取 crm.engagement.tender-brief.read 与 propose-confirm-reject 效果 crm.engagement.tender-brief.create、.update、.delete 使用同一界面。
客户跟进草稿
客户跟进草稿是等待在共享待审队列中的一条面向客户的跟进内容:注册的类型化用途(follow_up、product_recommendation 或 tailored_product_solutions)、在同一客户内解析的业务记录,以及调用方稳定的 occurrence_key —— 每个(客户、用途、业务记录、occurrence)仅一行,重复提交同一 occurrence 会返回既有草稿。草稿以比较并交换方式版本化:编辑方须携带其最后读取的 expected_version,不匹配时返回 409 version_conflict,且不覆盖任何人的决定。批准绑定确切的正文、收件人与 source_digest:创建之后联系人邮箱变更或证据记录编辑都会以 409 approval_stale 拒绝并给出可执行的 reason;对未发送、未锁定的草稿的人工刷新会按当前证据重跑用途、递增版本、清除批准并保留完整决策历史。未完成的对账派发会锁定同一 business key 的编辑、批准与刷新。排定主体不能批准;准备或批准内容从不发送、不创建 Task,也不更新最近联系。
会话路由:GET /crm/engagement-drafts?customer_id=… 列出客户草稿,GET /crm/engagement-drafts?customer_id=…&draft_id=… 返回单条草稿及其历史,POST /crm/engagement-drafts/submit 提交一个 occurrence({customer_id, purpose, business_ref_kind, business_ref_id, occurrence_key, intent, recipient_contact_id},intent 为 message | followup | message_and_followup,message 类 intent 需要收件人),POST /crm/engagement-drafts/by-prepared/:preparedWorkId/{edit,approve,dismiss,refresh} 通过队列行 id 决策。令牌通过受治理读取 crm.engagement.drafts.read 与两步的 crm.engagement.drafts.change(operation:submit、edit、approve、dismiss、refresh)访问同一表面。
Today 的“待你决定”审阅弹窗就是这些动作的浏览器客户端:通过 crm.engagement.drafts.read 与 crm.engagement.dispatches.read 读取草稿、历史与派发状态,通过队列行的 by-prepared 路由执行编辑/批准/忽略,并通过 crm.engagement.drafts.send 与 crm.engagement.drafts.change 在同一个人工确认之后提议发送与刷新。弹窗中的复制仅写入剪贴板,不主张任何联系;显示的发送状态只来自供应商的记录应答——受理不等于送达——因此结果未知时只提供核查,绝不提供重试。
定制产品方案(#2118)通过两步式 crm.engagement.tailored-solution.prepare({customer_id, selection_id, recipient_contact_id})按需准备:智能体从一份产品证据选择中组装依据(最多三条当前合格且可对外的条目,每条以确定的重叠规则与已确认的 CRM 匹配证据配对),确认的提案固定的正是该依据;确认执行时会对照当前选择与上下文重新校验,然后由 CRM 的用途处理器重新解析每条匹配证据,并以调用方稳定的 occurrence key(tailored:<customer_id>:<selection_id>)渲染草稿。选择缺失、为空或全部不合格、或缺少已确认匹配证据时,不会创建草稿并会指明缺失项。批准与发送会对照当前选择重新校验固定的产品来源,因此已停用、被替换或撤销对外授权的来源会被拒绝并指明对应推荐。对外正文只出现产品名称与匹配理由——不含内部标识、URL、版本或来源定位符——且准备从不发送。
GET /knowledge/policies/<id>/releases 按最新在前列出历史;GET /knowledge/policies/<id>/releases/<releaseId> 返回单个版本——被替代或已撤回的版本只能在此作为授权历史证据读取。撤回只对当前版本有效:POST /knowledge/policies/<id>/releases/<releaseId>/withdraw 对已撤回或已非当前的版本返回 409 release_not_current,并在同一事务中移除当前版本指针。
冲突会被明确报告。当 base_version 与草稿存储版本不一致时,发布返回 409 {"service":"atlas-knowledge","error":"version_conflict"} 且零部分写入;两个并发的发布会通过版本表的唯一约束串行化,失败方收到同样的 409 且什么都没有写入。条目读取(GET /knowledge/policies/<id>、GET /knowledge/workflows/<id>)新增可加性字段 current_release 摘要,从未发布时为 null;旧数据的 Published 标签并不等于发布版本。删除带当前版本的资产时,该版本会在同一事务中被撤回。读取面向所有角色;发布与撤回要求知识写权限(管理员与经理);所有路由均按组织隔离,外部组织的 ID 一律返回无法区分的 404。所有版本发布路由均拒绝委托令牌。
应收跟进
应收事项(receivable case)是同一笔来源欠款的稳定标识——来源是一笔已记录的交易或一份已上传的发票文档。其下的带日期观察记录销售确认的仍未收回信息:以十进制字符串表示、单一币种的剩余余额,到期日或明确的未知,以及跟进状态(open 待收、partial 部分结清、settled 已结清、disputed 有争议)。每次确认追加新版本并保留全部历史;已结清要求余额为零,待收/部分结清要求余额为正。此处仅记录跟进依据:不记账,也不修改来源交易或发票。
会话路由:GET /crm/customers/:id/engagement/receivables 列出各事项及其最新观察;GET /crm/customers/:id/engagement/receivables/:caseId 返回单个事项及全部历史(最新在前);POST /crm/customers/:id/engagement/receivables 开立事项并确认首条观察({source_kind, source_id, remaining_balance, currency, due_on, state});POST /crm/customers/:id/engagement/receivables/:caseId/observations 确认下一条观察并要求 expected_revision——不匹配返回 409 stale_revision,不写入任何数据。校验拒绝(invalid_balance、invalid_state、currency_mismatch、来源跨客户或不存在时返回 invalid_source、不存在的日历日期返回 invalid_due_on、未来时间戳返回 invalid_observed_at)一律 400,且不改动既有观察。token 调用方通过受治理操作 crm.engagement.receivable.list、crm.engagement.receivable.read 与两步确认的 crm.engagement.receivable.confirm 使用同一能力。使用 DELETE /crm/transactions/:tid 删除来源交易时,会在同一操作中关闭对应事项:追加一条余额为零的 settled 观察,备注为 transaction deleted,历史观察保持不变。该删除在存在关联事项时返回 {"deleted": true, "receivable_case_closed": true},否则 receivable_case_closed: false,并同时删除该交易自身的时间线记录。
年度合作依据
GET /crm/customers/:id/engagement/annual-evidence?year=YYYY 返回单个客户一个自然年(UTC 边界,见 timezone)的可复核年度合作依据:transaction_groups——按状态 × 币种分列的组,含 count 与 amount_minor,按交易 id 去重(同一发票编号下的两笔不同付款都计入);paid_minor_by_currency/outstanding_minor_by_currency 不含取消金额、绝不跨币种合并;该年度的 activity_timeline(最多 50 条,activity_total 为真实总数,超出时 coverage.timeline 为 partial);以及 outstanding_as_of——来自最新确认观察的应收跟进余额,按确认时点单列,绝不计入交易合计。该读取确定性且无写入:依据不变则 digest 相同。token 通过受治理读取 crm.engagement.annual-evidence.read 使用同一能力。
展会背景
销售为一个客户展会明确核实的展会事实——后续推广与邀请只使用该记录,不猜测展会安排:展会名称、起止时间(epoch 毫秒)、IANA 时区、地点、已核实的参展方(participation.customer / participation.company,各为 confirmed、not_confirmed 或 unknown)与展位归属(booth.party 为 customer、company、明确的 none 或 unknown,可带 booth.label)。未知状态保持未知——绝不呈现为已核实的展会安排。每次写入都会先在该客户名下解析 1-10 条引用来源({kind: document|fact, id},CRM 所有)后才落库;每次读取都携带解析后的 sources、confirmed_by 与 confirmed_at。
会话路由:GET /crm/customers/:id/engagement/exhibition-context 列出该客户的记录(无记录时 200 {items:[]});GET /crm/customers/:id/engagement/exhibition-context/:eventId 返回单条记录;POST /crm/customers/:id/engagement/exhibition-context 核实一个新展会的记录(重复展会返回 409 event_exists);PUT /crm/customers/:id/engagement/exhibition-context/:eventId 重新核实已有记录(不能更改展会标识);DELETE /crm/customers/:id/engagement/exhibition-context/:eventId 撤回该记录。拒绝情形——结束早于开始(invalid_timeframe)、非 IANA 时区如 Invalid/Zone(invalid_timezone)、引用来源在该客户下无法解析(跨组织或已删除文档,invalid_source)、无展位/未知展位却带展位号(invalid_booth_label)——一律返回 400,且不改变既有记录。令牌通过受治理读取 crm.engagement.exhibition-context.read 与 propose-confirm-reject 效果 crm.engagement.exhibition-context.create、.update、.delete 访问同一表面。
客户跟进草稿
客户跟进草稿是等待在共享待审队列中的一条面向客户的跟进内容:注册的类型化用途(follow_up、product_recommendation 或 payment_collection)、在同一客户内解析的业务记录,以及调用方稳定的 occurrence_key —— 每个(客户、用途、业务记录、occurrence)仅一行,重复提交同一 occurrence 会返回既有草稿。草稿以比较并交换方式版本化:编辑方须携带其最后读取的 expected_version,不匹配时返回 409 version_conflict,且不覆盖任何人的决定。批准绑定确切的正文、收件人与 source_digest:创建之后联系人邮箱变更或证据记录编辑都会以 409 approval_stale 拒绝并给出可执行的 reason;对未发送、未锁定的草稿的人工刷新会按当前证据重跑用途、递增版本、清除批准并保留完整决策历史。未完成的对账派发会锁定同一 business key 的编辑、批准与刷新。排定主体不能批准;准备或批准内容从不发送、不创建 Task,也不更新最近联系。
会话路由:GET /crm/engagement-drafts?customer_id=… 列出客户草稿,GET /crm/engagement-drafts?customer_id=…&draft_id=… 返回单条草稿及其历史,POST /crm/engagement-drafts/submit 提交一个 occurrence({customer_id, purpose, business_ref_kind, business_ref_id, occurrence_key, intent, recipient_contact_id},intent 为 message | followup | message_and_followup,message 类 intent 需要收件人),POST /crm/engagement-drafts/by-prepared/:preparedWorkId/{edit,approve,dismiss,refresh} 通过队列行 id 决策。令牌通过受治理读取 crm.engagement.drafts.read 与两步的 crm.engagement.drafts.change(operation:submit、edit、approve、dismiss、refresh)访问同一表面。
Today 的“待你决定”审阅弹窗就是这些动作的浏览器客户端:通过 crm.engagement.drafts.read 与 crm.engagement.dispatches.read 读取草稿、历史与派发状态,通过队列行的 by-prepared 路由执行编辑/批准/忽略,并通过 crm.engagement.drafts.send 与 crm.engagement.drafts.change 在同一个人工确认之后提议发送与刷新。弹窗中的复制仅写入剪贴板,不主张任何联系;显示的发送状态只来自供应商的记录应答——受理不等于送达——因此结果未知时只提供核查,绝不提供重试。
GET /knowledge/policies/<id>/releases 按最新在前列出历史;GET /knowledge/policies/<id>/releases/<releaseId> 返回单个版本——被替代或已撤回的版本只能在此作为授权历史证据读取。撤回只对当前版本有效:POST /knowledge/policies/<id>/releases/<releaseId>/withdraw 对已撤回或已非当前的版本返回 409 release_not_current,并在同一事务中移除当前版本指针。
冲突会被明确报告。当 base_version 与草稿存储版本不一致时,发布返回 409 {"service":"atlas-knowledge","error":"version_conflict"} 且零部分写入;两个并发的发布会通过版本表的唯一约束串行化,失败方收到同样的 409 且什么都没有写入。条目读取(GET /knowledge/policies/<id>、GET /knowledge/workflows/<id>)新增可加性字段 current_release 摘要,从未发布时为 null;旧数据的 Published 标签并不等于发布版本。删除带当前版本的资产时,该版本会在同一事务中被撤回。读取面向所有角色;发布与撤回要求知识写权限(管理员与经理);所有路由均按组织隔离,外部组织的 ID 一律返回无法区分的 404。所有版本发布路由均拒绝委托令牌。
应收跟进
应收事项(receivable case)是同一笔来源欠款的稳定标识——来源是一笔已记录的交易或一份已上传的发票文档。其下的带日期观察记录销售确认的仍未收回信息:以十进制字符串表示、单一币种的剩余余额,到期日或明确的未知,以及跟进状态(open 待收、partial 部分结清、settled 已结清、disputed 有争议)。每次确认追加新版本并保留全部历史;已结清要求余额为零,待收/部分结清要求余额为正。此处仅记录跟进依据:不记账,也不修改来源交易或发票。
会话路由:GET /crm/customers/:id/engagement/receivables 列出各事项及其最新观察;GET /crm/customers/:id/engagement/receivables/:caseId 返回单个事项及全部历史(最新在前);POST /crm/customers/:id/engagement/receivables 开立事项并确认首条观察({source_kind, source_id, remaining_balance, currency, due_on, state});POST /crm/customers/:id/engagement/receivables/:caseId/observations 确认下一条观察并要求 expected_revision——不匹配返回 409 stale_revision,不写入任何数据。校验拒绝(invalid_balance、invalid_state、currency_mismatch、来源跨客户或不存在时返回 invalid_source、不存在的日历日期返回 invalid_due_on、未来时间戳返回 invalid_observed_at)一律 400,且不改动既有观察。token 调用方通过受治理操作 crm.engagement.receivable.list、crm.engagement.receivable.read 与两步确认的 crm.engagement.receivable.confirm 使用同一能力。使用 DELETE /crm/transactions/:tid 删除来源交易时,会在同一操作中关闭对应事项:追加一条余额为零的 settled 观察,备注为 transaction deleted,历史观察保持不变。该删除在存在关联事项时返回 {"deleted": true, "receivable_case_closed": true},否则 receivable_case_closed: false,并同时删除该交易自身的时间线记录。
年度合作依据
GET /crm/customers/:id/engagement/annual-evidence?year=YYYY 返回单个客户一个自然年(UTC 边界,见 timezone)的可复核年度合作依据:transaction_groups——按状态 × 币种分列的组,含 count 与 amount_minor,按交易 id 去重(同一发票编号下的两笔不同付款都计入);paid_minor_by_currency/outstanding_minor_by_currency 不含取消金额、绝不跨币种合并;该年度的 activity_timeline(最多 50 条,activity_total 为真实总数,超出时 coverage.timeline 为 partial);以及 outstanding_as_of——来自最新确认观察的应收跟进余额,按确认时点单列,绝不计入交易合计。该读取确定性且无写入:依据不变则 digest 相同。token 通过受治理读取 crm.engagement.annual-evidence.read 使用同一能力。
展会背景
销售为一个客户展会明确核实的展会事实——后续推广与邀请只使用该记录,不猜测展会安排:展会名称、起止时间(epoch 毫秒)、IANA 时区、地点、已核实的参展方(participation.customer / participation.company,各为 confirmed、not_confirmed 或 unknown)与展位归属(booth.party 为 customer、company、明确的 none 或 unknown,可带 booth.label)。未知状态保持未知——绝不呈现为已核实的展会安排。每次写入都会先在该客户名下解析 1-10 条引用来源({kind: document|fact, id},CRM 所有)后才落库;每次读取都携带解析后的 sources、confirmed_by 与 confirmed_at。
会话路由:GET /crm/customers/:id/engagement/exhibition-context 列出该客户的记录(无记录时 200 {items:[]});GET /crm/customers/:id/engagement/exhibition-context/:eventId 返回单条记录;POST /crm/customers/:id/engagement/exhibition-context 核实一个新展会的记录(重复展会返回 409 event_exists);PUT /crm/customers/:id/engagement/exhibition-context/:eventId 重新核实已有记录(不能更改展会标识);DELETE /crm/customers/:id/engagement/exhibition-context/:eventId 撤回该记录。拒绝情形——结束早于开始(invalid_timeframe)、非 IANA 时区如 Invalid/Zone(invalid_timezone)、引用来源在该客户下无法解析(跨组织或已删除文档,invalid_source)、无展位/未知展位却带展位号(invalid_booth_label)——一律返回 400,且不改变既有记录。令牌通过受治理读取 crm.engagement.exhibition-context.read 与 propose-confirm-reject 效果 crm.engagement.exhibition-context.create、.update、.delete 访问同一表面。
客户跟进草稿
客户跟进草稿是等待在共享待审队列中的一条面向客户的跟进内容:注册的类型化用途(follow_up、product_recommendation、tailored_product_solutions 或 exhibition_invitation)、在同一客户内解析的业务记录,以及调用方稳定的 occurrence_key —— 每个(客户、用途、业务记录、occurrence)仅一行,重复提交同一 occurrence 会返回既有草稿。草稿以比较并交换方式版本化:编辑方须携带其最后读取的 expected_version,不匹配时返回 409 version_conflict,且不覆盖任何人的决定。批准绑定确切的正文、收件人与 source_digest:创建之后联系人邮箱变更或证据记录编辑都会以 409 approval_stale 拒绝并给出可执行的 reason;对未发送、未锁定的草稿的人工刷新会按当前证据重跑用途、递增版本、清除批准并保留完整决策历史。未完成的对账派发会锁定同一 business key 的编辑、批准与刷新。排定主体不能批准;准备或批准内容从不发送、不创建 Task,也不更新最近联系。
会话路由:GET /crm/engagement-drafts?customer_id=… 列出客户草稿,GET /crm/engagement-drafts?customer_id=…&draft_id=… 返回单条草稿及其历史,POST /crm/engagement-drafts/submit 提交一个 occurrence({customer_id, purpose, business_ref_kind, business_ref_id, occurrence_key, intent, recipient_contact_id},intent 为 message | followup | message_and_followup,message 类 intent 需要收件人),POST /crm/engagement-drafts/by-prepared/:preparedWorkId/{edit,approve,dismiss,refresh} 通过队列行 id 决策。令牌通过受治理读取 crm.engagement.drafts.read 与两步的 crm.engagement.drafts.change(operation:submit、edit、approve、dismiss、refresh)访问同一表面。
Today 的“待你决定”审阅弹窗就是这些动作的浏览器客户端:通过 crm.engagement.drafts.read 与 crm.engagement.dispatches.read 读取草稿、历史与派发状态,通过队列行的 by-prepared 路由执行编辑/批准/忽略,并通过 crm.engagement.drafts.send 与 crm.engagement.drafts.change 在同一个人工确认之后提议发送与刷新。弹窗中的复制仅写入剪贴板,不主张任何联系;显示的发送状态只来自供应商的记录应答——受理不等于送达——因此结果未知时只提供核查,绝不提供重试。
定制产品方案(#2118)通过两步式 crm.engagement.tailored-solution.prepare({customer_id, selection_id, recipient_contact_id})按需准备:智能体从一份产品证据选择中组装依据(最多三条当前合格且可对外的条目,每条以确定的重叠规则与已确认的 CRM 匹配证据配对),确认的提案固定的正是该依据;确认执行时会对照当前选择与上下文重新校验,然后由 CRM 的用途处理器重新解析每条匹配证据,并以调用方稳定的 occurrence key(tailored:<customer_id>:<selection_id>)渲染草稿。选择缺失、为空或全部不合格、或缺少已确认匹配证据时,不会创建草稿并会指明缺失项。批准与发送会对照当前选择重新校验固定的产品来源,因此已停用、被替换或撤销对外授权的来源会被拒绝并指明对应推荐。对外正文只出现产品名称与匹配理由——不含内部标识、URL、版本或来源定位符——且准备从不发送。
GET /knowledge/policies/<id>/releases 按最新在前列出历史;GET /knowledge/policies/<id>/releases/<releaseId> 返回单个版本——被替代或已撤回的版本只能在此作为授权历史证据读取。撤回只对当前版本有效:POST /knowledge/policies/<id>/releases/<releaseId>/withdraw 对已撤回或已非当前的版本返回 409 release_not_current,并在同一事务中移除当前版本指针。
冲突会被明确报告。当 base_version 与草稿存储版本不一致时,发布返回 409 {"service":"atlas-knowledge","error":"version_conflict"} 且零部分写入;两个并发的发布会通过版本表的唯一约束串行化,失败方收到同样的 409 且什么都没有写入。条目读取(GET /knowledge/policies/<id>、GET /knowledge/workflows/<id>)新增可加性字段 current_release 摘要,从未发布时为 null;旧数据的 Published 标签并不等于发布版本。删除带当前版本的资产时,该版本会在同一事务中被撤回。读取面向所有角色;发布与撤回要求知识写权限(管理员与经理);所有路由均按组织隔离,外部组织的 ID 一律返回无法区分的 404。所有版本发布路由均拒绝委托令牌。
应收跟进
应收事项(receivable case)是同一笔来源欠款的稳定标识——来源是一笔已记录的交易或一份已上传的发票文档。其下的带日期观察记录销售确认的仍未收回信息:以十进制字符串表示、单一币种的剩余余额,到期日或明确的未知,以及跟进状态(open 待收、partial 部分结清、settled 已结清、disputed 有争议)。每次确认追加新版本并保留全部历史;已结清要求余额为零,待收/部分结清要求余额为正。此处仅记录跟进依据:不记账,也不修改来源交易或发票。
会话路由:GET /crm/customers/:id/engagement/receivables 列出各事项及其最新观察;GET /crm/customers/:id/engagement/receivables/:caseId 返回单个事项及全部历史(最新在前);POST /crm/customers/:id/engagement/receivables 开立事项并确认首条观察({source_kind, source_id, remaining_balance, currency, due_on, state});POST /crm/customers/:id/engagement/receivables/:caseId/observations 确认下一条观察并要求 expected_revision——不匹配返回 409 stale_revision,不写入任何数据。校验拒绝(invalid_balance、invalid_state、currency_mismatch、来源跨客户或不存在时返回 invalid_source、不存在的日历日期返回 invalid_due_on、未来时间戳返回 invalid_observed_at)一律 400,且不改动既有观察。token 调用方通过受治理操作 crm.engagement.receivable.list、crm.engagement.receivable.read 与两步确认的 crm.engagement.receivable.confirm 使用同一能力。使用 DELETE /crm/transactions/:tid 删除来源交易时,会在同一操作中关闭对应事项:追加一条余额为零的 settled 观察,备注为 transaction deleted,历史观察保持不变。该删除在存在关联事项时返回 {"deleted": true, "receivable_case_closed": true},否则 receivable_case_closed: false,并同时删除该交易自身的时间线记录。
年度合作依据
GET /crm/customers/:id/engagement/annual-evidence?year=YYYY 返回单个客户一个自然年(UTC 边界,见 timezone)的可复核年度合作依据:transaction_groups——按状态 × 币种分列的组,含 count 与 amount_minor,按交易 id 去重(同一发票编号下的两笔不同付款都计入);paid_minor_by_currency/outstanding_minor_by_currency 不含取消金额、绝不跨币种合并;该年度的 activity_timeline(最多 50 条,activity_total 为真实总数,超出时 coverage.timeline 为 partial);以及 outstanding_as_of——来自最新确认观察的应收跟进余额,按确认时点单列,绝不计入交易合计。该读取确定性且无写入:依据不变则 digest 相同。token 通过受治理读取 crm.engagement.annual-evidence.read 使用同一能力。
展会背景
销售为一个客户展会明确核实的展会事实——后续推广与邀请只使用该记录,不猜测展会安排:展会名称、起止时间(epoch 毫秒)、IANA 时区、地点、已核实的参展方(participation.customer / participation.company,各为 confirmed、not_confirmed 或 unknown)与展位归属(booth.party 为 customer、company、明确的 none 或 unknown,可带 booth.label)。未知状态保持未知——绝不呈现为已核实的展会安排。每次写入都会先在该客户名下解析 1-10 条引用来源({kind: document|fact, id},CRM 所有)后才落库;每次读取都携带解析后的 sources、confirmed_by 与 confirmed_at。
会话路由:GET /crm/customers/:id/engagement/exhibition-context 列出该客户的记录(无记录时 200 {items:[]});GET /crm/customers/:id/engagement/exhibition-context/:eventId 返回单条记录;POST /crm/customers/:id/engagement/exhibition-context 核实一个新展会的记录(重复展会返回 409 event_exists);PUT /crm/customers/:id/engagement/exhibition-context/:eventId 重新核实已有记录(不能更改展会标识);DELETE /crm/customers/:id/engagement/exhibition-context/:eventId 撤回该记录。拒绝情形——结束早于开始(invalid_timeframe)、非 IANA 时区如 Invalid/Zone(invalid_timezone)、引用来源在该客户下无法解析(跨组织或已删除文档,invalid_source)、无展位/未知展位却带展位号(invalid_booth_label)——一律返回 400,且不改变既有记录。令牌通过受治理读取 crm.engagement.exhibition-context.read 与 propose-confirm-reject 效果 crm.engagement.exhibition-context.create、.update、.delete 访问同一表面。
客户跟进草稿
客户跟进草稿是等待在共享待审队列中的一条面向客户的跟进内容:注册的类型化用途(follow_up、product_recommendation、payment_collection、annual_cooperation_report、quotation_followup 或 next_year_cooperation_plan)、在同一客户内解析的业务记录,以及调用方稳定的 occurrence_key —— 每个(客户、用途、业务记录、occurrence)仅一行,重复提交同一 occurrence 会返回既有草稿。草稿以比较并交换方式版本化:编辑方须携带其最后读取的 expected_version,不匹配时返回 409 version_conflict,且不覆盖任何人的决定。批准绑定确切的正文、收件人与 source_digest:创建之后联系人邮箱变更或证据记录编辑都会以 409 approval_stale 拒绝并给出可执行的 reason;对未发送、未锁定的草稿的人工刷新会按当前证据重跑用途、递增版本、清除批准并保留完整决策历史。未完成的对账派发会锁定同一 business key 的编辑、批准与刷新。排定主体不能批准;准备或批准内容从不发送、不创建 Task,也不更新最近联系。
会话路由:GET /crm/engagement-drafts?customer_id=… 列出客户草稿,GET /crm/engagement-drafts?customer_id=…&draft_id=… 返回单条草稿及其历史,POST /crm/engagement-drafts/submit 提交一个 occurrence({customer_id, purpose, business_ref_kind, business_ref_id, occurrence_key, intent, recipient_contact_id},intent 为 message | followup | message_and_followup,message 类 intent 需要收件人),POST /crm/engagement-drafts/by-prepared/:preparedWorkId/{edit,approve,dismiss,refresh} 通过队列行 id 决策。令牌通过受治理读取 crm.engagement.drafts.read 与两步的 crm.engagement.drafts.change(operation:submit、edit、approve、dismiss、refresh)访问同一表面。
Today 的“待你决定”审阅弹窗就是这些动作的浏览器客户端:通过 crm.engagement.drafts.read 与 crm.engagement.dispatches.read 读取草稿、历史与派发状态,通过队列行的 by-prepared 路由执行编辑/批准/忽略,并通过 crm.engagement.drafts.send 与 crm.engagement.drafts.change 在同一个人工确认之后提议发送与刷新。弹窗中的复制仅写入剪贴板,不主张任何联系;显示的发送状态只来自供应商的记录应答——受理不等于送达——因此结果未知时只提供核查,绝不提供重试。
互动派发
发送一条已批准的互动消息是一次通过现有邮件适配器的、先保留且可审计的派发。两步式效果 crm.engagement.drafts.send({customer_id, draft_id, expected_version})要求 CRM 重新校验权限与精确的批准输入,并为该消息版本提交一次保留;只有被正面确认的保留提交才先于唯一一次供应商调用,因此同一客户、用途与业务记录在发送进行中、结果未知或已受理期间的重复、并发、编辑、克隆或重新准备,只会得到其已记录的派发状态,绝不会触发第二次供应商调用。结果分为 provider_accepted(附供应商消息标识)、definitely_failed(已在受理前证实失败;可由人工显式重试并追加新的尝试记录)和 unknown(超时或工作进程丢失;保留该状态,绝不自动重试)。供应商回执以一个稳定的派发引用记录为已受理,并承载唯一一次幂等的时间线效果——回执绝不显示为已送达。读取 crm.engagement.dispatches.read({customer_id},可选 dispatch_id 或 draft_id)返回已记录的保留及其尝试。定时主体、被撤销的 scope、过期批准、不可用的发送能力以及组织现有的每小时 100 条上限都会被拒绝。
跟进任务确认(tasks.engagement_followup.create)
确认一条客户跟进建议会在 Tasks 服务侧创建恰好一个任务,携带确认后的标题、负责人、日期、客户,以及业务记录为业务的对应业务。先用 POST /assistant/v1/actions/tasks.engagement_followup.create 提议({customer_id, draft_id, proposal_id},可选 title、due_on(YYYY-MM-DD 或 null)与 assignee_email 编辑,编辑后的值将与原始值一并留存);再用 POST /assistant/v1/proposals/:id/confirm 与 Idempotency-Key 头确认。提案绑定其评审时的草稿版本:提议与确认之间发生编辑、刷新或忽略会返回 409 proposal_stale,且不产生任何工作。重复确认,或在 Tasks 已成功而 CRM 中断后重试,都会返回原任务——身份由(组织、客户、用途、业务记录、occurrence、提案)派生的命名空间 source_id 决定。以不同客户、负责人或标题复用该身份会被拒绝,且不会改动已创建的任务。确认从不发送消息、从不确认业务结果、也从不把既有任务标记为完成;排定主体在每个适配器上都会被拒绝。
GET /governance/platform/skills/:id/quality?orgId=…&version=…&from=…&to=… 将汇总和已封存报告限定为所选组织、技能版本及时间范围。版本筛选先于 2,000 份报告上限执行,避免新版本挤掉历史比较证据。超出上限时会明确提示;不同币种保持独立,CSV 成本保留精确纳单位。
GET /governance/platform/skills/:id/quality?orgId=…&version=…&from=…&to=… 将汇总和已封存报告限定为所选组织、技能版本及时间范围。版本筛选先于 2,000 份报告上限执行,避免新版本挤掉历史比较证据。超出上限时会明确提示;不同币种保持独立,CSV 成本保留精确纳单位。
互动计划
一条已接受的互动计划是单个客户、单个用途的重复规则:在同一客户内解析的业务记录、明确的首次到期日(first_due_on)、用于比较仅日期到期值的 IANA 时区(绝非 UTC 午夜),以及 one_time、monthly 或 event_relative 之一。月度重复保留既定的日锚点并在短月收紧(1 月 31 日 → 2 月 28/29 日 → 3 月 31 日)。event_relative 计划从某个已确认的来源日期倒推显式输入的 lead_days —— date_ref_kind/date_ref_id/date_ref_field 指向一个已确认的订单、应收或报价日期 —— 并保存该来源的修订号作为 source_version:来源随后被编辑会使旧的到期依据失效、停止到期工作,必须由人工重新接受后才恢复准备。每次变更都按 expected_revision 比较并交换地版本化;不匹配返回 409 version_conflict 并保留胜出方。暂停、取消与修订会停止未来的准备,且不删除历史到期记录。保存或变更计划不准备、不发送任何内容,也不创建 Task。
会话路由:GET /crm/customers/:id/engagement-schedules 列出客户的互动计划(可选 status 过滤,可选 schedule_id 查看单条计划及其到期历史),POST /crm/customers/:id/engagement-schedules 应用一个操作({operation, …}:accept 携带完整依据 —— purpose、business_ref_kind、business_ref_id、recurrence、first_due_on、timezone,event_relative 另需 lead_days 与日期引用;update 修订指定字段;pause 与 cancel 需要 schedule_id 和 expected_revision)。调用方不可见的客户与未知客户返回相同的 404,因此当前失去可见性的销售代表无法变更。令牌通过受治理读取 crm.engagement.schedules.read 与两步的 crm.engagement.schedules.change(operation:accept、pause、cancel、update)访问同一表面。
industry_technology_digest 用途(#2125)为单个客户排定重复的行业与技术摘要计划。它绑定客户本身(business_ref_kind: "customer" 指向该客户),其到期标识为 schedule/revision/due period,因此修订后的计划会规划全新的到期事项。某个到期事项触发时,代理门控仅通过 EKB 绑定检索组织自身知识,并至多保留五条摘录,每条必须同时满足:(1)其精确声明定位符当前带有可对该客户分享的已确认使用授权(F08);(2)与该客户已确认的行业或技术事实重叠,并以此构成明确的契合理由(F01)。系统绝不调用浏览器、外部订阅源或研究接口。没有合格的配对时,该到期事项记录可见的跳过原因 no_eligible_relevant_source 且不创建草稿。已准备的草稿在审阅对话框中展示每个来源、摘录与契合理由,之后才允许编辑、复制或显式发送;批准与发送前会实时重新解析每个授权,外发正文不携带任何内部标识符或来源定位符。
互动准备
互动准备是已采纳计划在后台的有界运行状态:对每个到期的业务事项(标识为 org/customer/schedule/revision/due period,按需请求则为 org/customer/purpose/business_ref/request_id),至多存在一个待审结果——一条互动草稿,或一个带结构化 reason 的可见 skipped / failed / stopped 状态。claim 是带过期租约的持久行,并发的 tick 与重试因此收敛为一个结果;错过的到期事项收敛为只准备最新未完成的一个,更早的保留可见的 superseded 原因。权限、功能可见性、计划修订与客户状态等阻断会在任何效果之前记录具体的 stopped 状态;已证实的失败(例如来源消失)记录可执行的 failed 状态,仅由经确认的重试动作重新驱动。准备从不发送、从不批准,也不创建 Task 或付费余额。
会话路由:GET /crm/customers/:id/engagement-preparations 列出客户的准备行(可选 status 与 schedule_id 过滤),POST /crm/customers/:id/engagement-preparations 按需准备一个 occurrence({purpose, business_ref_kind, business_ref_id, request_id, recipient_contact_id?})——重复的 request_id 返回同一结果,新的 nonce 不能绕过该业务记录的待审工作。令牌通过受治理读取 crm.engagement.preparation.read 与两步的 crm.engagement.preparation.retry 访问同一表面:重试在人工确认后重新驱动一条已证实失败的行(customer_id、preparation_id),重试永不发送。
业务的客户需求
每个业务都保有自己的客户需求记录:销售的一条记录——在业务页的"客户需求"区输入,或告诉助手后在该区确认一张 draft_deal_needs 卡片——会被原文保存,随后由受治理的抽取(workflow: "deal_needs")读取,提炼出最多六个跟踪字段的事实:product(产品)、quantity(数量)、timeline(时间)、target_price(目标价)、compliance(合规)、packaging(包装)。每条事实都带一个 excerpt(必须是原记录的原文子串)与一个 confidence(high/medium/low);若某条摘录在原文中找不到,整次抽取都会判定失败,绝不会存下一条无法核实的事实。经由这些接口写入的每条事实都标记 stated_by: "rep"——记录是销售自己转述的说法,从不代表客户原话;"customer"(客户原话,来自邮件线索)留给后续范围。某字段尚无当前事实即为缺口,绝不臆测。
同一字段至多保留一条当前事实:稍后一条记录为同一字段给出不同取值时,会替换掉更早那条——除非两条记录发生在同一个亚洲/上海日历日,此时两条都保留,并都标记 conflict: true。若稍后的记录重复同一取值,则不产生任何新记录。
会话态接口:POST /crm/engagements/:id/needs/notes 保存一条记录({text},至多 2000 字)并运行抽取,返回 {summary, facts, gaps, note};抽取失败时记录本身绝不会丢失——该记录仍保留原文,note.extraction_status 变为 "failed" 并带一个 failure_code,响应中仍携带该业务现有的事实。GET /crm/engagements/:id/needs 读取该业务的当前全貌:{summary, updated_at, facts[], gaps[], notes[]},其中每条事实的 source({kind: "assistant_note", note_id, author, at})可定位到其来源的原始记录。POST /crm/engagements/:id/needs/notes/:nid/retry 对某条已存记录的原文重新运行抽取——记录本身无需重新录入。以上三个接口均按调用方所属组织限定范围,且要求已登录的组织成员会话;助手没有另一条写入路径——它只负责摆出 draft_deal_needs 卡片,真正的写入始终经由上面第一个接口,由销售自己点击确认后发出。
当前洽谈报价(报价洽谈)
客户的当前洽谈报价是客户与正在洽谈的报价单之间明确且留痕的关联。一条关联只绑定两者之一:Atlas 报价单的准确版本(quotation_id,每个版本只能有一条进行中的关联),或在 Atlas 外制作的报价单——一份客户文档(document_id)加上可选的展示信息(quotation_number、revision_label、amount_minor、currency、valid_until)。系统不做任何推断:新建版本、发送报价单或上传文件都不会自动把报价放入当前洽谈区,关联文档也不表示它已被发送、被接受或正在洽谈。金额与币种逐条独立显示,绝不合并合计。
会话路由(客户可见范围保护:越权或跨组织客户同样是 404):GET /crm/customers/:id/quotation-negotiations 列出进行中的关联及其报价单/文档的实时状态和派生的 expired 标记(超过有效期的已发送报价保持列出,直至被替换或移除);POST /crm/customers/:id/quotation-negotiations 关联 {opportunity_id, quotation_id} 或 {opportunity_id, document_id, …可选信息}——已被洽谈的版本返回 409 conflict,跨客户或不匹配的绑定返回 404,信息格式错误返回 400;DELETE /crm/customers/:id/quotation-negotiations/:negotiationId 明确移除;POST /crm/customers/:id/quotation-negotiations/:negotiationId/replace({quotation_id})在同一事务中把当前关联结束为“已替换”并建立新关联——新建版本绝不会自动替换洽谈中的报价,并发操作只允许一个胜出(失败方读到 409,胜出方保持不变)。当被关联的报价单通过既有报价路由被接受或拒绝时,其关联自动以该结果结束;报价单本身仍保留在业务的报价历史中。每次关联、替换、移除和生命周期结束都会追加审计事件和客户时间线记录(类型 quotation_negotiation)。DELETE /crm/customers/:id/quotations/:qid 彻底删除该客户的一份草稿报价单,其明细行、审计事件和洽谈关联一并删除,返回 {"deleted": true}。非草稿的报价单返回 409 quotation_not_draft 且不做任何变更:已发送报价单的明细行不可变。未知的报价单 ID、属于其他客户的报价单和属于其他组织的报价单,都返回相同的 404。 非草稿的报价单改为“作废”而非删除(#4194):POST /crm/customers/:id/quotations/:qid/void 把它移出工作列表——本列表、销售记录和业务的报价列表——而其记录、明细行和事件原样保留,返回 {"voided": true, "changed": true}(若已处于作废状态则 changed: false)。POST /crm/customers/:id/quotations/:qid/unvoid 原样恢复。草稿返回 409 quotation_is_draft——草稿只删除,不作废;跨客户、跨组织同样返回相同的 404。已作废的行可通过 GET /crm/customers/:id/quotation-negotiations?include_voided=1 和 GET /crm/engagements/:id/quotations?include_voided=1 再次取回,这正是找回并恢复一份已作废报价单的方式。作废不是状态:报价单仍显示为 sent 或 accepted,因为客户收到的确实是那一份。该动作记录为客户时间线条目(类型 quotation_void),而非报价单生命周期事件,因为它并未改变该文件的任何法律事实。
已保存的报价文件显示在同一客户的“报价”页签。GET /crm/customers/:id/quotation-negotiations 保留报价洽谈记录的 items,并新增已归档报价审阅文件的 reviewed_files。每个文件包含 file_id、file_kind(library 或 customer_document)、name、engagement_id、engagement_title、deal_answer、pre_atlas_ref、saved_at 和 fields(日期、编号、金额和币种及各自的审阅来源)。“不是交易”和“Atlas 之前的交易”保留原意;归档报价不会新增收入、收款或报价洽谈记录。用户可从该行打开有权查看的原文件。可见性取决于文件当前的客户关联;无权读取的文档库文件和已删除文档不返回,重复保存仅显示最新记录,已有文档报价洽谈记录时不重复展示文件行。历史已保存报价无需再次保存即可显示;缺少标准快照时仅返回有引文支持的文档值,界面标为“来自文档”。
基于已批准证据的报价单草稿(#2742)
售前助理可以在其所服务客户的某个进行中业务上,提出创建一份报价单草稿。提案时的依据与定制方案所组装的产品证据依据完全相同(至多三条当前有效条目,每条都对应一项已确认的 CRM 适配事实);确认后,通过既有的按业务创建接口,恰好创建一份状态为 draft 的报价单,每条证据条目对应一行。所有路径上都恒定成立两点:代理绝不定价——每行单价为 0,草稿备注中已说明,由人工定价;此处也绝不发送、绝不将报价单标记为已发送、绝不改动其状态。已结束(赢单或丢单)的业务、属于其他客户的业务、属于其他组织的业务,都会在创建任何内容之前被拒绝。代理断言走恰好一次的 POST /customers/action/quotation-draft-prepare/op_<hash> 回执路由,其幂等回执在创建报价单之前先行占位,因此重放的确认只会返回同一份草稿,而不会创建第二份。令牌通过两步式的 crm.engagement.quotation-draft.prepare({customer_id, opportunity_id, selection_id, valid_until, currency?})访问,在任何权限级别下都需要人工确认。
业务里程碑与寄样记录(#4282)
业务上的三项事实均由记录推导,绝不直接设置:GET /crm/engagements/:id/milestones 返回 {quoted, sample_sent, po_received, quoted_draft}。quoted 与 sample_sent 各自为 null,或为最早满足条件记录的 {at, ref_id, label}——前者取已设置 sent_at 且 voided_at 为空的报价单,后者取已设置 shipped_at 的寄样记录。quoted_draft 在业务上存在草稿报价单但尚未发送任何报价单时为 true,因此业务页可以显示“草稿,尚未发送”而非简单的“尚未”。po_received 取关联到本业务(engagement_id 指向它)且未删除的最早一份 po 类客户文档;当文件完全没有 engagement_id 时,仅在该客户只有一个进行中业务时才计入——客户名下有多个进行中业务且文件未显式关联时,该文件对任何一个业务都不计入,从而避免归属到错误的业务。调用方组织之外的业务返回 404 {"error":"not_found"};没有任何符合条件记录的业务返回 200 及三项 null。没有任何接口接受直接设置里程碑的值:作废报价单、删除寄样记录或移除文件都会让对应行回到 null。
寄样记录通过 POST /crm/engagements/:id/samples {description, shipped_at?, tracking?} 创建,返回 201 及创建的记录;GET /crm/engagements/:id/samples 按最新在前列出该业务下所有未删除的寄样记录。未填写 shipped_at 创建的寄样记录处于“已备样、尚未发货”状态,在 PATCH /crm/engagements/:id/samples/:sampleId 设置该字段之前不满足里程碑条件;同一路由也可编辑 description 与 tracking。DELETE /crm/engagements/:id/samples/:sampleId 对记录做软删除,若它正是该里程碑的来源,sample_sent 会回到 null。样品 ID 若不属于路径中指定的业务,返回与不存在时相同的 404。每个寄样路由要求与该业务阶段变更相同的组织成员权限,且每次读写都限定在该业务所属的组织范围内。
赢单核实(#4285)
Atlas 会留意采购订单、提取其内容,并将其与业务并排展示,但绝不会自行把业务标记为赢单。GET /crm/engagements/:id/verification 对调用方可读的业务始终返回 200——没有待核实记录时为 {verification: null},否则返回待核实记录(其 document 含 {id, filename, source}:上传的文件 source 为 null;由邮件归档的采购订单则为 {kind: "email", comm_item_id, sender_name, sender_email, subject, received_at},即邮件的发件人,而非文件被记在谁名下),并可能附带一个 comparison 数组,每行为 {field, po_value, deal_value, match},字段取 customer/product/quantity/amount/delivery/terms 之一(只要任一侧为 null,match 就是 "unknown",绝不猜测;其余为 true 或 false);调用方组织之外的业务返回 404。POST /crm/engagements/:id/verification/:vid/confirm {set_amount_from_po?, set_close_today?, partial?} 是唯一能写入赢单的接口,且只有该业务的负责人或组织的所有者/管理员/经理才能调用——其他人返回 403 forbidden;成功时返回 {ok: true, status: "confirmed" | "confirmed_partial"},已处理过的核实记录返回 409 {"error":"already_resolved"}。POST /crm/engagements/:id/verification/:vid/decline {reason, note?} 记录这份 PO 为何还不能让业务成为赢单,原因取自固定集合 sample_or_tooling/under_review/wrong_deal/still_negotiating——未知取值返回 400 {"error":"invalid_reason"},note 超过 300 字返回 400 {"error":"note_too_long"}——成功时返回 {ok: true, status: "declined"}。没有任何路径会自行把业务移到赢单:发现的 PO 只会让 GET /crm/engagements/:id 带上 pending_verification: {id, po_number};一笔业务若靠人手直接改成赢单、从未核实过任何 PO,则在同一接口读到 won_without_po: true——这两个字段都只出现在单笔业务的读取中。
GET /crm/today 的响应在既有 items 之外新增 to_verify 数组,每个待核实赢单一行:{engagement_id, title, customer_name, po_number, amount, currency, created_at}。GET /crm/team/members 的 members 每行新增 unverified_wins——该成员名下停留在“赢单·待核实”超过 3 个工作日的业务数,没有则为 0。迁移 0121_engagement_verifications.sql 新增了支撑以上全部功能的数据表。
所有者报价决策
组织所有者可以让超过阈值的报价在面向客户发出之前先经过自己。路由启用后,发送存储合计超过配置阈值的草稿报价——或其币种与配置的阈值币种不同(Atlas 没有汇率,外币一律拦截)——时,返回 409 {"error":"decision_required","decision":{…}} 而不发送:报价保持草稿状态,并创建一条待决策记录,携带报价编号、版本、币种金额、发起人与请求时间。自 #2982 起账本按报价内容(版本、合计、币种)为键:批准后合计在草稿中被就地修改时,会解析到一条全新的决策;同一报价同时至多一条待决策;已决策记录永不更改。所有者按内容批准或驳回:批准的决策仅在报价当前版本、合计与币种仍与已批准记录一致时放行再次发送(sent 事件记录 decision_id),内容已变更则重新拦下并生成新的待决策;被驳回的内容保持未发送,再次发送返回 409 {"error":"decision_rejected"},而变更后或新创建的内容会被重新评估。若路由配置无法读取,发送以 503 {"error":"routing_unavailable"} 失败关闭,且不写入任何数据。路由未启用、未配置,或合计未超过阈值时,发送与引入门槛前的构建逐字节一致,不产生决策记录。会话路由仅所有者可用(无所有者会话标志的调用方返回 403 {"error":"forbidden"};按组织隔离,外部组织的决策 ID 与不存在一样返回 404):GET /crm/owner/quote-decisions 按最新在前列出待决策与已决策项,附客户名称与报价当前状态;GET /crm/owner/quote-decisions/routing 与 PUT /crm/owner/quote-decisions/routing({active, threshold_minor, currency})读取与设置每组织唯一的配置;POST /crm/owner/quote-decisions/:decisionId/decide({outcome: "approved" | "rejected"})记录决策——已决策行不可变,没有其他写入口。GET /crm/owner/quote-decisions/:decisionId/evidence(#2982)为该决策组装单页证据包——洽谈与报价历史、付款记录、确认的应收余额、最近一次实付交易,每个板块标注其来源记录 ID。没有记录来源的内容(如今的单品最近成交价与成本基础)一律显示为命名缺口,绝不推算;来源读取失败时返回 503 {"error":"pack_assembly_failed"} 而非残缺的页面,且证据包严格只读。
货运进度跟进(#2112)
依据每票货物最近一次人工确认的承运方与运单证据跟进运输,并显示每个状态的核实时间——每次观察都携带 as-of 时点,带时点的记录绝不冒充实时的物流追踪。一票货物挂在 A09 订单身份(交期跟进案例,原样引用买方的订单号)加一个人工填写的货物编号下,一次性派生出稳定的 shipment_key;不同编号保持各自的历史,同一订单的多票货物互不合并。每次确认记录状态——in_transit(运输中)、exception(异常)或 delivered(已签收)——以及承运方、运单号或明确的未知(缺失的细节保持未知,绝不臆造)、核实时间、引用文件和备注。已签收只停止该票货物自身的 order_shipment 跟进;异常仍是跟进候选,交期承诺绝不会被查询或自动了结。这里没有承运方集成,也没有 ERP 写入:校验拒绝不落任何数据、不发起任何查询、不关闭任何跟进。
会话路由均受客户可见范围约束:GET /crm/customers/:id/engagement/order-shipments 列出该客户名下货物案例及其最近确认的进度与 as-of 时间;GET /crm/customers/:id/engagement/order-shipments/:caseId 查看单个案例的完整观察历史(最新在前);POST /crm/customers/:id/engagement/order-shipments 与 POST /crm/customers/:id/engagement/order-shipments/:caseId/observations 对案例根(给出订单案例、货物编号和订单文件时开新案)或既有案例记录 {status, carrier?, tracking_reference?, observed_at?, source_refs?, note?}——订单与编号必须匹配——201 返回 {case, observation, effect},其中 effect.cancelled_schedule_ids 列出已签收停止的跟进;拒绝以 404/409/400 返回具体原因。智能体断言走同样的读取与恰好一次的 POST /customers/action/order-shipment-confirm/op_<hash> 回执接口。
客户平台销售回顾(#2114)
只依据已确认的报告,回顾我方产品在客户各平台的销售表现。每条版本化观察以客户、产品、客户自己的平台标注和期间为键,每个指标都带有分类——reported(报告值,须引用来源)、estimated(估算值,须写明方法及佐证)或 missing(缺失,须说明原因)。标注永不合并,不同单位或币种的数值留在不同比较分组且无跨组合计,零或缺失的基期绝不产生虚构增长率,A11 核对记录只按引用复用——仅计入一次,绝不复制为额外销售。被撤回的引用文档会同时阻止记录与起草,且绝不抓取、查询或发布到任何外部平台。
会话路由:GET /crm/customers/:id/engagement/platform-sales 按新到旧列出已记录观察(复用证据只解析一次,并附同口径比较分组);POST /crm/customers/:id/engagement/platform-sales 记录一条观察。代理断言走同一表面:GET /customers/action/platform-sales-reviews、恰好一次的 POST /customers/action/platform-sales-prepare/op_<hash> 与 POST /customers/action/platform-sales-record/op_<hash> 回执路由。F06 目的 platform_sales_review(发生标识 psales:<customer_id>:<YYYY-MM>)每月至多准备一份有引用的回顾草稿;F05 计划目的只按日历月或一次性日期运行。
投标产品推广(#2124)
依据一条现行、已确认的投标简报,销售可准备一封将已确认投标要求映射到已批准产品证据的客户产品推广草稿。简报必须仍然存在、未过截止时间,且其引用的证据仍能作为已确认证据解析;已过截止、失效或不可访问的简报不会产生可发送草稿。草稿只覆盖已批准产品依据支持的要求,并向审阅者明确列出未支持的要求。仅为沟通内容——绝不构成投标提交,也不在证据之外承诺合规、价格、供货或中标。代理断言经由 POST /customers/action/tender-product-promotion-prepare/op_<hash>:调用方稳定的 request_id 重放同一发生;当同一投标的更早发送仍处于保留、发送中或未知状态时,新请求必须等待(业务键 org/customer/tender_product_promotion/customer:<tender_reference>)。
连接私人邮箱
GET /crm/integrations?accounts=omit 向有效成员提供安全的邮箱及已启用 Gmail 设置资料。can_create_personal 要求实时 CRM 写入权限,且组织未明确停用私人账号;创建共享账号仍需集成管理权限。私人 OAuth 将账号类型保存在一次性授权状态中,回调时重新确定所有者,不会将现有共享账号转换为私人账号。GET /crm/inbox/accounts 返回可信的 account_kind:personal、shared,或缺少可信元数据时的 null。此字段仅描述可见范围,不授予权限。共享授权、发信审批、凭据检查和既有休眠账号的限制保持不变。
平台协助接入(集成)
三个受花名册保护的端点供平台超级管理员代表单个组织操作其沟通集成;组织始终取自路径,每个动作都会入审计。 GET /crm/platform/organisations/{orgId}/integrations 返回渠道目录与该组织的连接,连接附带状态,以及由 Atlas 支持配置时的 support_attribution。 POST /crm/platform/organisations/{orgId}/integrations/{provider}/connect 以 Atlas 支持身份执行与该组织管理员完全相同的引导式连接; POST /crm/platform/organisations/{orgId}/integrations/{connectionId}/test 运行该组织的连接检测。三个端点均要求已验证的平台超级管理员会话——任何组织角色都不被授权——否则返回 403。任何响应都不会携带已存储的凭据;审计写入是前置条件,审计被拒时返回 503 且不执行任何操作。 自 #1870 起,邮件渠道通过同一流程接入:引导式连接将整套邮件凭据(收信与发信服务器及各自的端口和加密方式、邮箱账号、应用专用密码、发件人地址、读取历史窗口)整体加密封存,在写入任何内容之前即拒绝端口 25 以及端口与加密方式不一致的组合;邮件连接的检测响应在 check 中新增 observedCount、windowDays 和封闭的 cause 标记。任何响应或审计行都不会携带应用专用密码。
报价结果跟进与确认(#2107)
记录客户对某一已发送报价版本的已确认答复。答复取值为 accepted、rejected、changes_requested 或 undecided,并携带客户反馈文本、得知答复的时间以及逐字保留的证据引用。反馈以 CRM 自有的版本化备注形式关联到该版本——绝不另建并行的报价状态表。接受或拒绝会在调用者明确确认后触发既有的受保护报价生命周期(#879/#880)并返回其规范状态;此类答复至少需要一条证据引用。changes_requested 保留反馈并关联既有的版本操作而不创建任何内容;undecided 保持该报价的跟进可执行。任何拒绝——报价不存在、草稿或已过期、已有结论——都不会落库。
会话路由均受客户可见范围保护:GET /crm/customers/:id/engagement/quotation-result 列出该客户仍未决(实际仍为已发送)的报价及其最新确认备注;GET /crm/customers/:id/engagement/quotation-result/:quotationId 返回一个报价的完整备注历史(最新在前);POST /crm/customers/:id/engagement/quotation-result/:quotationId 记录 {response, feedback?, observed_at?, source_refs?}——成功返回 201 与 {quotation, note, effect},其中接受/拒绝的 quotation 即生命周期规范状态,effect.revision_action 为要求修改时单独的创建版本操作;拒绝按 404/409/400 返回具名原因。代理断言调用同样的读取以及恰好一次的 POST /customers/action/quotation-result-confirm/op_<hash> 回执接口。
样品选品跟进与结果(#2109)
跟进单个样品的选品结果,并在 A07 样品身份——客户样品文件加显式样品编号,一次性导出为稳定的 sample_key——之上记录确认的答复;不存在任何履约表。结果取值为 selected、not_selected 或 pending,同时携带确认日期、证据引用与人工反馈,以及产品决策:选中和未选中的产品料号及所述原因,或显式的“未提供”标记。选品与技术测试结果的存储和跟进相互独立——两者互不解决对方——最终的已选中/未选中仅停止该样品自己的选品跟进;无关沟通永远不会代替它。每个产品料号必须能在该客户自己的记录中解析,这里绝不改动任何订单或产品记录。校验拒绝不落库、不关闭任何跟进。
会话路由均受客户可见范围保护:GET /crm/customers/:id/engagement/sample-selection 列出该客户的选品案例及其最新确认结果;GET /crm/customers/:id/engagement/sample-selection/:caseId 返回单个案例的完整观察历史(最新在前);POST /crm/customers/:id/engagement/sample-selection 与 POST /crm/customers/:id/engagement/sample-selection/:caseId/observations 在案例根(给定样品文件与编号时新建)或既有案例上记录 {result, observed_at?, feedback?, source_refs?, selected_product_refs?, rejected_product_refs?, reasons?, products_not_provided?}——成功返回 201 与 {case, observation, effect},其中 effect.cancelled_schedule_ids 列出最终结果停止的日程;拒绝按 404/409/400 返回具名原因。代理断言调用同样的读取以及恰好一次的 POST /customers/action/sample-selection-confirm/op_<hash> 回扥接口。
席位额度与员工钱包授权
POST /governance/platform/billing/allowance-grants 需要已验证的平台超级管理员会话和同源 JSON,请求体为 {"orgId","reason","requestId"}。该操作按请求时刻所在的上海日历月读取运营方维护的订阅记录,并为 Auth 返回的每个在用席位,在其 allowance:<email> 钱包至多追加一条 reset 流水(清零周期结转余额)和恰好一条 grant 流水(金额为 allowance_tokens_per_seat)。确定性幂等键 allowance-grant/allowance-reset 加上流水的 UNIQUE(组织, 键)约束保证同一周期重复执行不会追加任何新流水;重放同一 requestId 返回已记录的摘要。订阅缺失或每席位额度为空时返回 {"allowancePerSeat":null,"seats":[]} 且不追加任何记录;金额永远来自配置而非代码。响应为 {orgId, periodStart, periodEnd, allowancePerSeat, seats:[{email,wallet,grantedTokens,resetTokens}], replayed}——追加了流水返回 HTTP 201,重放返回 200——同时写入一条 wallet.allowance_apply 平台审计记录,保存钱包前后摘要。
授权完全自助:POST /governance/billing/wallet-opt-in 记录登录成员本人的同意(含操作人与时间),DELETE /governance/billing/wallet-opt-in 撤回。两者均幂等,且当请求体 email 不是调用者本人时返回 403;任何管理员都不能替他人记录同意。GET /governance/billing 为拥有账单读取权限的调用者增加 optIns.self,为账单管理员增加 optIns.members(全部成员钱包及其授权状态)。授权只作用于扣费顺序中员工本人的钱包——未记录同意时该钱包绝不被扣费;席位额度与组织钱包不受影响。
互动计划
一条已接受的互动计划是单个客户、单个用途的重复规则:在同一客户内解析的业务记录、明确的首次到期日(first_due_on)、用于比较仅日期到期值的 IANA 时区(绝非 UTC 午夜),以及 one_time、monthly 或 event_relative 之一。月度重复保留既定的日锚点并在短月收紧(1 月 31 日 → 2 月 28/29 日 → 3 月 31 日)。event_relative 计划从某个已确认的来源日期倒推显式输入的 lead_days —— date_ref_kind/date_ref_id/date_ref_field 指向一个已确认的订单、应收或报价日期 —— 并保存该来源的修订号作为 source_version:来源随后被编辑会使旧的到期依据失效、停止到期工作,必须由人工重新接受后才恢复准备。每次变更都按 expected_revision 比较并交换地版本化;不匹配返回 409 version_conflict 并保留胜出方。暂停、取消与修订会停止未来的准备,且不删除历史到期记录。保存或变更计划不准备、不发送任何内容,也不创建 Task。
会话路由:GET /crm/customers/:id/engagement-schedules 列出客户的互动计划(可选 status 过滤,可选 schedule_id 查看单条计划及其到期历史),POST /crm/customers/:id/engagement-schedules 应用一个操作({operation, …}:accept 携带完整依据 —— purpose、business_ref_kind、business_ref_id、recurrence、first_due_on、timezone,event_relative 另需 lead_days 与日期引用;update 修订指定字段;pause 与 cancel 需要 schedule_id 和 expected_revision)。调用方不可见的客户与未知客户返回相同的 404,因此当前失去可见性的销售代表无法变更。令牌通过受治理读取 crm.engagement.schedules.read 与两步的 crm.engagement.schedules.change(operation:accept、pause、cancel、update)访问同一表面。
industry_technology_digest 用途(#2125)为单个客户排定重复的行业与技术摘要计划。它绑定客户本身(business_ref_kind: "customer" 指向该客户),其到期标识为 schedule/revision/due period,因此修订后的计划会规划全新的到期事项。某个到期事项触发时,代理门控仅通过 EKB 绑定检索组织自身知识,并至多保留五条摘录,每条必须同时满足:(1)其精确声明定位符当前带有可对该客户分享的已确认使用授权(F08);(2)与该客户已确认的行业或技术事实重叠,并以此构成明确的契合理由(F01)。系统绝不调用浏览器、外部订阅源或研究接口。没有合格的配对时,该到期事项记录可见的跳过原因 no_eligible_relevant_source 且不创建草稿。已准备的草稿在审阅对话框中展示每个来源、摘录与契合理由,之后才允许编辑、复制或显式发送;批准与发送前会实时重新解析每个授权,外发正文不携带任何内部标识符或来源定位符。
互动准备
互动准备是已采纳计划在后台的有界运行状态:对每个到期的业务事项(标识为 org/customer/schedule/revision/due period,按需请求则为 org/customer/purpose/business_ref/request_id),至多存在一个待审结果——一条互动草稿,或一个带结构化 reason 的可见 skipped / failed / stopped 状态。claim 是带过期租约的持久行,并发的 tick 与重试因此收敛为一个结果;错过的到期事项收敛为只准备最新未完成的一个,更早的保留可见的 superseded 原因。权限、功能可见性、计划修订与客户状态等阻断会在任何效果之前记录具体的 stopped 状态;已证实的失败(例如来源消失)记录可执行的 failed 状态,仅由经确认的重试动作重新驱动。准备从不发送、从不批准,也不创建 Task 或付费余额。
会话路由:GET /crm/customers/:id/engagement-preparations 列出客户的准备行(可选 status 与 schedule_id 过滤),POST /crm/customers/:id/engagement-preparations 按需准备一个 occurrence({purpose, business_ref_kind, business_ref_id, request_id, recipient_contact_id?})——重复的 request_id 返回同一结果,新的 nonce 不能绕过该业务记录的待审工作。令牌通过受治理读取 crm.engagement.preparation.read 与两步的 crm.engagement.preparation.retry 访问同一表面:重试在人工确认后重新驱动一条已证实失败的行(customer_id、preparation_id),重试永不发送。
当前洽谈报价(报价洽谈)
客户的当前洽谈报价是客户与正在洽谈的报价单之间明确且留痕的关联。一条关联只绑定两者之一:Atlas 报价单的准确版本(quotation_id,每个版本只能有一条进行中的关联),或在 Atlas 外制作的报价单——一份客户文档(document_id)加上可选的展示信息(quotation_number、revision_label、amount_minor、currency、valid_until)。系统不做任何推断:新建版本、发送报价单或上传文件都不会自动把报价放入当前洽谈区,关联文档也不表示它已被发送、被接受或正在洽谈。金额与币种逐条独立显示,绝不合并合计。
会话路由(客户可见范围保护:越权或跨组织客户同样是 404):GET /crm/customers/:id/quotation-negotiations 列出进行中的关联及其报价单/文档的实时状态和派生的 expired 标记(超过有效期的已发送报价保持列出,直至被替换或移除);POST /crm/customers/:id/quotation-negotiations 关联 {opportunity_id, quotation_id} 或 {opportunity_id, document_id, …可选信息}——已被洽谈的版本返回 409 conflict,跨客户或不匹配的绑定返回 404,信息格式错误返回 400;DELETE /crm/customers/:id/quotation-negotiations/:negotiationId 明确移除;POST /crm/customers/:id/quotation-negotiations/:negotiationId/replace({quotation_id})在同一事务中把当前关联结束为“已替换”并建立新关联——新建版本绝不会自动替换洽谈中的报价,并发操作只允许一个胜出(失败方读到 409,胜出方保持不变)。当被关联的报价单通过既有报价路由被接受或拒绝时,其关联自动以该结果结束;报价单本身仍保留在业务的报价历史中。每次关联、替换、移除和生命周期结束都会追加审计事件和客户时间线记录(类型 quotation_negotiation)。DELETE /crm/customers/:id/quotations/:qid 彻底删除该客户的一份草稿报价单,其明细行、审计事件和洽谈关联一并删除,返回 {"deleted": true}。非草稿的报价单返回 409 quotation_not_draft 且不做任何变更:已发送报价单的明细行不可变。未知的报价单 ID、属于其他客户的报价单和属于其他组织的报价单,都返回相同的 404。 非草稿的报价单改为“作废”而非删除(#4194):POST /crm/customers/:id/quotations/:qid/void 把它移出工作列表——本列表、销售记录和业务的报价列表——而其记录、明细行和事件原样保留,返回 {"voided": true, "changed": true}(若已处于作废状态则 changed: false)。POST /crm/customers/:id/quotations/:qid/unvoid 原样恢复。草稿返回 409 quotation_is_draft——草稿只删除,不作废;跨客户、跨组织同样返回相同的 404。已作废的行可通过 GET /crm/customers/:id/quotation-negotiations?include_voided=1 和 GET /crm/engagements/:id/quotations?include_voided=1 再次取回,这正是找回并恢复一份已作废报价单的方式。作废不是状态:报价单仍显示为 sent 或 accepted,因为客户收到的确实是那一份。该动作记录为客户时间线条目(类型 quotation_void),而非报价单生命周期事件,因为它并未改变该文件的任何法律事实。
连接私人邮箱
GET /crm/integrations?accounts=omit 向有效成员提供安全的邮箱及已启用 Gmail 设置资料。can_create_personal 要求实时 CRM 写入权限,且组织未明确停用私人账号;创建共享账号仍需集成管理权限。私人 OAuth 将账号类型保存在一次性授权状态中,回调时重新确定所有者,不会将现有共享账号转换为私人账号。GET /crm/inbox/accounts 返回可信的 account_kind:personal、shared,或缺少可信元数据时的 null。此字段仅描述可见范围,不授予权限。共享授权、发信审批、凭据检查和既有休眠账号的限制保持不变。
平台协助接入(集成)
三个受花名册保护的端点供平台超级管理员代表单个组织操作其沟通集成;组织始终取自路径,每个动作都会入审计。 GET /crm/platform/organisations/{orgId}/integrations 返回渠道目录与该组织的连接,连接附带状态,以及由 Atlas 支持配置时的 support_attribution。 POST /crm/platform/organisations/{orgId}/integrations/{provider}/connect 以 Atlas 支持身份执行与该组织管理员完全相同的引导式连接; POST /crm/platform/organisations/{orgId}/integrations/{connectionId}/test 运行该组织的连接检测。三个端点均要求已验证的平台超级管理员会话——任何组织角色都不被授权——否则返回 403。任何响应都不会携带已存储的凭据;审计写入是前置条件,审计被拒时返回 503 且不执行任何操作。 自 #1870 起,邮件渠道通过同一流程接入:引导式连接将整套邮件凭据(收信与发信服务器及各自的端口和加密方式、邮箱账号、应用专用密码、发件人地址、读取历史窗口)整体加密封存,在写入任何内容之前即拒绝端口 25 以及端口与加密方式不一致的组合;邮件连接的检测响应在 check 中新增 observedCount、windowDays 和封闭的 cause 标记。任何响应或审计行都不会携带应用专用密码。
席位额度与员工钱包授权
POST /governance/platform/billing/allowance-grants 需要已验证的平台超级管理员会话和同源 JSON,请求体为 {"orgId","reason","requestId"}。该操作按请求时刻所在的上海日历月读取运营方维护的订阅记录,并为 Auth 返回的每个在用席位,在其 allowance:<email> 钱包至多追加一条 reset 流水(清零周期结转余额)和恰好一条 grant 流水(金额为 allowance_tokens_per_seat)。确定性幂等键 allowance-grant/allowance-reset 加上流水的 UNIQUE(组织, 键)约束保证同一周期重复执行不会追加任何新流水;重放同一 requestId 返回已记录的摘要。订阅缺失或每席位额度为空时返回 {"allowancePerSeat":null,"seats":[]} 且不追加任何记录;金额永远来自配置而非代码。响应为 {orgId, periodStart, periodEnd, allowancePerSeat, seats:[{email,wallet,grantedTokens,resetTokens}], replayed}——追加了流水返回 HTTP 201,重放返回 200——同时写入一条 wallet.allowance_apply 平台审计记录,保存钱包前后摘要。
授权完全自助:POST /governance/billing/wallet-opt-in 记录登录成员本人的同意(含操作人与时间),DELETE /governance/billing/wallet-opt-in 撤回。两者均幂等,且当请求体 email 不是调用者本人时返回 403;任何管理员都不能替他人记录同意。GET /governance/billing 为拥有账单读取权限的调用者增加 optIns.self,为账单管理员增加 optIns.members(全部成员钱包及其授权状态)。授权只作用于扣费顺序中员工本人的钱包——未记录同意时该钱包绝不被扣费;席位额度与组织钱包不受影响。
GET /governance/billing/windows(#2609)返回登录账户的滚动用量窗口:记录在案的套餐等级,以及每个窗口(5 小时与 7 天)从用量账本按调用者邮箱跨组织汇总的客户信用点、套餐限额和恢复时间。拥有账单读取权限的调用者可查看本人状态;账单管理员额外获得 members(本组织账本可见的每位成员及同样的窗口状态)。无账单读取权限时返回空形态({"tier":null,"self":null,"members":[]});限额来自记录在案的 plan_tiers 行,绝不写死在代码里。
审阅后可召回标记与可召回读取
管家(管理员与经理)决定一条已批准的信息记录何时可被授权服务召回。POST /governance/memory-review/records/:recordId/review-eligible 携带空 JSON 请求体,为处于 active 状态、且属于管家本人审阅用途的已批准记录写入唯一一条"审阅后可召回"标记;响应返回该记录与标记。标记单向:只有记录被限制、更正取代或擦除时才会失效,既有记录不会被自动标记或自动返回。
每次请求使用新的幂等键,因此对已标记记录再次标记会得到终态结果 409 {"error":"already_marked"} —— 重复标记必须是有意为之,绝不静默。记录不存在或属于其他组织返回 404;非 active 或批准用途不符返回 409;非空请求体返回 400。代表(member 角色)的 POST 返回标准 403 拒绝且不产生任何变更。
GET /governance/memory-review/eligible 仅返回仍具备资格的已标记记录,形状为 {"items":[…],"next_cursor":null}。用途固定为管家本人的审阅用途;可选过滤参数为 subject_kind + subject_id(成对精确匹配)、sensitivity 与 limit(1–100)。敏感级默认拒绝:省略 sensitivity 时仅返回 normal 记录,需显式 sensitivity=sensitive 才返回敏感记录,其他取值返回 400。批准候选(POST /governance/memory-review/candidates/:id/approve)接受可选 {"sensitivity":"normal"|"sensitive"}(默认 normal),发布后的记录保持该敏感级;被更正的记录不会从 sensitive 降级为 normal。member 的 GET 返回空集合形状 200 {"items":[],"next_cursor":null}。每次标记与读取都进入内存审计留痕。
更正、限制与遗忘已召回记录
管家(管理员与经理)对已召回的经验记录执行生命周期控制:POST /governance/memory-review/records/:recordId/correct 用更正后继记录取代原记录(请求体为生命周期更正模式——provenance、validity、retention、metadata、content);.../restrict 空 body 将记录移出召回而不删除;.../erase 以墓碑形式遗忘;.../holds 设置法律保留({"reason_code","authority_ref"});POST /governance/memory-review/holds/:holdId/release 解除保留。POST /governance/memory-review/retention-sweep 携带 {"as_of"},恰好遗忘已过保留期的记录并跳过带有效保留的记录。
每项控制在下一次读取时立即生效:被限制、被取代或已遗忘的记录不会再出现在可召回读取中。更正保留记录的主题、用途与敏感级,且绝不携带"审阅后可召回"标记——后继记录须经管家重新标记才可召回。擦除带保留的记录返回 409 {"error":"active_legal_hold"} 并附保留 id,解除保留前不会删除任何内容。记录不存在或属于其他组织返回 404;非法状态转换(重复限制、重复擦除、重复释放、未来的 as_of)返回终态 409。代表(member 角色)POST 返回标准 403 拒绝且不产生任何变更。每项控制都进入内存审计留痕。
GET /governance/platform/skills/:id/quality?orgId=…&version=…&from=…&to=… 将汇总和已封存报告限定为所选组织、技能版本及时间范围。版本筛选先于 2,000 份报告上限执行,避免新版本挤掉历史比较证据。超出上限时会明确提示;不同币种保持独立,CSV 成本保留精确纳单位。
助手上传路由
助手上传可返回 stage_directive 事件,其中 kind: "upload_result",结果位于 fields.uploadReport.rows。每行包含 artifactId、filename、category、workflow,以及可选的 libraryFileId、issue、reviewPath。普通文件打开我的文档;财务文件和客户名录分别提供审核入口。原文件已保存不代表已提交客户或销售记录。无法读取或类型不明的文件仍会保留,并明确说明限制。客户端须校验内部审核路径,并按当前身份隔离结果。旧版分类卡片可携带 library_file_id,以复用已上传的原文件,避免重复上传。
仅存入我的文档的助手附件
超过 CRM 附件读取大小限制的文件可通过 POST /agent/chat 的 artifacts 传递:id 使用原生 libf_ 加 64 位小写十六进制标识,同时提供相同的 library_file_id 和 filename。普通附件仍使用 art_ 标识。服务器通过文档库核实当前访问权限和个人所有权后才确认保存。默认上传流程打开我的文档并提示未处理,不读取不存在的附件,也不根据文件名猜测客户。缺失、无权访问或不属于当前用户的原件不会显示为已保存。
助手对话历史
业务代表的助手会话由 agent 支柱保存,不再只存在于浏览器。GET /agent/assistant/threads 返回 {"items":[…]},即调用者本人的会话列表,按最近更新倒序,最多二十条,每条包含 id、title、archivedAt 与 messages。GET /agent/assistant/threads/:id 以 {"item":…} 返回单个会话。PUT /agent/assistant/threads/:id 以 {"title","messages","archivedAt","lastTaskStatus","context"} 写入:携带 messages 时整体替换记录,仅携带 title 时重命名已存在的会话;重命名不属于自己的会话返回 404,绝不因此新建。可选的 archivedAt(Unix 毫秒)表示客户端认定的最后更新时间,将作为该会话保存的时间顺序依据,使上传的历史对话保留原有次序,而不是全部挤到同一时刻;实时会话轮次不携带该字段,超前于服务端时钟的取值会被回拨,而不是拒绝。DELETE /agent/assistant/threads/:id 删除该会话及其全部消息,下一次读取在任何设备上都不再列出。浏览器已决定但未能送达的删除(离线或 5xx),会在下一次读取之前重新发起,期间该会话在本浏览器中始终保持隐藏:失败的删除会被重试,而不会被读取撤销。
组织与用户始终取自已验证会话,绝不来自请求输入。同组织的其他成员以及其他组织的任何成员,从集合读到 200 {"items":[]},从单个会话、重命名或删除读到无法区分的 404 {"error":"not_found"}。未登录的集合读取同样是诚实的空列表;未登录的写入返回 401。本能力上线之前创建的对话,会在持有它们的浏览器首次打开助手时一次性上传,过程中不会删除该浏览器中的任何对话。历史不提供 MCP 与 CLI 适配器——它是登录用户自己的对话界面,而非受治理的业务动作。
会话事件日志
每个会话在服务端还有一份事件日志,在会话轮次进行时写入。对话轮次通过 POST /agent/chat 请求体中可选的 thread_id 指明所属会话(1–64 个字母、数字、_ 或 -;其他取值返回 400;可与 operational_kinds 同时出现,该轮同样记录日志并校验所有者)。日志随后在回复开始前记录一条 user_message 事件({"text","context"}),再为每个流式帧记录一条事件,直到 message_stop,每条事件带一个序号。序号在会话内唯一且严格递增,但各轮之间不连续:每轮开启时预留一段序号,未用完的序号成为空缺。帧一经编号即发送,随后写入日志,因此仍在进行的轮次的事件可能稍晚于浏览器看到它们才出现;after 游标按序号读取,不受空缺影响。同一会话的轮次交叠时,事件读取只返回不会再变化的有序前缀:较晚交叠轮次的事件在较早轮次结束(或 30 分钟无活动)后才出现,head 止于该前缀。未以 message_stop 结束的轮次(失败、断开)在其流结束时关闭;轮次关闭或 30 分钟无活动后不能再追加事件(关闭时写入的唯一一条收尾事件除外,其序号高于此前所有事件,见“断线续传与停止”),因此已返回的 head 之下不会再出现新事件。发送给客户端的每个帧都以 seq 携带其事件序号,message_start 还携带 thread_id。不带 thread_id 的请求与以往完全一致,不写日志。该轮日志开启后,每个帧都带 seq;某个帧写入日志失败时,该帧已照常发送,只是日志中缺少这条事件。帧从不等待日志:下一段序号在后台预留,若当前段用完时尚未就绪,帧在其就绪前不带 seq 发送。若在轮次开始时无法访问日志或 2 秒内未响应,该轮在任何模型调用之前返回 503,除非调用者本人已保存的会话记录证明其为会话所有者;此时该轮正常回答,帧中不带 seq。
GET /agent/assistant/threads/:id/events?after=<n> 返回 {"items":[…],"head":<n>}:序号 n(默认 0)之后的事件,按从旧到新排列,每页最多 500 条,每条包含 seq、kind、data、run_id 与 at(ISO 时间);head 为安全游标:本次读取已完整覆盖到的最高序号。继续读取时把 after 设为 head。一页满(500 条)时,head 为该页最后一条事件的序号,下一次读取从其后开始。只有会话所有者可以读取:同组织的其他成员、其他组织的任何成员或未登录调用者读到 404 {"error":"not_found"};携带他人 thread_id 发送的对话轮次在任何模型调用之前返回同样的 404;无法校验会话所有者时,该轮在任何模型调用之前返回 503。与会话记录相同,若日志中的回答使用了私有邮箱内容,调用者须对每个来源邮箱持有当前授权才能读取,否则返回同样的 404。该校验覆盖会话曾记录的每一轮使用过邮箱内容的回合及日志开启以来已保存会话记录中出现过的每一轮(改写会话记录不会缩小该范围),而不仅是本页返回的事件,因此用 after 跳过这类轮次也不能绕过。这些路径中的会话 id 只解码一次,所有校验都使用同一个 id,因此 %2D 与 - 指同一会话;id 只解码一次,因此含 % 的已存 id 以 %25 请求;解码后含 /、控制字符或 Unicode 格式字符(如零宽或双向标记)的 id 返回 404。会话的首个轮次或首次保存会话记录即为发送者一次性永久认领该会话:已由其他成员保存的会话,即使尚无日志,也不能被他人记录;对其他成员已开启日志的会话执行 PUT /agent/assistant/threads/:id 返回同样的 404 且不保存任何内容(无法校验认领时返回 503,同样不保存)。只有携带 messages 且会被接受的保存才会认领会话;仅含标题、格式错误或超出上限的保存不认领任何会话。无法读取所涉回合的来源,或会话记录写入本身失败时,保存返回 503 {"error":"unavailable"},已存会话记录保持不变。所有者删除会话后,其仍以该 thread_id 发送的轮次正常回答但不记录日志(不带 seq),事件读取仍返回 404。DELETE /agent/assistant/threads/:id 会先删除该日志:日志无法删除时返回 503 并保留会话,以便浏览器重试;已删除的日志不会被仍在运行的回合重新创建。从未有过日志的已保存会话同样先为其所有者认领再删除日志,因此该 id 不会再被记录。只有日志、从未保存会话记录的会话删除时返回 200 {"item":{"id","deleted":true}}。重复已完成的删除返回 404。超出会话上限的修剪只删除已保存的会话记录;该会话的日志在所有者删除会话之前仍可由所有者读取,其邮箱来源校验继续生效。无法读取从未记录日志的会话的来源,或删除会话记录失败时,删除返回 503 {"error":"unavailable"},重试即可完成。日志或其邮箱范围无法读取时,事件读取本身返回 503 {"error":"unavailable"}。每个会话保留最新的 5,000 条事件,超出时按整轮删除最早的已完成轮次(轮次交错时亦然),仍在进行的轮次绝不删除,序号绝不复用。
断线续传与停止
记录日志的轮次在浏览器断开后继续运行:其帧仍会写入日志直到 message_stop,以平台在响应结束后允许的工作时间(约 30 秒)为限。GET /agent/assistant/threads/:id/events?after=<n>&wait=1 在读取结果中没有 n 之后的事件、且 head 不会越过 n 时,最多保持请求 25 秒,然后按上文格式返回(期间没有新事件时 items 为空)。读取结果还包含 open_runs,形如 [{"run_id","idle_ms"}]:每个仍在进行的轮次,及其距上次记录事件或被该轮心跳报到已过去的毫秒数(以服务端时钟计)。跟随某一轮的客户端只有在该轮仍在进行且空闲超过 45 秒时才视为中断;仍在安静工作的轮次会一直跟随到其收尾事件。重新连接的客户端从已收到的最后一个 seq 开始,此后始终从每次读取返回的 head 继续,而不是从看到的最新事件继续:较晚交叠轮次的事件要等较早轮次结束后才出现,轮次序号段中跳过的序号也永远不会被补上。未记录 message_stop 就结束的轮次,会以恰好一条收尾事件结束,其序号高于该轮可能已发送的所有 seq:一般为 interrupted,已停止的轮次为 message_stop {"stop_reason":"cancelled"}。未完成且 45 秒内没有新事件的轮次,会在该会话下一轮开始时以同样方式收尾;仍在工作但暂无输出的轮次(如较长的模型调用)每 15 秒向日志报到一次,因此只有确已停止的轮次才会被收尾。不带 thread_id 的请求仍在连接关闭时结束。
聊天轮次还可以在 POST /agent/chat 请求体中携带可选的 client_turn_id:客户端在发送请求前为该轮取的名字(1–64 个字母、数字、_ 或 -;必须同时提供 thread_id,其他取值返回 400)。它记录在该轮的 user_message 事件中,形如 {"text","context","client_turn_id"},因此从未收到 message_start 的客户端(连接先断开、在准备阶段点击了停止,或关闭后重新打开页面)可以从头读取事件,找到带有自己 client_turn_id 的 user_message,再跟随或停止该事件的 run_id。interrupted 事件说明该轮结束的原因:轮次出错后为 {"reason":"error"},轮次无响应(由下一轮收尾,或 30 分钟后)为 {"reason":"stale"},其他情况为 {"reason":"ended"}。
POST /agent/assistant/threads/:id/runs/:run_id/stop 显式停止某一轮,停止记录成功后返回 200 {"item":{"run_id","stop_requested":true}}。该轮在下一个帧处或下一次模型调用之前以 message_stop {"stop_reason":"cancelled"} 结束:正在进行的模型调用会完成并计费,不再发起新的模型调用,该帧之后日志不再记录任何内容。对于模型调用的每次供应商请求(包括首次请求,以及每次改用其他模型路由重试),该轮会在 2 秒内向日志确认是否已被停止。该确认在该次请求的额度预留开始时发出,并在发出供应商请求前等待其答复,因此与额度预留同时进行,除非比预留更慢,否则不额外增加等待;在发出确认之前已记录的停止一定会被看到,该次供应商请求不会发出;其预留额度会被释放,该次请求在用量记录中的失败类别为 cancelled。已停止的轮次、未知的轮次,或轮次进行中会话被删除,都会结束该轮。日志未能及时答复时不会阻塞该轮(与其他场景一致,日志不可达从不阻塞会话所有者),且首次失败后该轮不再确认;已记录的停止仍会随日志对下一批帧的答复到达该轮,最多延迟一批。政策与流程问题(operational_kinds)同样会在其两次模型调用之前检查停止。停止已结束的轮次同样返回 200。与事件读取相同,停止仅限会话所有者:他人的会话、未知的轮次或未登录调用者读到 404 {"error":"not_found"};无法访问日志时返回 503 {"error":"unavailable"}。
会话记忆
当轮次携带 thread_id 且该会话的日志中已有之前的轮次时,助手对该会话的记忆由日志构建,messages 中最后一条之前的所有消息都会被忽略:只需发送新消息(用户的文字,或继续本轮的前端工具 tool_result)。最后一条消息仍是本轮的指令。不带 thread_id 的请求,以及日志仍为空的会话的第一轮,与以往完全相同,由所携带的 messages 构建记忆。以 tool_result 继续的轮次会在其 user_message 事件中记录为 {"text":"","tool_result":{"tool_use_id","content"}},携带 operational_kinds 发送的问题会记录 "operational":true 及其 "operational_kinds";制度/流程类回答和错误提示不计入记忆。若日志无法读取,日志中只有制度/流程类回答或已被编辑替换的轮次(且第一轮未记录之前的消息),或会话引用过私人邮箱内容而调用者不再对每个来源邮箱持有当前授权,本轮将不带之前的记忆作答,绝不改用 messages 中之前的消息。由引用过私人邮箱内容的会话构建的记忆,会在模型读取前把这些来源邮箱记录到新的运行上,因此该运行的追踪记录与读取邮箱时一样受限;若无法记录,该记忆同样不予使用。
两个可选的请求体字段(仅在携带 thread_id 时可用)让记忆与用户看到的分支保持一致。每个字段指定一个轮次:已记录回复的 run_id,或之前的轮次 "prior:<i>"(见下文)。回复在开始前即失败的轮次,以其 error 帧在 "run_id" 中携带的运行指定,因此在其后编辑时会保留该提问。编辑并重新发送时传 "supersedes_after_run_id":编辑后的会话所保留的最后一个轮次,若编辑的是第一条提问则为 null;其后的所有轮次不再计入本轮及之后各轮的记忆。重新生成时传 "regenerates_run_id":被替换回复所在的轮次;本轮基于该轮次之前的记忆作答,之后的轮次会看到新回复,且不会重复该问题(被替换轮次之后的轮次均保留)。两者分别以 "supersedes_after_run" 和 "regenerates_run" 记录在本轮的 user_message 事件中,只能选择或截断记忆中已有的内容,不能增加内容。取值既不是 1–160 个 [A-Za-z0-9_-] 字符也不是 prior:<i>,或在没有 thread_id 时传入任一字段,返回 400。指定了该会话从未记录的 prior:<i> 不会被拒绝,而按轮次顺序处理:编辑并重新发送保留该轮次及之前已记录的之前轮次,不保留任何已记录的轮次;重新生成基于该轮次之前已记录的之前轮次作答。未被日志收录的轮次(见下文)早于所有已记录的轮次,因此其之前的内容都不保留。在日志启用前保存的会话,随其第一个被记录的轮次,在新消息之前一次性发送完整记录:之前的消息随该轮记录为之前的轮次("prior_turns"),每个轮次的 ID 为 "prior:<i>"(i 从 0 起算;一个轮次是一条用户消息——提问或前端工具的 tool_result——及回答它的回复),之后各轮只发送新消息时仍保留在记忆中。只有该会话的第一个被记录的轮次会记录这些消息(包括携带 operational_kinds 发送的问题);之后任何轮次发送的之前消息既不会被记录,也不会被使用。每条消息按发送时的原样记录,记忆在构建时才截略过长的消息,与按请求构建完全一致;若这些轮次一个日志事件容纳不下,则先舍弃最早的完整轮次("prior_offset" 记录其消息条数,保留的轮次 ID 不变)。该轮的 message_start 帧携带 "prior_from":第一个被记录的之前轮次的 i(若一个都容纳不下,则为发送的之前轮次数),以便客户端只为日志中保存的轮次分配轮次 ID。20 条消息与 24,000 个字符的上限在选定分支后作用于记忆,从不作用于记录的内容。在之前的消息内编辑并重新发送或重新生成时指定其轮次;若这是该会话第一个被记录的轮次,则同时发送完整记录。本轮自身的消息在一个日志事件容纳得下时完整记录,因此由日志重建的副本显示的正是发送的内容;更长的消息保留前 8,000 个字符及其结尾("text_tail"、"content_tail"),因此记忆对其的截略与以往完全一致。若其完整文本的分段数据不超过 1 MiB,该事件还会以 {"count", "chars"} 的形式声明 "text_parts"(工具结果则在 "tool_result" 内声明 "content_parts"),完整文本随后以 count 个同一 run_id 的 "user_text_part" 事件写入,每个为 {"index", "text"},位于回复的第一帧之前。这些事件只出现在本事件读取中,从不出现在对话流中。只有当 0 到 count − 1 的每个 "index" 都恰好出现一次、拼接后的文本恰为 "chars" 个字符,且以 "text" 开头、以 "text_tail" 结尾(工具结果为 "content" 与 "content_tail")时,才重建完整文本;否则显示开头和结尾,并明确标示中间部分缺失。两种情况下记忆完全相同。若消息还附带仅供本轮模型使用的说明,请在可选的 "display_text" 中发送用户看到的请求(非空字符串,仅在携带 thread_id 且未携带 operational_kinds 时可用,否则返回 400):日志记录该文本,之后的轮次记住的是显示的请求而不是这些说明,本轮仍按消息本身作答。
在记录轮次之前保存的会话一次性发送之前的消息时,可附带可选的 "prior_replies" 数组(仅在携带 thread_id 时可用),记录客户端在这些回复上显示的内容,使由日志重建的会话副本保留它们:每项为 {"index","run_id?","memory?"},其中 index 是该回复在最后一条之前的消息中的位置,run_id 是生成它的运行(1–160 个 [A-Za-z0-9_-] 字符),memory 是其记忆卡片 {"content","status"}(去除控制字符后 1–500 字符;status 为 proposed 或 saved)。最多 500 项;prior_replies 不是数组、超过该数量,或没有 thread_id 时返回 400;未指向之前消息中某条回复、或不含任何有效内容的项会被丢弃。它们只随第一个被记录的轮次所记录的之前轮次一起保存,作为每个轮次的 "replies" [{"at","run_id?","memory?"}](at 是该回复在该轮次 messages 中的位置);它们只是供客户端使用的数据,从不计入记忆。
POST /agent/chat 的可选 "prior_operational" 随 thread_id 发送 {"after","role","kinds","question"} 展示标记。普通首轮与操作型首轮均支持;仅含政策问题且没有之前模型消息的分支使用 after: -1。最多 500 项:after 为 -1 至之前最后一条消息索引的整数;role 为 user 或 assistant;kinds 为 policy、procedure 或两者;question 去除控制字符并修剪后为 2–8000 字符。容器错误、超量或缺少线程返回 400;无效条目丢弃。仅首个已记录的用户消息保留标记,之前的模型轮次优先占用预算,最旧标记先被省略。旧日志中没有 ID 的标记按原始存储数组位置识别;丢弃无效行不会重新编号其他标记。标记文本从不进入模型历史;它们从不计入记忆,且只由该会话第一个被记录的轮次保存。服务器在裁剪前分配 prior_op_N。明确携带该字段的请求在首个 message_start 收到仅含已保留 ID 的 prior_operational_ids,可以为空数组;分段首条消息会使该帧序号大于 2。后续轮次与未携带字段的旧请求不收到确认。客户端仅使用确认过的 ID。recheck_of 可指向同一会话已存储的标记,编辑也可使用该标记为锚点。首次上传时被省略的锚点会在日志的 supersedes_after_run 中规范为前一个保留的 prior:<i> 或 null,不把标记文本复制到模型上下文。
需要查询很多内容的轮次还可能记录 compaction 事件。当一轮自开始以来新增的消息(其工具调用与结果)将超过 48,000 个字符时,助手会在下一次模型调用之前,先压缩本轮最早的工具结果,直到本轮重新回到预算内:每个被压缩的结果保留工具名称、结果状态、开头内容以及最多 40 个找到的标识(客户、项目、文档与证据编号),需要更多细节时助手可凭标识再次查询。指令、工具列表与本轮之前的对话从不压缩。每个 compaction 事件携带 {"hop","compacted","charsBefore","charsAfter"}:其之前的那次模型调用、此时被压缩的结果([{"id","name","before","after"}],id 即其 tool_step 帧携带的工具调用编号),以及本轮新增消息在这次压缩前后的字符数(按该次模型调用的发送内容计算,此前已压缩的结果按压缩后的长度计入)。结果一经压缩,本轮之后都保持压缩。最终回答仍以完整结果校验。未超出预算的轮次不会记录 compaction 事件;不认识该事件的客户端会忽略它。
制度或流程问题的重新核对(携带 operational_kinds 的轮次)可以携带可选的 "recheck_of":其所刷新的回答所在的轮次——该回答的 run_id,或之前的轮次 "prior:<i>"。它必须同时提供 thread_id 与 operational_kinds;取值不符合这两种形式,或缺少它们时传入该字段,返回 400。记录本轮之前会先读取该会话的日志:只有当它指向同一会话的某个轮次(已记录的运行、第一个被记录的轮次所记录的之前轮次,或本轮所记录的之前轮次)时,才会以 "recheck_of" 记录在本轮的 user_message 事件中;否则,或日志无法读取时,该重新核对按一个独立的问题记录。回放日志的客户端会把带链接的重新核对应用到其所指向的回答上,而不会再显示一个轮次。
助手个人记忆
每位登录用户确认的助手记忆严格私有。GET /agent/memories 返回 {"items":[…]},只含调用者本人的生效记忆,按创建先后排列;每条含 id、content(1–500 字符)、locale、origin、status、createdAt、createdVia、updatedAt、retiredAt。POST /agent/memories 以 {"content","locale?","origin?","created_via?"} 新建:origin 取 user_statement 或 assistant_proposal(默认前者),created_via 取 chat 或 panel(默认 panel)。第 51 条生效记忆返回 409 {"error":"memory_limit_reached"};内容修剪、去除控制字符后不在 1–500 字符范围返回 400 {"error":"memory_invalid"}。PATCH /agent/memories/:id 以 {"content"} 编辑并同样限界;POST /agent/memories/:id/retire 停用记忆,可重复调用。
组织与用户身份每次调用都取自已验证的会话,绝不接受请求体传入。同组织的其他成员——包括管理员与平台超管——以及其他组织的成员,列表读到 200 {"items":[]},操作他人的记忆得到不可区分的 404 {"error":"not_found"}。未认证的列表读取同样返回诚实的空列表;未认证的写入返回 401。每次变更追加一条不含内容的审计事件(用户、动作、记忆 id、时间):记录发生过什么,绝不记录内容。用户本人的生效记忆以其聊天提示词中一段有界的「关于当前用户」呈现;读取失败时跳过该段,绝不断开对话。
助手在某条回复上提出的记忆卡片和提问,在整个对话中只决定一次。POST /agent/assistant/threads/:id/decisions 记录对一条回复的决定,以 turn 指明回复(回复的 run_id;对话在开始记录日志前保存的早期轮次为 prior:<i>):{"turn","kind":"memory","outcome":"saved","content"} 保存卡片中的记忆,{"turn","kind":"memory","outcome":"dismissed"} 忽略卡片,{"turn","kind":"ask","outcome":"skipped"} 跳过提问。返回 200 {"item":{"turn","memory?":{"outcome","memory_id?"},"ask?":{"outcome"}}},即调用后该回复的决定;保存时另含 "memory"(记忆条目)。每张卡片最多保存一条记忆:同一卡片再次保存——来自另一台设备、同时点击或旧页面——返回首次保存创建的记忆(即使之后已停用),不会再新建。已保存优先于忽略:忽略一张已保存的卡片返回 saved;其余情况以第一次决定为准。保存同样遵守 50 条上限与内容限界(409 {"error":"memory_limit_reached"}、400 {"error":"memory_invalid"})。GET /agent/assistant/threads/:id/decisions 返回 200 {"items":[…]},每条已决定的回复一项。与事件读取一样仅限对话所有者:他人的对话、未知对话或未登录调用者读到 200 {"items":[]},写入返回 404 {"error":"not_found"};对从未记录日志的对话写入——包括从其他组织访问的任意对话 id(只会访问到该组织自己的空记录)——返回 404 {"error":"not_logged"},此时卡片改用 POST /agent/memories 保存;日志无法访问时返回 503 {"error":"unavailable"}。删除对话会一并删除其决定。
知识检索:语义兜底与冲突数字
GET /knowledge/library/documents?q=… 按字面匹配文档名与已存文本。当关键词匹配没有结果时,该路由会对同一组织范围、同一权限过滤的文档集改用受治理的语义索引再检索一次,并返回 200 {"items":[…],"match":"semantic"}。match 仅在语义作答时出现——关键词作答完全不带 match——因此调用方绝不会被告知“找到了这些字词”而实际并没有。兜底检索同样遵守请求自身的 folder_id、ext 与 updated_within 收窄条件,按段落向量的余弦相似度排序,低于相似度下限的一律不提供;每个返回文档都携带其命中段落作为不带高亮的 excerpt。没有足够接近的文档、没有配置嵌入绑定、或嵌入调用失败,都保持诚实的 200 {"items":[]}——兜底绝不把可作答的搜索变成错误。不引入新的索引技术,也不使用 CRM 数据:语料就是资料库自身的就绪文档。
POST /knowledge/sandbox/ask 新增可加性字段 conflict。当答案所依据的段落对所问事实给出不同数字时——同一个报价有效期分别写作 30 天、45 天、六十天——响应携带 {"unit":"day","sources":[{"file_id","file_name","passage_index","figure","value","date"}]},每一份说法不一致的文档都会被引用并列入 sources,答案并列呈现这些数字,而不是只给出其中一个——相互矛盾的文档并不构成互相印证。figure 是该文档原样书写的数量(thirty days、45 days、六十天),date 是该文档自身的更新日期,没有则为 null。检测在既有的答案编排步骤内以数值与词法方式完成——不增加模型调用——且只有当数字在同一句中的前导词与问题相符时才计入,因此同一文档里无关的数字不会触发冲突。来源一致的答案完全不含 conflict,行为保持不变。登录应用会在引用的答案上方以中英文展示该分歧。
资料库段落检索:混合模式与位置信息
GET /knowledge/library/search?q=…&limit=… 新增可选参数 mode:token(默认值,不传 mode 即为此值)或 hybrid。mode=hybrid 采用与知识问答相同的融合召回排序段落:关键词排序的前 8 条与语义排序中相似度不低于 0.40 的前 8 条,按倒数排名融合(k = 60)合并为至多 16 条,再按 limit(1–20,默认 8)截取。每条结果保留 file_id、filename、ext、passage_index 与 snippet;score 改为融合分数(较小的小数,越大越相关);新增附加字段 locator 说明段落所在位置:{"page"?, "sheet"?, "range"?, "part"?, "line"?, "kind"}——PDF 页码、工作表与单元格范围(例如 Terms / A2:C2)、Office 文档部分或文本行号,以段落实际存储的为准,另加段落类型 kind(未存储时为 text)。
混合模式下,关键词一侧还会把型号作为整体匹配:MC-3200-T 这类查询词(由字母、数字及 -、_、/ 组成,至少含一个字母和一个数字,长度不少于 4)能找到 MC3200T、mc_3200_t 以及全角写法,同时保留原有的分词匹配。嵌入调用失败、超过 2 秒未返回或未配置嵌入绑定时,混合模式仍以 200 返回纯关键词排序——不会像 semantic=true 那样返回 503 embeddings_unavailable。混合模式收录的段落与其他模式完全一致:调用方所在组织中、调用方有权查看的就绪文档。
未使用新参数的调用方不受影响:不传 mode、mode=token 以及 semantic=true|false 返回的条目、顺序与字段与此前完全相同,且不含 locator。同时传 mode 与 semantic,或 mode 取 token、hybrid 以外的值,返回 400 invalid_request。助手的资料库检索与 ekb.library.search 动作(MCP ekb_library_search)均使用 mode=hybrid。
资料库段落读取
检索返回的片段只是段落开头,需要的条款常常在后面。GET /knowledge/library/files/:id/passages 读取检索命中内容的完整文字。使用 /knowledge/library/search 结果中的 file_id 与 passage_index,并且只发送下面两种查询形式之一:
?around=<n>&radius=1(radius可省略,唯一取值为1):按文档顺序返回第n段的完整内容,以及前后各至多一段,总计不超过 6,000 字符。?all=1:文件提取文字不超过 12,000 字符时,按顺序返回全部段落。更长的文件返回200,其中status: "too_large"、段落数passage_count和空的passages列表——不含任何文字。此时请改用around读取具体段落。
响应:{file_id, filename, ext, status: "ok"|"too_large", passage_count, passages: [{passage_index, text, locator}]}。每个 locator 与混合检索为该段落返回的一致:{"page"?, "sheet"?, "range"?, "part"?, "line"?, "kind"}。段落编号与检索使用的编号相同。
读取与检索的可见范围相同:调用方所在组织中、调用方有权查看的就绪文档。其他组织的文件、其他成员的个人文件、尚未就绪的文件以及超出范围的段落编号,一律返回相同的 404 not_found 响应体,不含任何文字。缺少或同时发送两种查询形式、未知参数、around 不是整数或 radius 不是 1,返回 400 invalid_request。该路由只接受 GET。动作 ekb.library.passages.read(POST /assistant/v1/actions/ekb.library.passages.read,请求体 {"file_id", "passage_index"?};不传 passage_index 即读取整份文件)、MCP 工具 ekb_library_passages_read 与 CLI 命令 atlas library read <file> [--passage <n>] 在 ekb:read 下返回相同内容;段落不存在时动作返回 404 not_found。
助手中的 read_library_passage 与 read_library_document 执行相同的读取。助手在一次回复中最多检索资料库两次:第三次返回 search_budget_reached,回复随即说明文档中没有写明答案。
沙盒回答:混合召回与生成式作答
POST /knowledge/sandbox/ask 的请求完全不变,改变的是它如何作答。沙盒此前只做打分、从不生成:答案就是关键词匹配最好的那一段原文,逐字引用,旁边附一个 confidence 百分比。这一设计现已明确反转。关键词打分把「Summarise our pricing policy」答成了一份质保文档,置信度还写着 66;而「价格是多少」则完全答不出来,尽管语料里明明写着「报价有效期三十天」——「价格」的中文二元组与「报价」毫无交集,任何确定性分词都无法弥补。
confidence 已从响应中移除,且不以任何数字替代。它只是关键词命中数的算术结果,从来不是概率;把它摆在一个错误答案旁边,正是本次改版要消除的失败。每条引用仍保留自己的 pct,它描述的是该段落与问题的词汇重合度,不对答案本身作任何断言。新增的 answer_kind 改为说明出处:"synthesized" 表示答案由模型依据被引用段落写成,"quoted" 表示这是原文逐字引用,"none" 表示没有答案。沙盒评价所保存的快照——问题、答案、范围、来源——形状保持不变。
召回是混合式的:共享的关键词排序与资料库已存的多语段落向量通过倒数排名融合(RRF,k = 60,最多八段)合并,因此任一信号找到的段落都可达,两个信号都找到的段落排在前面。向量只有在其文档属于调用者本组织范围解析所返回的结果时才被采纳;请求体中的 id 绝不会放行任何向量,被判为未找到、未就绪或无可用段落而排除的文档,在语义上同样不可达。未配置嵌入绑定、嵌入调用失败、文档没有已存向量,都只是仅凭关键词作答的正常提问,绝不是错误。
生成是一次受治理的 Workers AI 调用:temperature 为 0,采用 JSON schema 响应格式,经自动开通的网关发出,携带每次调用的组织、运行与技能元数据,关闭缓存与网关日志。模型只收到问题和带编号位置的段落文本,此外一无所知——没有文件 id、没有文件名、没有组织——并只返回它用到的位置编号,因此每一条引用、名称与摘录都在服务端依据本次提问自己的召回结果解析得出。若被引用段落本身以阿拉伯数字写出数值,而答案给出的数字在其中找不到,该答案会被拒绝。
拒答不是降级的答案。当模型判定这些段落并不包含所问内容时,响应为 {"phase":"notFound","answer_kind":"none","answer":""},并附带 closest:召回到的最接近的至多三段,每条为 {"file_id","file_name","passage_index","snippet"},摘录上限 200 字符,且不含任何分数。此时绝不会改用关键词原文引用顶替——那正是质保文档答案的成因。closest 只随 notFound 出现,可以为 [],在 done 与 scopeEmpty 上一律不出现。
降级的每一级都是 200,绝不是 500:模型作答 → done、"synthesized";模型不可达、抛错或返回无法解码的信封 → done、"quoted",即确定性原文;若关键词同样毫无匹配,则为 notFound 并附 closest;模型拒答 → notFound、"none",并附 closest;什么都没召回到 → notFound、"none"、closest: []。用量归属投递被拒只降级计量,绝不降级答案。该路径的日志只记录 id 与错误类别:问题与段落文本从不写入日志。
两项行为保持不变。携带非空 operational_kinds 的提问仍为确定性作答,answer_kind 为 "quoted",因为受治理的制度或工作流版本是应当原样引用的授权表述,不应被转述;该提问也仍然仅限登录会话,不对委托 token 开放。答案依然是供人核对的回答,而不是合规判断。
混合范围中无法读取的工作簿。范围同时含工作簿与文档,且表格引擎无法读取某个工作簿(工作表布局不受支持,或其索引无法加载)时,该工作簿列入 excluded,reason: "spreadsheet_unreadable",并带 detail 字符串,内容为引擎自身的拒绝原因。只有在确认问题不是计算时,才改由文档作答,而不是停在拒绝上:问题被判定为针对文档;或引擎的计划是查找、筛选或「不是表格问题」,而非求和、计数、平均、最小、最大或比较;或引擎没有得出具体计划(例如列名有歧义)且问题不含计算用语。没有单一目标工作表的拒绝会列出它所涉及的每个工作簿。否则保留表格拒绝——计算类问题如此,文件数与 16,000 单元格预算以及要求缩小选择的拒绝如此,所有工作簿都无法加载时的计算类问题也如此;仅含工作簿的范围不变。
文档类问题跳过表格判断。在混合范围中,问题若不含计算用语(如 total、sum、average、count、how many、maximum、minimum、compare、more than、合计、总和、平均、多少个、最高、最低、比较),也没有点名范围内的工作簿(按文件名,或「表格」「spreadsheet」等字眼),就直接由工作簿之外的文档作答,不再调用判断「是否表格问题」的模型。仍需该判断时,文档召回与之同时进行,而不是在其之后。只有调用者可见的工作簿才参与名称匹配。因此 excluded[].reason 取值为 not_found、not_ready、no_passages 或 spreadsheet_unreadable;仅最后一种带 detail。
料号所在行可作为依据。不含计算用语的问题会把工作簿的行保留在文档召回中,因此某行写明的数字(例如价格表中某料号的起订量)即使在表格引擎无法读取该工作簿时也能作答并引用;计算类问题从不把行当作文本阅读。答案引用了其行的工作簿不会列入 excluded。引用表格行时会连同表头一起显示:工作簿行在每个值旁标出其列名,引用带 sheet 与 range;PDF 表格行前加 Table header: …。此类摘录上限为 400 个字符,而非 200。
POST /knowledge/sandbox/ask 的每个响应——生成式、表格或制度作答、未命中或拒绝——都带 Server-Timing 响应头,含六个以整毫秒计的阶段:scope、recall、model、post、total 与 route。route 为混合范围的路由判断(工作簿查找,加上文档捷径或表格判断);与之并行的召回只把路由结束后仍需等待的时间计入 recall。
组织技能页面
GET /governance/skills 是技能页面自己的目录:调用者所属组织下每个在用的目录技能,包含其中英文名称、由其 applicability.surfaces 得出的目录角色 id 与布尔值 appliesToAssistant、与既有使用摘要相同口径的近 30 天 attempts/succeeded/failed/refused/conversationsCompleted/conversationsFailed/liked/disliked 使用数据,以及本组织自己的 enabled 状态与设置 revision。仅限所有者或管理员;其余任何已登录调用者读到的都是如实的 200 {"items":[]},而非拒绝。POST /governance/skills/:id/setting 接收 {"enabled","revision"},连同操作管理员与时间一起写入本组织的决定,并采用与其他平台头记录相同的乐观 revision 保护:过期写入不改变任何数据,返回 409 stale_revision;既非目录技能、也非调用方组织自建技能的 id 返回 404 unknown_skill;两类技能使用同一开关与同一保护。停用一个技能不会授予或收回任何权限——雇员可执行的操作类别与权限级别两种情况下均不变——它只是不再让该技能出现在该组织此后的任何新解析中;已在使用某个已固定版本的运行不受影响,会正常完成。GET /governance/internal/skills/resolve 新增两个可选查询参数,均向后兼容且为附加式:不传 org 时,解析行为与之前完全一致。只有调用方既指定 version、又同时传入 pinned=1,才等同于由服务器自己确认:该版本来自它自身持有的状态(例如已保存的提取结果,或本次请求已经解析过的技能回执),而不是来自浏览器提供的请求头或查询参数——这一区分很重要,因为技能的版本号与摘要是对已发布目录定义的确定性取值、并非秘密,客户端一旦见过,便可无限次重放,从而在组织停用后仍能持续解析该技能。只有不带版本号的调用,或带版本号但未设置 pinned=1 的调用,才会被拒绝为 404 skill_disabled_for_org;真正经过服务器确认的固定延续调用,绝不会以此为由被拒绝。此处的每一次读取与写入都仅限定在调用者自己所属的组织内,因此一个组织对某技能的设置永远不能被另一个组织的会话读取、列出或更改。
GET /governance/internal/skills/index 是助手每轮对话读取技能库所用的内部接口,仅接受带签名的内部调用。org 与 surface 两个查询参数均为必填(助手传 surface=assistant);缺少任一参数返回 400 invalid_request,缺少内部请求头或签名返回 403 forbidden。它返回 200 {"items":[...]}:每个在用、适用范围包含该界面、且本组织未关闭的目录技能各占一项,按技能 id 排序,每项包含 skillId、version、digest、中英文 name 与 description,以及用于发现的 terms 与 tools。助手在系统提示中按 id、名称和一句话说明列出这些技能,其 load_skill 工具返回所列某个技能的完整文本:每次加载都会针对调用者所属组织重新解析该技能,因此对话中途被关闭的技能,或从之前对话中重放的 id,都会以 skill_disabled_for_org 被拒绝,不返回文本、不固定版本、不写使用记录。加载成功会在本次回复中固定 id@version,写入一条技能运行记录,并在回复下方的“使用的技能”一行列出该技能;每次回复最多加载三次。该索引不授予任何权限:它只用于发现,加载技能不改变任何权限。
管理员教给雇员并发布的方法,也会记录到本组织的技能库中,作为一个摘要与文本完全相同的组织技能版本。GET /governance/skills 以 source: "taught" 列出它,并附带 taught 字段,说明 hireId、scope(account 表示针对某个客户,organisation 表示整个组织)以及 taughtVersionId,即雇员自己的版本 id;其 id 为 taught_organisation 或 taught_account_<hireId>,版本号按发布顺序为 1.0.<n>。雇员使用该版本期间,其 status 为 active;方法一经撤回、回滚或失效,立即变为 retired,撤回或被取代的行仍留在页面上,显示为已下架。雇员每次应用该方法的运行都会针对它写入一条技能运行记录,inputKind 为 taught_method,因此其近 30 天使用情况与其他技能一样计数。该行为只读:POST /governance/skills/:id/versions/:version/activate 与 retire 返回 409 id_reserved,POST /governance/skills/:id/setting 返回 404 unknown_skill,生成的 id 以 taught_ 开头的新技能会以 409 id_reserved 被拒绝,而在记录教学方法之前管理员已用此类 id 自建的技能仍是普通自建技能,保留自己的开关与操作;教学方法不计入 50 个技能的上限。雇员仍按原样从 AI 团队读取自己的方法:技能库中的这一行只是记录与使用计数,绝不是第二份指令来源,因此 GET /governance/internal/skills/index 在任何界面都不会列出它,GET /governance/internal/skills/resolve 只在指定确切 version 且带 pinned=1 时才返回它,并标注 source: "taught"。智能体通过仅接受签名内部调用的 PUT /governance/internal/org-skills/taught 写入这条记录,与技能运行记录一样绑定到所属组织;操作者绑定不一致时返回 400 invalid_request,该版本已属于另一个教学版本、或该 id 是管理员自建技能时,分别返回 409 version_conflict 或 409 id_conflict。每次写入都带有 sourceRevision,雇员的方法每变化一次它就增大;收到的最高值总会被记录,比它更旧的写入不改变任何状态,因此延迟或重复的写入绝不会让已撤回的方法复活,也不会撤销更新的发布。技能库按原样保存方法的确切文本,无论其中空白如何。当组织的保留期限清除了 AI 团队中某个方法的文本时,同一次保留清理也会清除技能库副本的文本;该行保留摘要并显示为已下架,之后的任何写入都不会再启用它。发布、撤回或清除后丢失的记录,会在管理员打开技能页面时补齐:雇员当前使用的每个方法每次都会核对,其余仍处于启用状态或仍保留文本的已记录方法按轮次复核,每次有限条数,因此每一条都会在有限次访问内被覆盖。
选择发件账号与个人默认账号(#2834)
GET /agent/prepared/:preparedWorkId/sender-projection 返回待批准邮件草稿可用的发件选择。组织没有配置任何邮件账号时,响应为 {"state":"none","from_address":"…"},审阅界面会明确标示既有的 Atlas 邮箱备用路径。否则响应为 {"state":"accounts","accounts":[…],"default_connection_id":…};每一项都是当前登录用户的邮箱设置中可见的个人或共享账号,state 为 ready(当前有发送权限)或 inactive;inactive 项可带 reason——needs_org_action、disconnected、verification_pending 或 paused——且在用户无发送权限时不返回其邮箱地址。只有 ready 账号可被选择或绑定。共享账号要求当前登录用户实时持有具名 can_send 授权;撤销后,该选择会立即消失,个人默认值也随即失效。响应绝不包含凭据、主机、用户名或 token。默认账号缺失、失效或用户已无权使用时,default_connection_id 为 null,客户端必须要求明确选择,不能擅自采用列表第一项。
POST /agent/prepared/:preparedWorkId/sender-default 携带 {"connection_id":"…"},把该可用账号保存为当前登录用户在本组织中的个人默认发件账号;跨组织、不可用或无权使用的 id 返回 409 sender_not_available,且不写入。POST /agent/prepared/:preparedWorkId/approve 可携带 {"sender_connection_id":"…"}。审阅界面显示账号 id 与配置版本、From、Reply-To、To、Cc、Bcc、主题、正文,以及每个附件不可变的 id、version 和 content_hash。批准摘要会绑定这些确切字段及账号凭据版本;其中任何一项变更都必须在调用邮件服务商之前重新审阅。未明确选择时,只能采用当前仍有效的个人默认账号。暂停、检查失败或仍在草稿状态的账号绝不触发 Atlas 备用发件路径;更改个人默认账号也绝不会改变已经记录的批准。
已批准发送的尝试记录与人工重发(#2835)
每一次已批准的邮箱发送都作为一条持久命令在 CRM 内执行:在任何连接被触碰前先预留,最多发起一次 SMTP 尝试,并且只按封闭结果词表定稿一次——refused、failed、provider_accepted 或 outcome_unknown。邮件服务器接收只是接收,永远不等于客户送达。若消息可能已被接收后发生超时或连接中断,命令记录为 outcome_unknown,系统绝不自动重发。同一批准的并发重复请求——即使客户端键不同——都归并到同一条命令、最多一次尝试;重复提交返回该命令的结果,同一键指向不同内容则在触碰任何服务商 I/O 之前返回 409 send_key_conflict。
Today 的审阅界面通过 GET /agent/prepared/:preparedWorkId/send-state 读取尝试状态(草稿从未尝试发送时为 404),并连同封闭原因一起展示结果。结果不明、失败或被拒绝的尝试会提供显式重发:POST /agent/prepared/:preparedWorkId/retry-send,请求体 {"confirm_duplicate_risk":true,"prior_command_id":"cmd_…"}——界面在调用前明确提示可能重复发送的风险,重发会记录一次绑定当前发件账号状态的全新人工批准,CRM 把新命令链接到先前尝试并完整保留先前结果。仍在发送中的命令和已被接收的命令永远不可重发——重发会在重新批准之前返回 409 send_in_flight 或 409 send_already_accepted。
持续的定时同步权限服务故障
当 CRM 无法核验已登记的定时同步授权人时,前 15 分钟连续故障只会重试,不会显示为邮箱权限问题。达到 15 分钟后,account.sync.cause 和已脱敏的账号诊断同步健康会显示封闭原因 authority_service_unavailable。这表示 Atlas 暂时无法核验其权限服务;邮箱权限没有被更改。下一次有效的允许或拒绝决定会先清除记录的首次不可用时间。允许会恢复既有同步路径;拒绝保持既有 authority_lost 行为。响应不会包含 Auth 正文、凭据、邮箱内容、服务商文本、游标或租约数据。
明确确认客户沟通偏好
POST /crm/customers/:id/facts 新增类型化请求体 {"kind":"contact_preference","channel":"email"|"phone"},并要求提供 1–128 个字符的 x-atlas-idempotency-key 请求头。首次接受返回 201 {item,replayed:false},保存一条人工确认事实:category=preference、fact_key=preferred_channel、fact_value 为所选渠道,并记录调用者与确认时间。该操作不会创建任务、时间线记录、最近联系变更或对外消息。原有 {"body":"…"} 人工备注请求继续保持既有 other/note 行为及可选幂等键;系统绝不把旧的自由文本解析成偏好。
完全相同的类型化重试返回原事实和 200 {item,replayed:true}。同一键改用另一渠道,或在类型化偏好与人工备注两种模式之间复用,都会返回 409 idempotency_conflict 且不新增事实;即使人工备注正文恰好等于类型化请求指纹也一样。未知类型值、缺少类型化操作键或键格式无效均返回 400。用不同键确认的相反偏好会作为两条独立已确认事实保留,直到用户通过 POST /crm/facts/:id/reject 明确拒绝不需要的事实;任何请求都不会静默覆盖另一项选择。读取继续使用 GET /crm/customers/:id/facts。所有操作都取自登录会话所属组织,并沿用现有客户所有者可见性守卫,因此跨组织或不在当前销售客户簿中的客户,与未知客户一样返回 404 not_found。
同一端点还接受新增的类型化请求体 {"kind":"intent","body":"…"}——即手工创建客户表单必填的意向/市场。它保存一条已确认的人工特征:category=intent、fact_key=market_or_interest、fact_value 为去空格后的文本,并记录调用者与确认时间;这条已确认的意向特征正是满足客户 fact.market_or_interest 持续经营要求的依据。与偏好模式一样,它要求 1–128 个字符的 x-atlas-idempotency-key 请求头;完全相同的重试返回 200 {item,replayed:true};同一键改变正文,或在意向、偏好与人工备注三种模式之间复用,都会返回 409 idempotency_conflict 且不新增事实。正文为空或全空白、缺少或无效的类型化操作键,或与类型化请求同时传入 category,均返回 400。除此之外没有任何副作用:不触发 AI 提取、时间线记录、任务、最近联系变更或对外消息,并沿用相同的组织与可见性守卫。
具名人类团队
Auth 管理组织中的具名人类团队,每名成员最多拥有一个当前团队分配。这些仅支持会话的 REST 操作要求当前有效的租户成员关系、所有者或管理员有效角色,以及读取所需的 members:read 或修改所需的 members:write。自定义管理员角色仍受实时权限限制。平台身份或已签名的旧角色声明本身不授予权限。组织与审计操作者由已验证会话确定;请求体拒绝组织或操作者字段。写入要求同源 Origin 请求头。现有委托令牌不会新增权限范围;MCP/CLI 适配器和设置界面由后续故事提供。
| REST | 请求 | 响应 |
|---|---|---|
GET /auth/teams?cursor&limit&status | — | {items,next_cursor} |
GET /auth/teams/:id?cursor&limit | — | {team,members,next_cursor} |
POST /auth/teams | {name} | 201 {team} |
PATCH /auth/teams/:id | {name,expected_revision} | {team} |
POST /auth/teams/:id/archive | {expected_revision} | {team} |
GET /auth/teams/assignments/:email | — | {assignment} |
PUT /auth/teams/assignments/:email | {team_id,expected_assignment_revision,expected_team_revision} | {assignment} |
团队结构为 {id,name,status,revision,member_count},状态为 active 或 archived。名称去除首尾空格后不能为空,最多 80 个字符,在组织内不区分大小写且唯一,包括已归档团队。两个列表默认 50 行,limit 最大 100,分别按团队 ID 和成员邮箱稳定升序排列。将不透明的 next_cursor 原样作为 cursor 传回;游标绑定组织、集合和状态过滤条件。next_cursor:null 表示结束。列表 status 接受默认值 active、archived 或 all。
分配结构为 {email,team_id,revision}。尚未分配的有效成员返回 team_id:null 和修订号 0。成员行包括 email、display_name、有效 role 和 assignment_revision。路径中的邮箱须 URL 编码。指定非空团队时必须提交 expected_team_revision;移除分配使用 team_id:null 且必须省略该字段。每次成功修改返回递增修订号;移除后保留修订号墓碑。成员角色保持不变。人数与归档检查计入已停用但仍有分配的成员;获授权的所有者或管理员可以移除这些分配。
每次修改在同一原子事务内检查实时权限、目标成员关系、分配修订号和有效目标团队修订号,并记录组织隔离的审计事件、可信操作者及修改前后状态。过期修订号、已归档目标、向已停用成员分配团队、归档非空团队均返回 409,且不会留下部分数据或审计修改。未知与其他组织的成员或团队详情统一返回 404。未认证返回 401;无权限团队集合返回 200 {items:[],next_cursor:null},详情读取返回 404,写入返回 403。无效输入返回 400;权限源或存储不可用返回 503,不能伪装为空集合。
当前用户工作区发现
GET /workspace/capabilities 使用已登录的 cookie,由网关通过 Auth 绑定确定当前 userId 和 orgId。响应的 areas 包含 Home、Knowledge、Routine、Plugins 和 Skills,每项带有 id、defaultRoute 与 availability。Routine 在自身验收前为 hidden;modules.sales 为 unknown,此接口不声明销售模块资格。
没有会话时返回 401;Auth 故障时返回其错误。响应使用 no-store。查询参数及调用方提供的身份请求头不能选择组织。发现信息仅用于显示,不是访问许可;客户端继续执行既有角色、权限和功能开关检查,最终访问由各领域服务决定。
下一步建议的联系渠道指引
GET /agent/assistant/brief 可在每条重点客户记录中附加 next_step.guidance。封闭的 state 只能是 ready、needs_review 或 unknown;reason 只能是 no_preference、confirmed_preference、conflicting_preference、untyped_preference、missing_channel、coverage_unknown 或 specialist_activity;factIds 最多包含 20 条已确认偏好事实的引用。旧版已保存简报可以不含此字段。
新的一般电话建议只采用值严格为 preferred_channel 的已确认事实:邮件会选择第一位有邮件地址的联系人,并改用既有邮件活动;电话会选择第一位有电话号码的联系人,并保留电话活动。目的、到期日期、排序及任务身份均不改变。记录冲突、旧版记录、不支持的值、联系人渠道缺失或读取失败时,系统绝不自行解释或静默替换;Today 会显示待核对状态,沿用既有客户资料及编辑入口,并在每次编辑后要求重新勾选可见的核对确认,才启用原有接受操作。专项活动保持不变,同时显示偏好例外说明。同一工作日已经接受或忽略的决定保持不变;指引变更只影响新建议。系统不会自动发送,只有原有的明确接受操作才会创建任务,且接受请求不会增加任何新的授权字段。
已存储的助理权限
GET /agent/assistant/permissions 返回当前登录成员所在组织全部九类动作的 {items:[{key,tier,fixed,default_tier,updated_by,updated_at}]}。未存储的动作使用目录默认值,updated_by 与 updated_at 为 null;读取不会创建记录。所有已登录成员均可读取,包括尚未包含新权限映射的旧会话。
PUT /agent/assistant/permissions 接受任意动作子集,例如 {"tiers":{"work.prepare":"never","work.background":"with_permission"}},并返回完整规则表。仅组织所有者或管理员可以保存,其他成员收到 403 forbidden。组织及修改人始终取自已验证会话,不接受客户端字段指定。权限等级为 by_itself(可自行处理)、with_permission(需经允许)和 never(从不)。未知动作、未知等级或格式错误返回 400 invalid_request;请求包含任意固定动作时,即使等级未改变,也返回 422 permission_fixed。拒绝的请求不保存任何修改,通过校验的子集以原子方式保存。存储不可用返回 503 permissions_unavailable,不会伪造默认结果;数据库未绑定返回 503 db_unbound。
| 动作键 | 默认等级 | 固定 |
|---|---|---|
customer.status_by_rule | by_itself | 否 |
customer.log_inbound | by_itself | 否 |
opportunity.update_from_inquiry | with_permission | 否 |
customer.confirm_suggested_fact | with_permission | 是 |
work.prepare | by_itself | 否 |
task.create_followup | by_itself | 否 |
work.background | by_itself | 否 |
external.send | with_permission | 是 |
commercial.price_or_promise | never | 是 |
本版本仅存储选择,不改变助理执行行为、已有 AI 开关或单个助理的权限等级,也不新增设置页面。
Today 工作台分析:KPI、管道与营收趋势
GET /crm/analytics/kpis、GET /crm/analytics/pipeline 和 GET /crm/analytics/revenue-trend 为 /today 销售工作台提供数据,三者均仅按调用者所在组织限定范围,不接受任何查询参数,也不含客户或销售身份。所有金额字段汇总 customer_engagements.amount,保留每笔交易自身的 ISO 币种(customer_engagements.currency,默认 CNY),绝不换算或混入其他币种:每项汇总先按币种分组,再提供 by_currency 数组(每种实际出现的币种各一条 {currency,value}),并保留同名旧版标量字段——该标量仅在这项汇总的范围内只有单一币种(或完全没有交易)时才为真实数值,一旦跨越多个币种立即降为 0。
写入这两列的两个接口在入口处就做校验:POST /crm/customers/:id/engagements 与 PATCH /crm/engagements/:id 接受 amount 为有限数字,或数字字符串(如 "15000000"),取值范围 [0, 1e15]——其余情况(负数、NaN、Infinity、超出上限或非数字)一律返回 400 {"error":"amount_invalid"},且不写入任何数据。currency 必须是 3-4 个 ASCII 字母,存储时转为大写("cny" 会变成 "CNY");缺省、null 或空字符串保留原有默认行为,其余任何形式都返回 400 {"error":"currency_invalid"}。
GET /crm/analytics/kpis 返回 {monthly_revenue,pipeline,open_opportunities,win_rate_ytd}。monthly_revenue 为 {current_cents,previous_cents,mom_percent,current_by_currency,previous_by_currency}:current_cents/previous_cents 汇总当前/上一 UTC 自然月内更新的已赢单交易;mom_percent 仅在两个月都能归并为同一单一币种时才计算,否则为 0(跨币种或混合币种的变化不构成百分比)。pipeline 为 {open_total_cents,weighted_forecast_cents,open_total_by_currency,weighted_forecast_by_currency}:open_total_cents 汇总全部未结(非赢单非输单)交易;weighted_forecast_cents 汇总每笔未结交易金额 × 自身概率(缺失时用阶段概率,两者皆无为零)÷ 100。open_opportunities 与 win_rate_ytd 为计数与四舍五入百分比,不受币种影响。
GET /crm/analytics/pipeline 返回 {items},按组织管道目录中每个启用的未结类型阶段各一行(目录读取失败时降级为四阶段兜底列表),每行为 {stage,count,amount_cents,amount_by_currency,share_percent}。count 为该阶段跨币种的真实交易数。share_percent——该阶段占整个未结管道的百分比——只要未结管道中任意位置跨越一个以上币种,所有阶段的该值即一并降为 0,绝不用元和美元相加计算百分比;某一阶段本身恰好只有单一币种,也不能因此单独获得有效百分比,只要管道中其他阶段的交易属于不同币种。
GET /crm/analytics/revenue-trend 返回 {items},为过去 12 个自然月的分桶(从最早到最近),每桶为 {month_start,total_cents,total_by_currency}:按已赢单交易的 updated_at 所在 UTC 自然月分桶,每个月份独立判断是否单一币种。
口径一致的销售报告(桌面销售结果)
三个只读接口支撑桌面端“洞察 › 复盘 › 销售结果”视图(STORY-V2-ERP-07):用一个报告数字打开其背后精确的授权记录来解释它。所有合计都由服务端对完整结果聚合得出,绝不来自客户端当前页,且每个响应都会回显实际使用的范围。范围由已验证的调用者按“商机列表”的同一规则解析:销售代表始终只统计自己名下已分配客户(客户归属人且为已分配状态;公海或他人客户不可见),所有者/管理员/经理默认统计整个组织,owner=me 收窄到自己的客户。归档客户的交易不计入任何数字。不接受客户端提供的邮箱、组织或操作者。
GET /crm/reports/sales-summary?period_start=&period_end=&owner=me 返回 {report, captured_at, scope, period, pipeline, closed}。pipeline 按已配置的当前阶段统计授权的未归档交易:items 按目录顺序为每个启用中的组织管道阶段给出一行(stage、stage_name、count、amount_by_currency、missing_amount_count),另有 unknown_stage_count 统计处于已停用或未知阶段键的交易,以及 total_count。金额原样汇总 customer_engagements.amount——交易自身币种的整数单位——按实际出现的币种各给一行 {currency,value},绝不换算或混合;未填金额的交易计入 missing_amount_count,绝不按零值处理。closed 按每笔交易“采集时刻之前最后一次持久化的终态变动(成交/流失类目录阶段)”唯一计数,该变动时刻落在半开区间 [period_start, period_end) 内:won 与 lost 各带 count、amount_by_currency 和 missing_amount_count。采集时最新变动为非终态的交易已被重新打开:不计入,并在 reopened_excluded 中披露。当前阶段为终态但没有终态变动记录的交易没有可靠时间戳:不计入任何期间,并在 legacy_excluded 中披露——绝不以 updated_at 顶替成交时间。缺少或无效的期间返回 400 invalid_period。
GET /crm/reports/sales-drilldown?metric=pipeline_stage&stage=…(或带 period_start/period_end 的 metric=closed_won/metric=closed_lost;owner=me 与汇总同样收窄;limit 1–200 默认 50,offset 分页)在与汇总相同的筛选和访问范围下返回 {total, items, limit, offset, captured_at, scope, period},因此分页 ID 与被统计的总数一致。pipeline_stage 必须提供 stage——启用的目录键或字面量 __unknown__(对应已披露的停用/未知分组)。每条记录包含 id, title, customer_id, customer_name, stage, amount, currency, stage_entered_at, updated_at,且仅成交类指标包含 terminal_transition_at——使该交易落入此组的确切变动时刻。下钻始终按自己的 captured_at 重新计算:与更早汇总对照的客户端必须比较时间戳与总数,并明确标记报告已变化,而不是沿用旧数字。非法输入返回 400 invalid_metric、invalid_stage、invalid_period、invalid_limit 或 invalid_offset。
GET /tasks/report-overdue?timezone=Asia%2FSingapore&limit=&offset= 按“首页/任务”到期日汇总的同一规则统计逾期未完成跟进——截止时间早于所请求 IANA 时区中调用者当天本地零点的待办任务——但范围是调用者有权查看的记录而不仅是自己的任务:销售代表统计自己的任务,所有者/管理员/经理另外统计其有权阅读的团队任务;他人的私人任务永不计入。返回 {report, timezone, captured_at, day_start, total, items, limit, offset, merge_pending?},每条记录包含 id, title, customer_id, due_at, assignee_email, visibility;总数与列表来自同一谓词,分页 ID 与 total 一致。正在并入其他客户的任务会被排除,并像列表接口一样通过 merge_pending 报告。未知或格式错误的时区返回 400 timezone_invalid;越界的 limit/offset 返回 400。这些是沿用现有分析接口面的只读能力:不新增 CLI 命令或 MCP 工具(STORY-V2-ERP-07)。
每个业务数字只有一套定义
GET /crm/owner/metrics?period=quarter 接受 week|month|quarter|ytd|annual。时间段以 UTC 秒表示,结束时刻不计入,elapsed_pct 为四舍五入后的进度百分比;周从星期一开始,ytd 截至读取时刻。缺少或无效的时间段在计算前返回 400 invalid_period。所有者和管理员读取本组织;经理沿用团队复盘的销售名单(目前是本组织所有非空客户负责人);销售代表在计算前收到 403 forbidden。组织和名单范围来自已验证的调用者,不能通过查询参数扩大。
响应包含 period、figures、at_risk 和 definitions。每个数字为 {value,target?,definition,by_currency}。revenue_won 和 deals_won 分别汇总与统计在所选时间段内更新的已赢单交易;new_customers 统计期间创建的客户;win_rate 是期间更新的赢单数除以赢单与输单总数的百分比并四舍五入,没有已结束交易时为 null。open_pipeline 汇总当前所有未结交易。金额类数字(revenue_won、open_pipeline、weighted_forecast、stalled_deals.value.amount)绝不跨币种相加:保留每笔交易自身的 ISO 币种(customer_engagements.currency,默认 CNY),by_currency 为范围内每种实际出现的币种各一条 {currency,value},旧版的 value/amount 字段仅在该范围只有单一币种(或完全没有交易)时才是真实数值——一旦跨越多个币种立即降为 0,绝不是混合后的数字。只有季度营收、新客户数和赢单数可带已保存的公司目标,营收目标从存储的分转换为元,且该目标本身不区分币种。
weighted_forecast 汇总未结交易金额 × 概率 / 100,优先采用交易自身概率(包括零),其次采用组织配置的阶段概率,两者均缺失时按零计算。stalled_deals.value 为 {count,amount}:客户当前 Standards 活跃度为 at_risk 或 lost 的每笔未结交易分别计数并合计金额,不采用固定 30/45 天阈值。
at_risk.total 按客户去重。具名 items 含 kind、customer_id、customer_name、owner_email、reason_key,以及可选的 opportunity_id 和 amount。原因为 never_contacted(没有最后联系时间)、deal_concentration(单笔未结交易超过其自身币种在当前范围内管道金额的一半——绝不跨币种比较)或 stalled。每个停滞客户仅保留一条该原因,指向最大停滞交易,并将该客户的停滞交易金额合计写入 amount;与上述数字不同,这一按客户合计的金额尚未按币种拆分,若同一客户同时存在两种币种的停滞交易,仍会显示为一个相加后的数字。同一客户有多个原因也只计一次。每个数字及 definitions 中的规则均提供 {label,rule,standards_href},包含明确的 stalled 和 at_risk 键,且 standards_href=/knowledge/standards。此服务不会改变现有页面数字。
企业主、管理员和经理可读的团队成员数据
GET /crm/team/members?period=month 按命名的 week|month|quarter 返回每位销售一行数据,也可用 start/end(UTC 秒,结束不计入,end > start,跨度不超过 400 天)指定区间并标记为 custom。两种形式同时出现、都不出现或任何无效值都会在读取前返回 400 invalid_period。该读取面向组织所有者、管理员和经理(平台操作员的租户会话角色为 admin),销售代表在任何客户或业务行被读取之前即收到 403 forbidden;以“销售代表视角查看”的所有者同样被拒绝。组织与名单范围来自已验证的会话,不能通过查询参数指定。
每行包含 email、customers、open_opps、open_amount_yuan、won_period_yuan、stale_customers、last_activity_at 和 healthy|watch|at_risk 状态,按地址排序(忽略大小写),与所有者全景图同一时段的团队行完全一致(两个读取共享同一实现)。行只保留存储地址与复盘名单完全一致的销售,因此大小写变体不会产生重复的人。summary 为各行之和(people 为行数),period.elapsed_pct 为期间已过份额(四舍五入),金额按元计。不包含目标、应收、任务或画像数据。
每位销售和团队的转化率
GET /crm/team/conversion 回答“业务卡在哪一步”:对任意时间窗,按销售名单中的每位销售和整个团队返回两个比率——新业务进入“已报价”的比例(inquiry_to_quoted)和已报价业务的赢单比例(quoted_to_won),每个比率附上其背后的数量 {base,advanced,pct}。所有者、管理员和经理读取本组织数据(平台运营者的租户会话按管理员处理);销售代表在任何业务读取之前收到 403 forbidden。不带参数时时间窗为最近 90 天(end 为当前时刻,start 为 end − 7,776,000 秒);显式给出 start 和 end(UTC 秒,两者要么都给要么都不给)时使用该精确时间窗,即使其中的业务之后改变阶段,每次读取结果也相同。只给一个边界、边界非整数、end 不晚于 start 或跨度超过 400 天时,在读取之前返回 400 invalid_period。
响应包含 window、min_base(5)、quoted_stage("quoted",组织的管道没有“已报价”阶段时为 null)、team 和 members(每位销售一行——与团队复盘相同的非空客户负责人——含邮箱和两个比率)。团队数字汇总每位成员的业务,绝不是各成员百分比的均值。阶段“处于或超过已报价”指其为看板位置不早于“已报价”的开放阶段或赢单阶段;业务的初始阶段取其第一条阶段记录的 from_stage,否则取当前阶段。只有背后至少有 5 个业务时才显示百分比(否则 pct 为 null),四舍五入到整数;所有规则只读取 occurred_at ≤ end 的阶段历史,因此过去的时间窗永不改变。
月度复盘背后的已保存目标
GET /crm/team/goals?start=<s>&end=<s> 针对一个以 UTC 秒表示的显式时间窗,返回覆盖它的公司目标和名单中每位销售自己的月度目标,让经理能够把当月结果与人们设下的目标作比较。两个边界都必须是整数,end 必须晚于 start,跨度不超过 400 天——其他任何情况(包括命名的 period 形式)都会在读取之前返回 400 invalid_period。该读取面向组织所有者、管理员和经理(平台操作员的租户会话按管理员处理);销售代表在任何目标行被读取之前收到 403 forbidden。组织与名单范围来自已验证的会话,不能通过查询参数指定。
响应包含 month(原样返回请求的时间窗)、company 和 members。company 是覆盖时间窗第一天的 crm_company_goals 行——与所有者全景图使用同一“最晚开始的匹配”查找规则——形如 {period_start, period_end, revenue_target_cents},该行未保存营收目标时 revenue_target_cents 为 null,没有已保存行覆盖该日时 company 本身为 null。members 按地址排序(忽略大小写)为名单中每位销售保留一行 {email, revenue_target_cents},读取时间窗第一天的 crm_member_goals;当月未保存目标的销售回答 null,而不是零。此读取为只读:目标写入仍保留在仅限所有者的全景图路由和销售自己的月度目标路由,两者均不变。
业务阶段变更
POST /crm/engagements/:id/stage 将一笔业务移至组织管道目录中另一个已启用的阶段——请求体为 {stage, note?, reason_category?}。未知或未启用的 stage 键返回 400 unknown_stage,并附带该组织当前允许的键;stage 与业务当前所在阶段相同时,会在任何写入之前被拒绝,返回 409 stage_unchanged——这适用于所有阶段类型,不仅是输单阶段:重复提交或过期重试绝不能写入第二条历史记录,更不能悄悄用另一个原因覆盖已记录的输单原因。移入输单类阶段还须提供 reason_category,取值为 incumbent、price、sample_spec、cancelled 或 other 之一;选择 other 时还须填写 1 至 500 个(去除首尾空白后)字符的 note,且这一 500 字符上限对其他类别提供的 note 同样适用——缺失或超长的原因会在任何写入之前返回 400 {"error":"lost_reason_required","missing":"category"|"note"}。PATCH /crm/engagements/:id 按同样规则接受 stage:移入输单类阶段时以相同的 400 拒绝(目标与已保存的阶段相同时除外,那只是普通字段更新,无需提供原因),且该变更始终走与 POST 相同的规范写入路径——历史记录、客户时间线行、变更事件流与赢单自动升级都在同一个 D1 批次中完成,绝不是裸列更新。
每次写入都只追加一条 engagement_stage_history 记录,绝不修改已有记录:actor_kind(今天始终为 user;atlas 留待后续故事)、rule_id 与 sources(在后续故事赋予规则可命名、可读取的内容之前均为 null),以及仅由输单变更写入的 reason_category,与既有的 from_stage/to_stage/note/occurred_at/actor_email 字段并列。GET /crm/engagements/:id/history 按时间正序返回全部字段;sources 以 JSON 数组形式返回,未读取任何内容时为 [],绝不为 null。GET /crm/engagements/:id 还会附带 lost_reason——{category, note, occurred_at, actor_email} 或 null——其存在与否取决于业务当前所在阶段是否为输单类阶段:业务一旦被移出输单阶段,即使记录该原因的历史行从未被删除,也不再附带旧的原因。
GET /crm/engagements/:id/history 还会为每条带有来源的记录返回 sources_resolved:相同的 id、相同的顺序,每项为 {id, kind, label},其中 kind 取 fact、quotation、sample、document、verification、history 或 unknown 之一——客户需求事实读作 <fact_key>: <value>,报价单读作其编号(尚无编号时为 Quotation),寄样读作其说明,文件读作其文件名,赢单核实读作 PO verification · <po_number>(采购订单未注明编号时,取其证据中的文件名,再退为该词组本身),被撤销的阶段记录读作 stage move <to_stage>。解析严格限定在本组织范围内,且整段历史按类型各做一次批量读取,绝不按 id 逐条读取:属于其他组织、或已被删除的记录,其 id 返回 kind: "unknown" 并以原始 id 作为标签,而不会透露该记录的任何信息。没有来源的记录完全不带 sources_resolved 字段。
Atlas 阶段推进、撤销与规则状态
条件满足时,Atlas 将进行中的业务向前推进一步,且永不写入赢单类或输单类阶段。needs_confirmed 从 inquiry 触发:该业务当前的客户需求事实已包含产品、数量与时间,其中产品与数量为 stated_by: "customer",没有任何当前事实被标记 conflict,且每条来源记录的创建时间不早于业务自身的 created_at。evaluation 从 needs_confirmed 触发:该业务的某张报价单已有 sent_at 且 voided_at 为 null,或某条寄样记录的 shipped_at 已经到达(shipped_at <= now)且未被删除——shipped_at 由销售代表填写,最多可提前一年,因此预计将来发货的寄样只是计划,永不推进业务。触发点就是产生这些证据的写入操作:POST /crm/engagements/:id/needs/notes 及其重试、POST /crm/engagements/:id/quotations/:quotationId/send,以及设置 shipped_at 的寄样路由。每次推进追加一条 engagement_stage_history 记录,带有 actor_kind: "atlas"、rule_id(needs_confirmed 或 evaluation)与 sources——即事实 id,或单个报价单 / 寄样 id——并写入与人工移动完全相同的客户时间线行与变更事件。
保护机制。业务必须已处于该规则的预期阶段;两端阶段都必须是该组织目录中已启用的 open 类键;且目标阶段的排序必须严格晚于来源阶段:已走得更远的业务、处于自定义阶段的业务,以及处于赢单类或输单类阶段的业务,永不被移动。若某规则上一次的推进被撤销,或有人把业务往回拖过该次推进,则该规则保持沉默,直到 sources 中出现那条记录未曾包含的 id。预期阶段随写入一同传递:当业务已不在规则所读取的阶段时,Atlas 路径直接拒绝(409,不写入任何内容);而阶段 UPDATE 还带有 AND stage = <expected>,以拦住落在这两条语句之间的移动——因此并发的人工移动获胜,包括人工标赢单、超越到更靠后的阶段,以及人工先做了规则本要做的那一步,规则什么都不写——没有历史记录、没有时间线行、没有变更事件。GET /crm/engagements/:id/needs 继续返回 waiting_on(product_missing、quantity_missing、timeline_missing、product_customer_words、quantity_customer_words、conflict、source_before_deal),并且与规则使用同一个函数,二者绝不会出现分歧。
POST /crm/engagements/:id/stage/undo 接受 {history_id},在七天内撤销一次 Atlas 推进。权限与 POST /crm/engagements/:id/stage 相同:该业务所属组织的任何已认证成员。缺少 history_id 返回 400 history_id_required。未知业务、来自其他组织的记录,或属于另一笔业务的记录,都返回 404。不是 Atlas 推进、没有 rule_id,或发生在七天之前的记录,返回 400 not_undoable。当业务已不在该记录设置的阶段、其后已存在新的阶段变更,或 customer_engagements.updated_at 在本次请求读取与写入之间发生变化时,返回 409 changed_since 且不写入任何内容。成功时:以 customer_engagements.updated_at 上的比较并交换恢复阶段,追加一条备注为 undo 的 USER 历史记录(携带原 rule_id 与指向被撤销记录的 sources),记录一条 stage_rule_undos,并在同一批写入中补上该次回退的 customer_timeline 行与 change_events 行——业务自身的历史、客户时间线与变更流对“这次推进是否已被撤销”绝不会出现分歧:200 {"ok":true,"already_undone":false,"stage","rule_id","undo_count_7d","paused_at"}。对同一条记录再次撤销不是错误——返回 200 {"ok":true,"already_undone":true},且不改变任何内容。
在任意滚动七天内,同一条规则在全组织范围被撤销超过三次,该规则即自我暂停:设置 paused_at,规则评估为不推进,而 waiting_on 仍照常返回。GET /crm/settings/pipeline/rules 返回{"items":[{"rule_id","paused_at","undo_count_7d","enabled"}]},始终包含两条规则——从未被撤销过的规则没有状态行,这表示运行中,而不是缺失。undo_count_7d 在每次读取时从撤销台账重新计算,因此随着撤销记录移出时间窗而回落。POST /crm/settings/pipeline/rules/:rule_id/enable 清除暂停,并重新开始滚动时间窗——只有重新启用之后发生的撤销才会计入下一次暂停,因此管理员刚恢复的规则不会被紧随其后的一次撤销立刻再次暂停——然后返回同一份列表。它与其他所有阶段写入共用 pipeline 权限,因此销售代表会得到 403;而既非 needs_confirmed 也非 evaluation 的 rule_id 返回 400 unknown_rule。
月度复盘“为什么丢单”背后的业务
GET /crm/team/lost?start=<s>&end=<s> 列出一个以 UTC 秒表示的显式时间窗内,进入组织管道目录中丢单类阶段的每一次业务变动——窗口内每笔流失的业务都附带阶段变更时原样填写的丢单原因(流失原因对话框保存的自由文本备注),不分组,按时间正序排列。两个边界都必须是整数,end 必须晚于 start,跨度不超过 400 天——其他任何情况(包括命名的 period 形式)都会在读取之前返回 400 invalid_period。两个可选的查询参数可进一步筛选同一读取,且同样限定在同一组织范围内:category(与保存的原因类别精确匹配)和 q(对备注做不区分大小写的子串匹配)——留空或省略时,两者都返回全部保留的行,行为不变。该读取面向组织所有者、管理员和经理(平台操作员的租户会话按管理员处理);销售代表在任何业务行被读取之前收到 403 forbidden。组织范围来自已验证的会话,不能通过查询参数指定。
响应包含 month(原样返回请求的时间窗)和 items——从 engagement_stage_history 关联业务与客户读出的、每进入一次丢单类阶段的一行:{engagement_id, title, customer_id, customer_name, owner_email, to_stage, note, reason_category, occurred_at},其中 occurred_at 为毫秒时间戳。目录中不存在的阶段不算丢单;进入赢单或开放阶段的变动不会出现。note 是原样保存的丢单原因——变更未填写时为 null,读取方按缺失呈现,绝不编造原因。reason_category 为 incumbent、price、sample_spec、cancelled 或 other 之一——该列存在之前写入的行为 null。只读接口:不更改任何业务、阶段或备注。
负责人洞察文案
POST /agent/owner-insight 仅供已登录且有效的组织成员调用。请求示例:{"view":"overview","facts":[{"key":"revenue","label":"收入","display":"¥17万"}],"context":"可选"}。view 可取 overview、decision、work_item、performance、team、coming_up、org_health 或 money。须提交 1–20 条事实,键名为互不重复的小写标识;标签不超过 80 字,展示值不超过 60 字,背景文字不超过 500 字。Accept-Language 以 zh 开头时使用中文,否则使用英文。
响应包含 template、based_on、generated_at 和 source。网页把 {key} 替换为请求中原样提交的 display;模型文案不得自行写入数字、金额、百分比或数量词。模型输出无效、超时或服务故障时,返回该视图固定的后备句子,source:"fallback",并列出全部已提交事实。视图或输入无效返回 400 invalid_request;未登录返回 401。模型调用经受控网关按登录成员所在组织计量并留痕。
私人助理工作项
任何已登录成员都可存储自己的工作。组织和 owner_email 仅来自有效会话。读取、修改或引用他人的父工作项(包括同组织或跨组织),与不存在的工作项一样返回 404 not_found;未登录返回 401。
POST /agent/assistant/work-items 接收 {type,title,customer_id,opportunity_id?,task_id?,source_ref?,parent_id?,thread_id?}。类型为 opportunity|touch|analysis|plan|task;title 和 customer_id 必须为非空字符串。缺少 thread_id 时自动生成。创建返回 201 {item};同一成员相同 source_ref 的未结束工作返回现有项 200 {item}。done 或 dropped 后可重新创建。parent_id 必须是当前成员自己的顶层工作项。无效输入返回 400 invalid_request。
GET /agent/assistant/work-items 返回顶层工作及嵌套 children,没有结果返回 {items:[]}。?status=waiting_on_you 等状态筛选按报告状态返回平铺结果,包含子项。无效筛选返回 400 invalid_status。GET /agent/assistant/work-items/:id 返回 {item},含子项。字段包括 id、owner_email、type、title、customer_id、opportunity_id、task_id、source_ref、parent_id、status、status_reason、thread_id、created_at、updated_at(毫秒)、plan 和 children;不返回 org_id 或 plan_json。
PATCH /agent/assistant/work-items/:id 接收 {status,reason?}。允许 picked_up → working/paused/done/dropped;working → paused/done/dropped;paused → working/dropped。其他转换返回 409 invalid_transition。reason 最多 200 字符,仅 paused 保留。
PATCH /agent/assistant/work-items/:id/steps/:index 接收 {state:"done"|"skipped"} 并返回 {item}。不存在的序号返回 404,无效状态返回 400,已结束的工作或 Never 步骤返回 409。步骤包含 key、text_key、action_key、state 和 tier,初始状态为 todo。组织权限提供 by_itself、with_permission 或 never;权限读取失败返回 503 work_items_unavailable 且不保存。第一个非 never 的 todo/running 步骤为待处理步骤;若它需要许可,picked_up/working 报告为 waiting_on_you。步骤更新后无待处理步骤则存储为 done。Never 步骤仍显示。本接口仅存储进度;执行权限强制与后台执行由其他能力负责。
在后台运行工作项任务
POST /agent/assistant/work-items/:id/background 由 :id 所指工作项的已登录本人所有者调用,提交 {instruction};组织与 owner_email 仅来自有效会话。instruction 去除首尾空白后必须非空,并与聊天一样截断到 8000 字符。请求在这条任务下创建一个 analysis 类型的子工作项,继承父项客户,并携带稳定的 source_ref background:<parentId>:<sha256(instruction)>:相同指令始终指向同一个未结束的子项,重试因此是重跑该子项而不是新建。子项被置为 working 并开始运行;响应为 200 {item}——子项已在运行时返回同一答案,且不会运行第二次。父项不存在、属于他人、或本身是子项时返回 404 not_found;请求体不可读返回 400 invalid_request。
后台运行使用与聊天相同的受管控模型循环,仅开放只读工具并经同一网关归因计费:它只能读取与比对,绝不发送、批准或修改记录,客户也看不到任何过程。运行在 25 秒内结束。成功时,收集到的回答作为恰好一条助理消息追加到父任务的会话记录(会话写入会整体替换消息集,因此先读后写),随后子项唯一的计划步骤标记完成——不再有待处理步骤,子项在“进行中”里读取为已完成。失败或超时不向父会话写入任何内容;子项置为 paused 并附简短原因(最多 200 字符,超时为 timed out after 25s),仍列在其父任务之下,正因如此才能提供重试。
启动受组织的 work.background 助理权限约束:never 返回 403 permission_never,档位无法读取返回 503 permission_unverified;by_itself 与 with_permission 均可启动——本人发起的请求即为许可。
根据问题选择仪表盘视图
已登录成员可向 POST /agent/view-pick 提交 {"question":"销售情况怎么样?","locale":"zh-CN"}。去除首尾空白后问题须为 1–500 字符,语言为 en 或 zh-CN。受管控模型只从九个视图中选择名称和设置:business、team、person、client、decisions、coming_up、ai_team、money、org_health。例如销售问题可返回 {"view":"team","settings":{"team":"sales","period":"quarter"}}。设置仅允许销售团队、人员邮箱、由当前调用者 CRM 权限精确匹配的客户,以及 week|month|quarter|ytd|annual 时间段。模型不回答问题,也不生成数字。无法匹配或模型结果无效时返回 {"view":null,"settings":{}},并记录问题供复核;模型服务失败也返回空视图,但不记录。无效请求返回 400 invalid_request;未登录返回 401。
GET /agent/view-pick/unanswered?cursor= 按时间从新到旧列出本组织未匹配的问题,每页 50 条,格式为 {items:[{id,at,actor,question,locale}],next_cursor},游标按时间和 ID 倒序翻页。只有组织负责人或管理员能查看;其他已登录成员收到 {"items":[],"next_cursor":null}。无效游标返回 400 invalid_request。
按业务标题限量搜索
GET /crm/engagements?title_q=Bearing%20supplier%20qualification 按不区分英文大小写的标题片段搜索调用者可见的业务。服务端将 title_q 去除首尾空白并截为最多 100 个字符,在现有组织和角色权限内先筛选,再按最近更新顺序最多返回 11 条 {"items":[...]}。调用方展示前 10 条;如有第 11 条,应要求补充更完整的标题;items 为空表示权限范围内无匹配记录。明确传入但为空白的 title_q 也返回 {"items":[]},不会退回完整列表。每条记录包括 title、customer_name、stage 以及 amount 和 currency;金额是该币种的整单位(CNY 为元),未填写时为 null。不带 title_q 时,原有业务列表行为不变。
工作台客户概览
GET /crm/workbench/customer-summary 仅接受当前租户的普通登录会话。所有者/管理员查看公司范围,经理仅查看已分配的有效团队,销售代表仅查看自己负责的客户。公司范围包含公海和未分配客户;个人/团队范围仅包含已分配客户。CRM 查询前及返回前均实时验证 Auth 权限,响应不公开负责人名单。通用 CRM 权限保持不变;不接受委托令牌或内部断言,MCP/CLI 对等能力另行交付。
响应:{schema_version:1,scope,scope_revision,evaluated_at,definitions_version,customers,active,at_risk,open_pipeline:{deal_count,amount_by_currency:[{currency,amount}],unknown_amount_count}}。范围为 {kind:"company"}、{kind:"self"}、{kind:"team",team:{id,name,revision}} 或 {kind:"empty",reason:"team_unassigned"|"team_archived"}。有效空范围返回零值。evaluated_at 为服务器计算时间(毫秒);definitions_version 固定为 customer-current-book-v1。这是当前客户存量,不是月度流量或历史归属报表。
customers 统计所有状态;active 仅统计状态为 active 的客户,而非活跃度。at_risk 统计距上次联系整天数超过 30,或存在未结束商机且超过 45 天的有效客户,每个客户只计一次。天数公式为 CAST((evaluated_at-COALESCE(last_contact_at,0))/86400000 AS INTEGER),空联系时间使用零;任务不影响风险。open_pipeline 统计阶段不为 won/lost、且关联同组织范围内现有客户的商机,不限制客户状态。范围依据客户当前负责人,而非商机负责人。非空金额按币种独立求和,空币种默认 CNY;金额为完整货币单位,不除以 100,也不跨币种合并。空金额计入 unknown_amount_count;某币种若全部金额未知,则无金额合计条目。
GET /crm/workbench/customer-contributors?metric=customers|active|at_risk|open_pipeline&scope_revision=…&cursor=…&limit=… 返回 {schema_version:1,scope,scope_revision,evaluated_at,definitions_version,metric,totals,items,next_cursor}。totals 包含全部四项指标,与本页记录来自同一 CRM 快照。客户记录为 {id,name,status,last_contact_at};商机记录为 {engagement_id,customer_id,customer_name,name,stage,amount,currency},其中 name 为商机标题,customer_name 来自同一范围内的客户关联,空金额保持 null。默认 limit 为 50,最大 100,按 ID 升序。原样传回经过认证的 next_cursor,其绑定用户、组织、指标及范围版本;next_cursor:null 表示结束。打开明细时刷新合计与计算时间;跨请求数据可能变化,不提供跨页历史快照。
所有响应均含 Cache-Control: no-store。无效会话返回 401;不支持的凭证或实时拒绝返回 403;重复/未知参数、客户端权限提示、无效指标/页大小或篡改/其他主体游标返回 400。缺少/过期范围版本或权限变化返回 409 scope_changed 并丢弃记录,应先刷新概览。权限超时、缺失绑定/密钥、无效权限响应或 CRM 数据损坏/不可用返回 503,不能伪装成空成功;有效空结果返回 200。不接受公开组织、负责人、团队或角色参数。
工作台稍后读取
GET /crm/today/snoozes 返回当前登录用户在当前组织内仍有效的“稍后”记录,格式为 200 {"items":[{"item_key":"task:…","until_at":1780000000000}]};没有记录时,items 为空数组。此读取接口不接受其他用户或组织参数。POST /crm/today/snooze 将事项推迟三天,不修改任务、客户、业务或草稿本身。工作台根据返回的 item_key 过滤事项并更新计数;读取失败时保留上次成功读取的列表,若此前从未成功读取,则显示所有事项。
销售成员季度 KPI 目标
GET /crm/sales-targets?quarter_start=YYYY-MM-DD 返回 {quarter_start,items:[{member_email,kpi,target_value,updated_by,updated_at}]};省略参数时读取当前 UTC 日历季度。组织负责人和管理员可查看所有成员,其他登录成员只能查看自己的记录。未设置的 KPI 没有记录,目标视为 null,绝不当作零。
PUT /crm/sales-targets?quarter_start=YYYY-MM-DD 一次提交一条 {member_email,kpi,target_value},返回保存的记录。季度起始日只允许 1 月 1 日、4 月 1 日、7 月 1 日或 10 月 1 日。KPI 只允许 revenue_won、deals_won、new_customers、pipeline_added、win_rate、customer_touches。目标值必须为正整数,成交率为 1–100;金额采用整数元。仅负责人或管理员可写入,其他人收到 403;无效输入返回 400 且不保存。成员邮箱统一转小写,组织及修改人由已核验会话决定。周、月、YTD 与年度目标按各季度重叠的 UTC 日历天数折算;成交率取已设置季度的按天加权平均。现有“我的本月”个人收入目标和销售阶段概率仍使用原有接口。
负责人决策与搁置计划
GET /agent/assistant/owner-decisions 返回 {items,total,quote_routing_active}。每项包含 id(plan:… 或 quote:…)、kind、title、customer:{id,name}、requested_by:{email,name}、held_at 和政策引文 reasons:{title,section,quote}[];计划另有 plan_summary、proposal_id,报价另有 quote_decision_id。负责人和管理员可查看本组织未过期的搁置计划;报价路由启用时,仅负责人可查看待定报价。其他成员得到空列表。
POST /agent/assistant/owner-decisions/:id/decide 接受 {decision:"approve"|"reject",reason:string}。去除首尾空白后的理由须为 10–500 个字符;缺失或不合要求时,先于权限检查返回 400 invalid_reason。计划需由已登录的负责人或管理员决定,报价仅限负责人;其他人得到 403 forbidden。批准将理由作为政策覆盖说明,通过现有确认路径重新检查政策。报价使用负责人的会话调用 CRM,并保留 404 not_found、409 conflict。
政策阻止的计划自创建起等待七天,经 policy_changed 转为阻止的计划也适用。重复发送未修改的消息会复用已有的待定、拒绝或过期计划。销售直接确认或拒绝得到 403 owner_decision_required 和 owner_decision:{outcome,reason,at};界面仅说明等待、拒绝或过期,没有覆盖按钮。查看计划也返回该信息;重新申请需修改消息。其他计划仍在 15 分钟后过期。获批消息使用组织就绪邮箱,否则使用 OUTREACH_FROM_EMAIL;回复地址为发起销售。Atlas 地址缺失时记录确定发送失败。
拒绝或过期会为发起销售创建且仅创建一个任务,关联计划客户、附理由、当天到期。五分钟扫描重试失败的任务交付,以 owner_decision 和 plan:<proposal_id> 保证幂等。GET /agent/assistant/owner-decisions/log?cursor=… 返回 {items,next_cursor},每页 50 条,最新在前;字段为 id,at,actor,decision,item_title,customer_name,reason,outcome,过期操作者为 system。全部读写限定本组织。部署 Agent 前应用 0025_owner_decision_log.sql:新增日志,并在保留计划内容不可变约束的同时,仅允许政策搁置将过期时间延长至创建后七天。
历史发送尝试的状态(#3825)
GET /agent/prepared/:preparedWorkId/send-state 仅在当前登录用户所属组织的草稿没有发送尝试时返回 {"state":"none"}。若存在 CRM 持久命令,仍以命令及其编号和重试字段为准。若没有命令,较早的发件记录返回 {"source":"legacy","state":"sent","can_retry":false}、{"source":"legacy","state":"failed","can_retry":false} 或 {"source":"legacy","state":"queued","can_retry":false}。历史结果在有记录时可能包含 cause 或 message_id,但不会包含命令编号。sent 表示邮件服务器已接收,不代表客户已收到;queued 表示没有记录最终结果。考虑重新审批并发送前,应先核实失败或结果不明的尝试。命令重试接口需要真实的先前命令编号,不能重试这些历史尝试。读取仅限当前用户所属组织。若 CRM 状态读取失败或响应格式无效,接口返回 503 send_state_unavailable;只有已认证的 CRM 返回 200 {"state":"none"} 时,公开接口才返回 {"state":"none"}。状态不可用时不得推断可以发送或重试。
我的业绩数字
GET /crm/analytics/performance?member=me&period=quarter 接受 member=me 或销售邮箱,以及 period=week|month|quarter|ytd|annual。缺少或无效的时间段在计算前返回 400 invalid_period。负责人和管理员可查看任何成员;经理沿用团队复盘展示的销售名单(目前为组织内所有非空客户负责人)或本人;其他成员只能查看本人——比较不区分大小写,越权查看他人返回 403 forbidden。组织范围来自已验证会话,不接受查询参数。
响应为 {period,member,kpis,revenue_forecast}。period 使用 UTC 秒、结束时刻不计入,elapsed_pct 为四舍五入后的进度百分比;周从星期一开始,ytd 从 1 月 1 日至读取时刻(进度 100)。kpis 包含六项 KPI:revenue_won、deals_won、new_customers、pipeline_added、win_rate、customer_touches,每项为 {kpi,you,you_by_currency,target,prorated_target,team_mean,forecast,pace},pace 取 ahead、behind、on、no_target。you 为销售本人的数字:赢单按 updated_at 汇总金额与计数;新增业务金额按 created_at 汇总;新客户统计期间创建的本人客户;成交率为赢单占赢单与输单总数的四舍五入百分比(两者皆无时为 null);客户触达统计本人的沟通类时间线记录。对于两项金额类 KPI(revenue_won、pipeline_added),一旦销售本人在该 KPI 和时间段内的交易跨越一个以上币种,you 即为 null——绝不是混合后的数字;you_by_currency(每种实际出现的币种各一条 {currency,value})始终保留真实数字,且这种情况下的销售会被排除在 team_mean 之外,不会被当作零参与平均。
目标来自季度 KPI 目标:周、月按重叠的 UTC 日历天数折算各季度,成交率取按天加权平均;未设置的 KPI 的 target 与 prorated_target 为 null,pace:"no_target",绝不编造数字。prorated_target 为期间目标 × 已过进度;比率类不折算。team_mean 对在重叠季度存有该 KPI 目标的全体销售的 you 求平均(无人时为 null),无目标的销售不会拉低均值。金额为交易自身币种的整数单位,其他数字保留一位小数。
数量类预测(deals_won、new_customers、pipeline_added、customer_touches)为当前值加近 56 天周均速率 × 剩余周数;win_rate 预测即近 8 周成交率本身;revenue_won.forecast 等于 revenue_forecast.total_yuan。revenue_forecast 为 {won_yuan,won_by_currency,open_weighted_yuan,open_weighted_by_currency,open_deal_ids,trend_yuan,trend_by_currency,total_yuan}:期间内已成交金额 + 每笔未结交易 × 自身概率(未设置时用阶段概率,两者皆无按零)+ 近 56 天已成交金额 ÷ 8 × 剩余周数。won_yuan、open_weighted_yuan、trend_yuan 三者各自一旦其对应交易跨越一个以上币种即为 0——绝不是混合后的数字;对应的 won_by_currency/open_weighted_by_currency/trend_by_currency 数组始终保留真实的分币种数字。total_yuan 仍将这三个已按币种判定的数字直接相加,因此若某销售在同一期间的已成交、未结和近期趋势恰好分属三种不同的单一币种,总计仍会把不同币种加在一起——这是该接口目前唯一尚未做到按币种安全的数字。此接口只计算不存储,也不改变现有页面数字。
发布模式、生效日期与回滚
发布制度版本成为一步经过确认的操作(#3949)。POST /knowledge/policies/:id/releases 在 base_version 与 answerable 之外,接受可选的 release_note(文本,至多 500 字符)、mode(enforce——所有既有版本的默认值——或 warn_only)、enforce_at 与 effective_at(毫秒纪元整数),以及 notify: {readers, plan_owners}(两个布尔值,必须同时给出)。每个字段都可省略:本变更之前的请求体保持原样发布。enforce_at 仅适用于 warn_only 版本;组合或形状错误返回 400 invalid_request,不创建任何版本。工作流(workflow)的同一路径拒绝这些字段:它们仅适用于制度(policy)。
评估遵循发布节奏。effective_at 在未来的版本,在该日期到来之前不参与 POST /internal/policy/evaluate(其 id 仍计入 policy_set_revision,因此发布对固定引用方可观察)。warn_only 版本在 enforce_at 之前按 warn 评估,自该时刻起按 enforce 评估;本应 block 的结论仍会被记录——该 reason 以 outcome: "warn" 与 mode: "warn_only" 返回,且响应顶层的 would_block 为 true。每个 reason 都携带其评估所处的 mode;最强结论仍然跨版本胜出;无日期且为默认 enforce 模式的版本评估结果与从前完全一致。
POST /knowledge/policies/:id/releases/:releaseId/rollback(经理/管理员,可选请求体 {"release_note": "…"})把指定历史版本的存储内容重新发布为一条新的不可变版本,并记录 rolled_back_from_release_id。新版本为无日期的 enforce:它的意义就是立即生效。来源按调用者组织与 URL 中的制度双重定位,因此其他组织的版本、其他制度的版本与未知 id 是同一个 404——且不创建任何版本。已发布版本绝不就地修改;历史不可变。
GET /knowledge/policies/:id/releases 按新到旧列出各版本,每条在原有字段之外携带 mode、enforce_at、effective_at、release_note、withdrawn_at 与 rolled_back_from_release_id;单版本读取与发布响应携带相同字段。通知:notify: {readers, plan_owners} 仅做校验,Atlas 今天尚无按用户的通知通道,因此不会推送任何内容。发布实际可观察的效果是:发布说明通过版本列表触达读者;policy_set_revision 发生变化,代理的既有政策固定引用在下一次确认时返回 policy_changed("指引已变更——请再次确认")。CRM 参与方案的"指引已变更——刷新?"提示由剧本版本的固定引用计算得出,不受制度发布影响;按用户推送与该提示作为已记录的缺口留待跟进。
随制度保存的场景用例(#3950)
每条制度可保存至多 20 条场景用例——客户类别、动作类别、执行角色与负责人预期的结果——随制度本身保存在 memory_v2_policy_cases(迁移 0031_policy_cases.sql)。GET /knowledge/policies/:id/cases 对所有已登录角色返回 {items, last_run};每个条目包含 {id:"mpcase_…", title, customer_class, category, actor_role, expected_outcome, expected_section_id, created_by, created_at}。外部组织的 ID 与不存在的 ID 一律返回无法区分的 404 not_found。
PUT /knowledge/policies/:id/cases 接受 {items:[…]} 并整组替换,要求 Knowledge 规则权限(当前为管理员与经理);ID 由服务器生成。每个条目必须恰好携带 {title, customer_class, category, actor_role, expected_outcome, expected_section_id}:标题 1–200 字符,category 为 task-creation|record-change|draft-preparation|external-communication|administration 之一,expected_outcome 为 none|warn|block 之一,两个事实字段为 1–120 字符的字符串或显式 null(已知缺失),expected_section_id 为 null 或制度自身章节 ID 之一——其余情况或第 21 条用例一律 400 invalid_request 且不写入任何数据。
POST /knowledge/policies/:id/cases/run 接受 {draft?: boolean}(缺省为草稿),返回 {run_id:"mpcrun_…", items:[{case_id, expected, actual, section_id, pass}], passed, failed, ran_at}。用例以制度的草稿规则(draft:false 时为当前版本)与组织内其他所有已发布制度一起,经由同一个 evaluatePolicySet 评估器计算;用例未提供的事实视为已知缺失、不匹配任何条件,因此 actual 恒为 none|warn|block,pass 恰为 actual === expected。每次运行都追加到 memory_v2_policy_case_runs 并始终作为数据可读——用例运行绝不发送通知。
发布设有门槛:POST /knowledge/policies/:id/releases 会以即将发布的内容运行全部已保存用例,只要有用例的实际结果与预期不符,即拒绝并返回 422 {"service":"atlas-knowledge","error":"cases_failing","failed_case_ids":[…]};此时不发布任何内容,拒绝本身也不写入运行记录。全部用例通过时发布继续,并从存储的运行中读取通过数展示。用例在发布时自动重跑(运行记录关联新版本),并在 catalog_hash——仅为动作目录类别映射的 sha256,普通目录元数据变化不会触发——与已存最近一次运行不同时自动重跑:最早的一次用例读取会修复存储的结果。评估器无法读取的发布内容以 503 policy_unavailable 拒绝发布,绝不盲目放行。错误码:400 invalid_request、401 unauthorized、403 forbidden、404 not_found、422 cases_failing、503 storage_unavailable。所有路径均按组织隔离;任何请求字段都不能选择租户。
从公司文档提议制度章节与规则(#3952)
POST /knowledge/policies/proposals/from-document 接受 {document_id},仅负责人或管理员可用(Knowledge-rules 能力)。所指文档必须是调用者所在组织内其可读的公司文档;该读取在生成任何内容之前就按组织限定,不存在、属于其他组织或其他成员个人的文档返回 403 document_not_readable,且不生成任何内容。生成只调用一次治理的 Workers AI 网关,并以文档自身的段落为唯一来源。响应为 {proposal_id, document:{id,title}, sections:[{index,title,body,source_passage}], rules:[{id,sentence,applies_to,when,outcome,section_index,source_passage,checkable}]}。每项的 source_passage 由服务器校验为文档原文的逐字子串,校验失败的条目直接丢弃、绝不返回。规则的谓词不全是三个可信事实——applies_to 中的操作类别、actor_role、customer_class——时返回 "checkable":false 且没有 when。提案 7 天后过期。
POST /knowledge/policies/proposals/:id/accept 接受 {keep:[rule_id...], edits?:[{rule_id, sentence?, outcome?}]},创建一个草稿制度,包含提案的章节与恰好被保留和编辑的规则,其 source 字段作者为 atlas:<document_id>;被编辑的句子追加到所引章节的正文,被编辑的结果替换该规则的结果。其他组织的 id 与不存在的 id 同样返回 404 not_found;超过 7 天的提案返回 410 proposal_expired;未知的 keep id 或编辑未保留的规则返回 400 且不写入任何内容。此表面从不发布:草稿生来就是 Draft、没有发布版本,Publish 始终是单独的显式步骤。
已保存制度提案列表(#3968)
GET /knowledge/policies/proposals(Knowledge 规则能力——如今为管理员与负责人)按最新在前列出组织已保存的提案,最多最近 50 条。每项携带 {id, document_id, document_title, created_by, created_at, expires_at, kind, value, source_ref}。kind 在读取时从存储的载荷推导:"policy" 是章节与规则提案(#3952),"standard" 是公司文档标准草案,其提议的 value 与 source_ref 随行;policy 提案的 value 与 source_ref 读作 null。读取仅按会话限定组织,且严格只读——提议的值绝不会被当作事实。集合语义:不具备 Knowledge 规则能力的调用者与空闲组织读到同样的 200 { items: [] }(标准页正是把它渲染为“暂无提议的标准”状态),绝不是拒绝;存储损坏返回 503 storage_unavailable。
一条已记录覆盖的详情(#3962)
GET /agent/assistant/policy-overrides/:id(管理员或负责人——与旁边覆盖列表相同的角色;其他人得到一样的纯 404)返回 {item},一条已记录覆盖的入口详情:{id, at, actor_id, operation, proposal_id, subject_ref, customer_id, reason, policy_set_revision, category, customer_class, actor_role, sections}。覆盖记录本身不携带的两个事实——动作 category 与 customer_class——从同一批次写入的决策行中恢复;日志行丢失(其写入是尽力而为)时退化为该操作目录自带的类别和未设置的分类,绝不会读取失败。sections 列出每条被引用的 block 理由:{policy_id, policy_title, section_id, heading, quote}——正是“据此建立规则”草稿所读取的事实。按经过验证的会话限定组织:其他组织的 id 与不存在的 id 同样返回纯 404 not_found,绝不会跨组织读取记录;存储不可用返回 503 store_unavailable。
从一条已记录覆盖起草制度(#3962)
POST /knowledge/policies/drafts/from-override 接受 {override_id},位于 Knowledge 规则能力之后——与制度工作室的写入相同的边界(如今为管理员与负责人)——并在读取任何内容之前检查,因此不具备该能力的调用者被拒以 403 forbidden,不会创建草稿。所指覆盖严格通过 Agent 按组织限定的 GET /agent/assistant/policy-overrides/:id 读取,并转发调用者自己的会话,因此其他组织的 id 与不存在的 id 同样是纯 404 not_found,不会创建草稿;无法完成的读取返回 503 override_unavailable,绝不会返回半份草稿。
草稿由事件本身构成:一个章节,正文以被引章节的标题与引文开头,并附上越权者自己记录的理由;一条 schema-2 规则,适用于事件的操作类别,并在覆盖记录了客户分类与操作者角色时携带对其的 eq 谓词——未记录的事实让该谓词保持未设置,而不是阻断草稿。规则的结果是 block:收紧或放宽都在编辑器里进行,绝不在这里。草稿以 source = "override:<id>" 保存,生来就是 Draft、没有发布版本——此表面从不发布。响应为 201 {item, replay}:item 与提案 accept 返回的形状相同,另加 replay——对组织最近 30 天提案运行的新规则预览,编辑器由此展示新写法本会带来什么。错误:401 unauthorized、403 forbidden、404 not_found、400 invalid_request(缺失或超长的 override_id)、503 override_unavailable 与 503 storage_unavailable。
适用于一位客户的制度(#3963)
GET /knowledge/policies/applying?customer_id=(浏览器会话;每个已登录角色均可,与制度工作室自身的读取一致;组织仅取自会话——任何请求字段都不能选择租户)返回 { items: [{policy_id, title, release_no, section_ids, outcome_kinds}] }:本组织已发布、且至少有一条 schema-2 规则适用于该客户的制度。是否适用仅由规则自身的 customer_class 谓词决定——没有分类谓词的规则适用于每一位客户,已知缺失的分类不匹配任何规则,而 actor_role 谓词约束的是操作者而非客户,因此绝不会把一项制度从答案中剔除。section_ids 列出适用规则引用的章节,outcome_kinds 列出它们携带的结果种类(warn、block);绝不返回章节正文。
客户的分类通过 CRM 的内部分类读取解析,绝不读 EKB 的表。这是一个集合端点:未认证的调用者、缺失或畸形的 customer_id、不存在或属于其他组织的客户、无法完成的 CRM 读取以及损坏的已发布集存储,都回答同一个 200 { items: [] }——绝不是 404,也绝不猜测适用性。每个条目还携带 sections: [{section_id, heading, heading_zh}]——被引用章节的可读名称(发布时的标题,存有中文时并列中文),与 section_ids 一一对应;当存储的稳定 id 与章节记录不一致时整体省略,因此客户页的标签永远显示名称而不是原始 id。
作为被引用指令的流程(#3966)
已发布的流程(一个 workflow 发布版本)可以声明它适用于谁、适用于什么——可选的 applies_to,形如 {categories, customer_class, country},三个可选列表,每维至多 20 个非空值、每个至多 120 字——并且任何步骤都可以携带 cites: {policy_id, section_id},指明其背后的制度章节。存储的形状是冻结的:未知的键、超长的值或非列表会在流程创建与 PATCH /knowledge/workflows/:id 上被 400 invalid_request 拒绝。PATCH 上只有被提及的 applies_to 才会变动:缺席时保留已存声明原样,因此不建模该字段的客户端保存时绝不会悄悄丢掉它;null 表示清除。发布时该声明与其余内容一起按字节原样快照,已撤回的发布版本绝不再被匹配。
GET /internal/procedures/match?org_id=&category=&customer_class=&country= 是 EKB 面向 Atlas 服务的 Service Binding 接口,不是公开 REST 端点,使用与 POST /internal/policy/evaluate 相同的内部 HMAC 签名(绑定方法、路径与查询)。Agent 的准备路径询问哪个已发布流程治理它们即将撰写的面向客户的草稿,并得到 applies_to 匹配的最新已发布版本,或字面 null。org_id 为必填且被签名覆盖——内部调用上没有会话主体,被篡改的组织就是签名失败——category 必须是五个操作类别之一。未声明适用性的流程绝不匹配;流程声明了的每个维度都必须匹配:已声明维度遇到未知查询事实即视为不匹配,因此答案绝不会是无法核实其条件的流程。严格只读;缺失密钥、未绑定数据库或存储错误返回 503,绝不是静默的 null——只有真实的评估才能回答“没有适用的流程”。
Atlas Studio 合成图草稿
本候选版本注册 EKB 图草稿后端。编写、目录及布局接口只有显式配置合成模式才可使用;缺少 STUDIO_GRAPH_DRAFTS_MODE=synthetic、至少 32 字符的 STUDIO_GRAPH_DRAFTS_CURSOR_KEY、条件计算预算或 EKB 增量侧表时,接口保持不可用。移除这些编写配置后,仍可在当前权限及日志可用的前提下读取保留中的命令回执。这些接口不激活图、不发送消息、不预留模型,也不发布证明。原有 /ekb/workflows 流程继续独立提供服务。
使用普通组织会话 Cookie。Auth 在每次操作及命令重放时重新核验会话、成员身份、原生角色及有效权限。读取要求当前 knowledge:read;编写要求原生 admin 或 manager 角色及当前 knowledge:write。旧会话中的 owner 声明和 view-as 不能扩大当前权限。不接受平台会话、委托令牌或传入的 principal assertion。缺少身份为 401 authentication_required;当前权限存储不可用为 503 dependency_unavailable。
| 方法 / 路径 | 输入与结果 |
|---|---|
GET /ekb/studio/workflows?limit=10 | 显式 limit=1..100,可选签名 cursor。返回 {schema_version,request_id,items,next_cursor};已登录但无读取权限的成员得到空集合。 |
GET /ekb/studio/workflow-catalogs?limit=10 | 规范目录集合,含精确端口类型、配置字段、identity 映射、schema 引用及显式条件预算。单个版本使用 GET /ekb/studio/workflow-catalogs/:catalog_version。 |
PUT /ekb/studio/workflow-layouts/:layout_id | 规范 StudioGraphLayoutDocument;URI 须与内容摘要推导的标识一致。精确重放保留原不可变记录。读取或核对丢失的响应使用 GET /ekb/studio/workflow-layouts/:layout_id。 |
POST /ekb/studio/workflows | {command_id,definition};要求已通过布局 URI 存储的不可变布局引用,以及精确匹配的目录、schema、端口及映射。 |
GET /ekb/studio/workflows/:id/draft | 返回 {schema_version,request_id,item},含定义、修订号、摘要、生命周期与更新时间。不存在或跨组织详情均为 404 source_unavailable。 |
PATCH /ekb/studio/workflows/:id/draft | {command_id,patch};patch.id 须与命令一致,target、expected_revision 和 base_digest 须指向当前草稿。旧版本返回 409 revision_conflict。 |
POST /ekb/studio/workflows/:id/draft/validate | {definition_ref};返回 item 包装的校验结果,含 valid、invalid 或 blocked、安全字段诊断及检查时间。 |
GET /ekb/studio/commands/:command_id | 返回 item 包装的命令状态。响应丢失时保留原命令 ID 与输入。not_observed 要求继续核对状态,不能据此断言未执行。 |
命令返回 item 包装的 StudioCommandStatus;完成为 200,已准入但仍待处理为 202。无效 schema、目录、端口或映射会拒绝写入并保留原草稿。同一命令 ID 携带不同内容为 409 idempotency_conflict;精确重放在重新核验当前权限后返回原结果。错误为 {error:{code,message_key,request_id,retry,field_errors?}}。响应使用 Cache-Control: no-store 与 Vary: Cookie, Authorization。JSON 上限 64 KiB;未知权限字段及重复或未知查询参数均被拒绝。
先读取目录 GET /ekb/studio/workflow-catalogs?limit=10,再读取草稿集合 GET /ekb/studio/workflows?limit=10。采用的 internal.condition.v1 支持带类型的 field(...) 引用、比较、and/or/not、严格 UTC time(...) 与注入时间 now()。存在的 null 与不可用的 missing_fact 不同;unknown 遵循三值布尔表。解析、执行及读取的六项预算均由目录显式提供,不执行 JavaScript、属性遍历、回调或 I/O。编辑前保留精确的当前引用。每次成功编辑都会使旧预览绑定失效。集合游标绑定成员、组织、页大小及集合修订号;快照改变时返回冲突,须重新读取集合。扫描工作有明确上限;权限或存储不可用不会伪装成成功的空结果。本接口不提供删除、旧流程转换、激活或发布命令。
合成图预览日志(所属服务处理器)
Agent 所属服务处理器提供规范预览与日志接口,供中央路由协调接线;路由注册与能力发现是独立集成步骤,不代表生产可用或已激活。创建预览要求普通原生会话、Auth 当前成员身份、当前 knowledge:write 及原生 admin/manager 编写权限。读取使用当前 knowledge:read。Bearer、传入 assertion 及平台身份不能替代该会话。
服务器须显式配置 STUDIO_GRAPH_PREVIEW_MODE=synthetic、游标签名密钥、条件与控制器预算,以及 STUDIO_GRAPH_PREVIEW_PROOF_POLICY:{version,evidenceTtlMs,retentionMs,maximumTraceRows,requiredCoverage:"complete"}。不提供生产策略默认值。新计算及当前证明读取缺少依赖或策略时为 503 dependency_unavailable;未配置合成模式为 503 feature_unavailable。历史命令核对只要求当前 Auth 与日志可用,不依赖当前 EKB 或计算配置。retention 限制不可变记录的提供期限;本处理器不执行物理清除,也不授予删除或分享权限。
| 方法 / 路径 | 契约 |
|---|---|
POST /agent/studio/previews | {command_id,definition_ref,mode:"preview",model_source:"synthetic",scenario_ref,cohort_ref,override_ref,injected_time}。服务器解析精确的 EKB 草稿、目录及校验结果,保存结果、trace 与原命令回执。不接受真实适配器或客户端证明。 |
GET /agent/studio/previews/:id | 规范 item 包装快照,显式标注合成模式、无效果能力适配器、结果及可选的合成证据。已存证据链接只提供发现,不授予权限。 |
GET /agent/studio/previews/:id/trace?limit=10 | 显式 limit=1..100,可选签名游标。规范 trace 页绑定预览、定义引用、成员、组织、不可变快照及递增序号;公开行仅含已授权引用,不含原始私有输入。 |
GET /agent/studio/commands/:command_id | item 包装的历史命令状态,重新核验当前权限与完整性。来源变化或证明过期使当前证明失效,但不重写仍在保留期内的原命令结果。 |
GET /agent/studio/preview-evidence/:evidence_id | item 包装的不可变 StudioPreviewEvidenceRecord,重新核验计算、当前来源及真实时钟有效期。客户端对象的哈希不能产生发布权限。 |
有限来源适配器接受 synthetic.positive.v1、synthetic.negative.v1、synthetic.zero.v1、synthetic.null.v1 或 synthetic.unknown.v1,并要求 synthetic.cohort.single.v1 与 synthetic.override.none.v1。注入的场景时间与真实证明签发、过期时间独立。分支预览可在部分覆盖、无证明的情况下完成;节点失败保留安全诊断。不预留模型、不写业务数据、不发送消息,也不进行真实分派。
计算前先建立持久 pending 预留。202/pending 表示已准入,不表示完成。保留原命令 ID 与输入并核对状态;pending 不能证明未执行,也不能授权自动换命令重试。最终写入比较 pending 快照,将计算、trace、证据及最终回执原子保存。精确最终重放返回原状态;不同内容为 409 idempotency_conflict。不可变计算快照 2 与后续内联证明投影独立:保留真实签发时间,投影推进修订号及更新时间。这些记录仅是合成预览,不是发布、激活或 UAT 验收。当前来源、版本、策略变化或过期为 409 stale_preview;不存在或跨组织详情为 404 source_unavailable。全部响应为 no-store,并按 Cookie/Authorization 区分。
检查自己保存的合成预览运行
在同一 STUDIO_SYNTHETIC_PILOT_ORGS 与 STUDIO_GRAPH_PREVIEW_MODE=synthetic 试点内,具备当前 knowledge:read 权限的普通原生会话可检查自己创建并已最终保存的图预览。每次请求都会重新检查 Auth 成员资格、功能资格、创建者与组织、当前 EKB 图可见性、持久记录完整性及保存时确定的保留期限。证明过期或图被修改后,已记录的完成状态仍是历史事实,不代表证明当前有效。现有 evidenceTtlMs 仍限制实时预览证明;历史仅在保存时的 retentionMs 内可见。实时预览、跟踪和证明接口仍拒绝过期或已变化的证明。Bearer、平台身份与传入断言不能替代该会话。此只读视图不发布、不激活、不执行员工、不读取项目任务,也不直接查询其他服务的表。
| 方法 / 路径 | 响应 |
|---|---|
GET /agent/studio/runs?form=graph&limit=10 | {schema_version,items,next_cursor},版本为 atlas.studio.run-inspect.v1。仅列出当前创建者已最终保存的预览 ID。 |
GET /agent/studio/runs/:run_id | 直接返回 RunInspectionView:已记录的配置、执行、效果、费用、失败和不确定性。run_id 是已保存的 studio-preview:… ID。模式为 preview,效果能力为 incapable,未记录费用保持 null。 |
GET /agent/studio/runs/:run_id/events?limit=10 | 直接返回 RunEventPage,包含实际观察到的 graph_preview/graph_preview_trace 来源、稳定事件 ID、已记录时间、next_cursor、异常和 resync_required。不推断未记录的状态转换或成功效果。 |
列表和事件请求必须带 limit=1..100,可选 cursor;列表还必须指定 form=graph。缺少表单、请求更广范围或 staff/project,以及非预览运行类型,返回 503 unsupported_capability。其他创建者或组织的预览 ID 返回 404 not_found。不支持的方法返回 405;无效或重复查询字段返回 400 invalid_definition。
游标 MAC 输入与现有预览跟踪游标使用不同域,并绑定组织、操作者和流。续页仅引用上一页实际返回且获授权的最后一个 ID。策略变化返回 409 resync_required;游标过期返回 409 stale_cursor;无效或跨接口游标返回 400 invalid_cursor。提供 restart_without_cursor 时按提示重新开始。当前权限不足仍会拒绝;当前图可见性不可用或持久记录损坏返回 503 dependency_unavailable,不会伪装为完整的空历史。超过保存时保留期限的记录在有界索引扫描前排除;其详情和事件返回 404 not_found。空页仅表示保留期内的历史为空。列表最多检查 128 个候选项,并读取一个额外项检测溢出。SQL 在返回持久字段前将每个字段限制为 65,536 个 UTF-8 字节;跟踪行使用现有证明策略的 maximumTraceRows 加一个溢出检测项。超限即拒绝,不返回截断 JSON 或假装完整的事件流。响应采用 Cache-Control: no-store 和 Vary: Cookie, Authorization。
规则驱动的客户状态更改进入“已为你完成”(#3915)
“规则满足时更改客户状态”这一项权限(customer.status_by_rule)同时管理客户的两种规则状态:知识库→标准中的联系状态和热度标签。它是标准的上限:“自行处理”沿用每个过渡的 auto/propose 策略,并应用每一次热度下降;“需要许可”把每个过渡都变成提案,把每次热度下降变成一条待定保持记录加一条进入“待决定”的 warmth_change 建议;“从不”既不应用也不提案,同时趋势、days_since_activity、next_expected_at 与 computed_at 仍然更新。“自行处理”下应用的热度下降,或在“待决定”中被接受的热度下降,都会写入一条 warmth_change 类型的客户时间线记录,其确定性 id 为 warmth:<组织>:<客户>:<UTC日>。
GET /crm/assistant-changes?since=<毫秒时间戳>(按负责人限定范围:代表只能看到自己名下客户的更改)返回 200 {"items":[...]},最新在前,最多 200 行;since 缺省为 24 小时前。每行携带 id(更改的时间线 id)、customer({id,name})、from、to、rule(activity_level_change 或 warmth_change)、permission_key(customer.status_by_rule)、occurred_at,以及被撤销后的 undone_by/undone_at。撤销记录是另一种时间线类型,永远不会作为更改列出。
POST /crm/assistant-changes/:id/undo 恢复原状态且幂等:重复调用返回已存储的结果。联系状态引擎会把 crm_customer_activity 恢复为 previous_level——若客户在此之后已再次变动,则回答 409 {"error":"status_moved_on"},绝不覆盖更新的状态;热度则改为写入一条 undone 保持记录。两者都把原值写入 crm_status_rule_holds(附加式迁移 0107_status_rule_holds.sql),并绑定客户当前的 last_contact_at,因此所有读取路径在下一次联系之前都显示原状态,同时写入一条确定性 id 为 undo:<更改 id> 的 status_change_undo 时间线记录。跨组织的更改 id,或调用者无权查看的客户上的更改,与未知 id 一样回答 404 {"error":"not_found"}。
#3916 把两类收件箱更改加入同一信息流与撤销。customer.log_inbound 为 by_itself 时,一封入站邮件若其存储的匹配快照只有一个完全匹配的胜出者(comm_match_proposals.state = 'proposed'),会经 #259 的确认路径自动记入该客户,操作者为 assistant:<负责人邮箱>、链接 source = 'assistant';模糊、未匹配、出站及更低档位的邮件仍由人工审核,陌生发件人产生的新客户始终等待确认。信息流以 rule = "inbound_log" 列出这些行,permission_key 为 customer.log_inbound,并附 subject 与 match_reason(exact_email/exact_phone)。opportunity.update_from_inquiry 为 by_itself 时,规划器给出的业务/阶段建议会经同一“仅执行一次”的确认应用(其回执保证幂等),并以 rule = "opportunity_update" 列出,附 proposal_kind 与 from/to(阶段移动或新建业务的阶段);为 never 时规划器完全不生成业务/阶段建议。邮件正文不是这些写入的任何输入——起决定作用的只有匹配器的存储状态、分类的结构化答案与存储的权限档位。
撤销 inbound_log 会删除链接、撤回助手写入的时间线条目(这是唯一可以移除同步条目的路径,且仅因其操作者为 assistant:*——人工删除/更正的拒绝逻辑保持不变)、删除 comm_item_reviews 行、把邮件退回审核队列,并根据剩余的最新计入条目重算 last_contact_at——全部在一个批次内完成。撤销 opportunity_update 会按快照恢复原阶段与字段,或删除助手创建的业务;以业务 updated_at 为准的 CAS 会在有人在此之后修改过该业务时回答 409 {"error":"changed_since","changed_by":"..."}。GET /crm/communications/triage-summary 的每个关注项还附加了 email_excerpt——为“待决定”行引用的、不超过 280 字的邮件摘录。
客户备注可编辑与仅对同事可见的判断
PATCH /crm/customer-notes/:id 接受 {"body":"…"},返回 200 {item}:新正文、原有的 created_at 和前移的 updated_at。DELETE /crm/customer-notes/:id 返回 204。两者都限定作者:只有备注的登录作者可以编辑或删除,同一客户的其他读者返回 403 forbidden。跨组织的备注 id,以及不在销售自己名下客户上的备注,都返回 404 not_found——与未知 id 相同的回答,且不改动任何数据。正文去空格后必填,为空返回 400 body_required。不设字数上限,因为该备注通过 POST /crm/customers/:id/notes 创建时也没有上限。
当仍有 N 条提取出的特征通过 customer_facts.source_note_id 引用该备注作为依据时,删除被拒绝,返回 409 note_in_use 和标明数量的 {"in_use":N}。该引用不会被置空:AI 提取的特征始终保留它来自的备注,依据可追溯。通过 POST /crm/facts/:id/reject 否决特征不会删除其记录,因此产生过特征的备注会作为依据保留。备注通过迁移 0108_customer_note_updated_at.sql 获得 updated_at 列;此前写入的记录回填为各自的 created_at,该字段不会为空。
POST /crm/customers/:id/facts 接受可选的 category,取值为 preference、family、career、health、taboo、intent、relationship、other、reflection。省略时行为不变:一条已确认的手工特征,category=other、fact_key=note,且不过期。未知取值返回 400 invalid_category 且不写入;与类型化的 {"kind":"contact_preference"} 同时传入 category 也同样被拒绝,因为该类型自带固定分类。reflection 记录的是同事对客户的判断,而不是关于客户的可核查事实:它像其他特征一样存储、读取和交接,永不过期,并且无论状态如何都按分类排除在面向客户的内容之外。AI 提取器无法产生这一分类——只有人才能记录自己的判断。复用 x-atlas-idempotency-key 但改变分类会返回 409 idempotency_conflict。
客户时间线上的渠道标识
GET /crm/customers/:id/timeline 为每条记录增加 channel:引用已确认沟通条目的记录(ref_type:"comm_item")返回 email、wecom 或 whatsapp,其余记录(包括手工记录的联系)返回 null。已确认条目写入时间线时类型为 email 或 message,因此此前企业微信会话与 WhatsApp 会话在页面上无法区分;渠道从被引用的条目中还原,不重复存储。
该字段为追加字段,无需迁移。type、排序、分页(before、limit)、scope=communications 与 scope=customer-history 允许清单以及 synced 标记均不变。该查询按组织限定,因此引用属于其他组织的记录返回 null;查询只取渠道字段,源消息的任何附件定位符都不会返回。
沟通时间线的渠道、来源与业务
GET /crm/customers/:id/timeline 现在每条记录都带有 provenance——一个 {kind, by} 对象,kind 取值为 synced、sent、assistant 或 manual 之一:当记录的 actor_email 以 assistant: 开头时为 assistant(AI 助手代替前缀后所指的成员行事,该成员即 by);当记录引用已确认的沟通条目(ref_type:"comm_item")时为 synced,by 是关联该条目的人;当记录是一条 email_sent 行时为 sent(Atlas 在 by 批准后发出的邮件);其余情况为 manual,此时 by 就是该记录的 actor_email。记录还新增 engagement_id 与 engagement_title,标明这条沟通所涉及的业务;两者在没有关联业务时均为 null,被删除的业务会将指针清空回 null。channel 现在也会为手工记录的行返回——当记录人当时存了渠道时(从该行的 metadata JSON 中读取),否则仍为 null。
POST /crm/customers/:id/timeline 接受可选的 channel——wecom、whatsapp 或 email——存入该行的 metadata;未知取值不写入任何数据,返回 400 invalid_channel。同时接受可选的 engagement_id,必须是该客户名下的某个业务,否则不写入任何数据,返回 404 engagement_not_found。PATCH /crm/timeline/:id 接受相同的两个字段,均可传 null 清除;只有记录人本人写下的行才能被更正,这条既有规则不变。
POST /crm/communications/:id/confirm 接受可选的 engagement_id,会对照已确认记录的 customer_id 校验——若该业务属于另一个客户,返回 404 engagement_not_found 且不确认任何内容。存储结果新增 engagement_id(未设置时为 null),并像该处返回的其他字段一样,在重复确认时原样回放。
GET /crm/customers/:id/communications/pending 为新增接口:返回 200 {items: []},或返回 items,其中每项为 {id, channel, direction, occurred_at, match_state}——这些是仍在等待人工核对、且其存储的匹配快照(comm_match_proposals)将本客户列为候选对象的已同步消息,绝不包括已确认、已否决或已关联的消息。结果按 limit(默认 50,最大 100)分页;不返回任何正文、主题或附件定位符。该查询按组织限定范围,并进一步受限于调用者可读的邮箱授权与销售自己的客户范围;它为客户页面上“等待关联”的提示条供数,人工仍需逐条确认每一次关联。GET /crm/communications/queue?view=pending 接受可选的 customer_id,以同样方式收窄队列——只显示匹配候选中包含该客户的条目——提示条上的“去核对”链接正是这样打开队列的。
业务指针通过追加型迁移 0110_timeline_engagement.sql 引入,新增可为空的 customer_timeline.engagement_id 列与一个 IF NOT EXISTS 索引;ensureSchema 会探测该列,并为迁移执行前就已初始化的数据库补上它。此项没有回填:已同步的消息本就不带业务,因此没有任何既有记录可以事后被赋予业务;channel 也无需回填,因为它是在读取时从源条目联结得到的,而不是存储在该行本身。
客户与联系人编辑拒绝过期写入
PATCH /crm/customers/:id 与 PATCH /crm/contacts/:id 接受可选的 expected_updated_at 请求字段:即客户端开始编辑时读取到的该记录 updated_at(正整数,毫秒)。当它与服务器当前记录不再一致——期间已有第二个写入者修改过——这次覆盖式写入将被拒绝,返回 409 {"error":"stale_write","current":{...}},其中 current 是此刻按组织限定范围的服务器记录,客户端可据此展示对比,并明确地以新的基线重新提交。取值不合法(非整数或非正数)时不写入任何数据,返回 400 invalid_expected_updated_at。该字段缺省时,既有调用方的最后写入胜出行为保持不变;所有权变更所用的 expected_revision 前置条件相互独立、维持原样。此项不涉及迁移;无论是否携带该字段,跨组织的 id 始终返回同样的 404 not_found。
客户类型变更写入时间线
当请求确实改变了 customer_type 时,PATCH /crm/customers/:id 会同时向 customer_timeline 追加一条类型为 customer_type_change 的记录,并与字段写入处于同一个 D1 批次,因此变更不可能在没有记录的情况下落库。该记录以调用者作为 actor_email,metadata 为 {"from": "<key>|null", "to": "<key>|null"}——存的是组织分类目录的键,而不是拼好的句子,因此各客户端按各自语言解析显示名称。若请求设置的类型与客户当前持有的一致,则不写入任何记录;创建客户时带上类型同样不写入:创建不是变更。其他可以改动类型的写入路径(导入提交、录入批次提交)适用同一规则。此项无需迁移——customer_timeline.type 没有 CHECK 约束——且不做回填,因此本次发布之前的历史不会出现类型变更记录。该记录由 GET /crm/customers/:id/timeline 返回,scope=customer-history 下同样返回。customer_class 的变更不在范围内,不写入任何记录。
检索基准接口(内部)
知识检索基准(#4239)新增三个仅用于 staging 评测的接口:POST /knowledge/rag-bench/index,请求体 {"lane":"A"|"B"|"C","corpus_version":"<清单 sha>"};POST /knowledge/rag-bench/query,请求体 {"lane","query","top_k"};以及 POST /knowledge/rag-bench/reset,可带可选的 {"lane","corpus_version"}。三个接口都要求 Knowledge 写入权限(普通业务代表收到 403 forbidden),并按登录会话做组织隔离——请求体不能指定租户,每个组织只能看到自己的基准数据。各检索通道只索引本组织就绪库文件中已存储的段落(与沙箱读取同一路径),绝不重新解析二进制文件;所有模型调用都经由受治理的 Workers AI 网关,模型标识已冻结。查询返回按序的块级结果,含引用定位符、摘录与 model_usage 计数,并可附带 degraded/error 字段说明失败的模型阶段(记录失败,绝不中断)。这些接口通过增量迁移 0034_rag_bench_chunks.sql、0035_rag_bench_embeddings.sql、0036_rag_bench_graph.sql、0037_rag_bench_builds.sql 与 0038_rag_bench_chunk_progress.sql 建 ekb_rag_* 表;它们不改变已上线的搜索行为,属于内部评测接口,不是公开产品 API。
标签值整理与精确到值筛选(#4226)
组织里会不断积累含义重叠的标签值,两个整理操作负责归并。POST /crm/label-sets/{setId}/values/merge({"target_value_id":"…","source_value_ids":["…"]})把每个使用来源值的客户改挂到同一标签集的目标值上——同时使用来源与目标的客户最终只保留一个目标标签——随后删除来源值,并以 {"merged":{"customers":n,"values":m}} 返回有多少客户变更、多少值被合并。整个效果在一个原子 D1 批次中完成:任何拒绝都不改动数据。拒绝码稳定:400 target_in_sources(目标值出现在来源中);400 invalid_value_ids 并列出问题 id(目标或来源不属于该调用者组织内的这个标签集,跨集、跨组织同样适用);400 invalid_entity_type(联系人实体的标签集,联系人值的合并是后续版本);403 forbidden(无 labels:write 权限的调用者,以及 v1 不合并的个人标签集);404 not_found(调用者看不到的标签集 id)。受保护的行业集正常参与——合并重复的行业值是正当的去重,客户的行业派生跟随保留的值。
PATCH /crm/label-values/{valueId}/category({"category_id":"…"})把一个值移动到其他分类——集内或同实体类型的其他标签集——不改动任何客户或联系人的标签(它们引用值行本身,其 id 不变),并以 {"item":…} 返回更新后的值。客户列表的标签值筛选选项会立刻把该值列在新标签集与新分类下。拒绝:400 protected_set(值属于受保护的行业集且目标分类属于另一个集;行业集内移动分类仍允许);400 invalid_entity_type(目标分类所在集的实体类型不同);403 forbidden(调用者无权管理的组织标签集);404 not_found(值或目标分类对调用者不可见——外部 id 视为不存在)。移动的两端要求相同权限:个人标签集的所有者可以在自己的个人集内整理,但把值移动到组织标签集(包括受保护的行业集)与来源集一样需要标签管理权限。
客户列表可精确到值:GET /crm/customers?label_value=<id> 返回携带该值的客户,多个 id 用逗号分隔(label_value=a,b,列表内取或),并与既有的 label_set 筛选(取与)组合。值只经由调用者可见的标签集解析,因此未知、跨组织或他人个人标签集的值 id 一律匹配不到。为在整理前说明后果,GET /crm/label-sets 为每个公司实体的标签值附带 usage_count:携带该值的不重复客户数。
扫码连接企业微信机器人
连接器启用后,组织管理员可打开「插件 → 企业微信 → 连接企业微信」,选择「前往企业微信」。系统打开新的企业微信官方页面。请使用企业微信扫码并授权机器人,保留 Atlas 窗口直到显示「已连接」。授权成功后,Atlas 还须验证机器人订阅,才会确认连接成功。
机器人新收到的文字和语音转写将进入共享 CRM 收件箱。发起连接的管理员获得指定账户的读取/审核权限;其他成员须被明确授权。设置权限本身不授予消息读取权限。此路线不读取无关会话、历史存档或媒体,也不能发送消息。不支持的消息类型会记录在账户诊断中,不导入其内容。
二维码过期或取消后,请重新开始。关闭对话框会在当前浏览器标签页保留待完成的连接,再次打开即可继续查询状态。如果其他应用接管机器人,请先停止该应用的连接,再选择「检查连接」。暂停会停止导入,恢复会重新连接。断开会撤销已保存的机器人凭据,后续须重新扫码授权。已导入条目仍受原有访问规则保护。
REST 合约
以下三个端点要求当前组织成员拥有账户管理权限,平台支持人员不能代为授权。组织和发起用户来自已验证的调用者。请求体为 {};未知或其他用户、组织的会话返回 404。
POST /crm/integrations/wecom/qr/start返回{session_id, authorize_url, expires_at},expires_at为毫秒 Unix 时间戳。环境未配置时返回503 connector_unavailable。POST /crm/integrations/wecom/qr/:sessionId/poll返回{status, connection_id?, reason?},查询间隔至少三秒。状态为pending、connecting、connected、expired、cancelled或failed。响应中断后重试同一会话;已消费的会话始终关联原连接。POST /crm/integrations/wecom/qr/:sessionId/cancel返回相同状态结构。只有授权尚未消费时才能取消,否则返回原连接。
仅打开返回的官方二维码网址,并使用 noopener,noreferrer。不要处理提供方窗口消息,也不要记录二维码网址。机器人密钥仅在服务端加密保存,不会出现在这些响应中。现有的带版本校验的 /crm/communications/connections/:id/test、/pause、/resume、/disconnect 操作管理该账户。机器人账户不允许使用生成令牌的 /reconnect。
运维须配置 CRM 凭据密钥、R2 和 WECOM_BOT_CONNECTIONS Durable Object 绑定,并在验收环境启用 WECOM_BOT_CONNECTOR_ENABLED。默认关闭。出站 WebSocket 在 Cloudflare Durable Objects 中运行,产生持续运行费用;定时任务恢复意外中断的连接。此路线与下方旧版自建应用回调路线分开。
通过 Direct Meta 接收 WhatsApp 文本
适用于由组织所有者或管理员手动配置的 Meta Cloud API 账号。本连接器仅接收新的点对点文本消息,不发送消息,不导入历史、群聊、媒体或投递状态。成功查询账号不代表能够接收消息。
- 由部署操作员记录客户同意、Meta 应用与 WhatsApp 商业账号(WABA)及号码的归属、令牌所属应用、所需权限和资产分配,以及应用密钥确实属于该应用的证明。凭据和证明须保存在受保护的位置。仅成功查询号码不足以完成审批。
- 操作员在目标环境的 CRM Worker 上配置
WHATSAPP_APPROVED_ACCOUNTS。JSON 数组中每项包含org_id、app_id、waba_id、phone_number_id、approval_ref、expires_at(毫秒时间戳)及credential_sha256。证明引用对应已审核的材料;到期须重新审核。缺失、无效或过期的审批会禁用设置和回调。重复声明同一号码会使整个配置无效。 - 在 Superpower → 插件中打开 WhatsApp,连接已批准的账号。填写六个字段:号码 ID、WABA ID、Meta 应用 ID、访问令牌、Meta 应用密钥和独立的 webhook 验证令牌。令牌及密钥密封保存,不会返回。连接操作只读取提供方信息,不会订阅应用或更改 Meta 配置。
- 在 Meta 中将已批准的应用订阅到该 WABA 及 messages 字段。复制连接后显示的回调地址(账号页面也可查看),使用独立的 webhook 验证令牌在 Meta 中完成地址验证。回调路径为
GET|POST /crm/integrations/whatsapp/callback/:connectionId。 - 获得客户授权后,向商业号码发送一条新的测试文本。再次检查账号,并确认 Atlas 中出现相同文本。只有当前凭据及配置版本同时具备应用订阅、回调验证和已验证签名的消息,接收状态才会显示就绪。
计算审批指纹时,在受保护的 JSON 文件中提供 org_id 和包含六个设置字段的 credential 对象,然后在仓库中运行以下命令。命令仅输出摘要:
bun -e 'import { whatsAppCredentialFingerprint } from "./services/crm/src/services/integration-whatsapp-access.ts"; const v=await Bun.stdin.json(); console.log(await whatsAppCredentialFingerprint(v.org_id,v.credential));' < /secure/whatsapp-account.json
审核对应凭据证明后,才可将摘要写入 credential_sha256。SHA-256 的规范输入为以下数组的 UTF-8 JSON:["atlas-whatsapp-v1", org_id, app_id, waba_id, phone_number_id, access_token, app_secret, webhook_verify_token]。更新 CRM 密钥时须保留其他已批准账号。轮换凭据后必须重新审核指纹、验证回调并接收测试消息。修复不能更换号码、应用或 WABA 身份;不同身份须新建经过审批的账号。旧版仅含号码信息的账号可使用同一号码的完整凭据进行修复。
预发布操作员:首次审批配置
协调员分配预发布配置窗口后,使用受支持的发布脚本。检出目录必须为干净的 staging,且与最新 origin/staging 一致;ATLAS_SESSION_COOKIE_FILE 须指向已批准的平台操作员会话,并具备 Cloudflare 操作权限。
bash scripts/deploy-staging.sh crm --whatsapp-approval /secure/entry.json --entry-sha256 ENTRY_SHA256 --manifest /secure/manifest.json --manifest-sha256 MANIFEST_SHA256
entry.json 包含一项已审核的审批对象,使用上述七个字段。清单以 approval 重复该对象,并记录 account_id、worker、environment、token_expires_at、window_start 和 window_end。时间均为毫秒时间戳。审批到期不得晚于令牌到期或已分配窗口。两个输入文件均不得包含提供方凭据。
审核 expected.deployment_id、expected.version_id、expected.parts(模块名称、SHA-256 摘要及字节数)、expected.bindings 和 expected.settings(不含 bindings 的完整 Cloudflare 设置响应)。在 health 中固定七项预发布提交:web、crm、agent、auth、tasks、ekb 和 governance。密钥创建新版本时,Cloudflare 版本注释可以变化;运行设置必须保持一致,最终快照也须固定新的注释。提供两个已审核文件各自的 SHA-256;文件修改后会被拒绝。
此模式仅在 WHATSAPP_APPROVED_ACCOUNTS 尚不存在时创建配置。已有允许列表会被拒绝,避免覆盖未知审批。脚本从已部署代码发布新的 CRM 版本,核对代码、原有绑定、兼容设置及各服务身份,并在最终检查通过前保留预约。它不会上传本地 Worker 构建。若结果未知或失败,须先核实保留的预约,不得自动重试或回滚。配置成功并不证明已收到真实消息。
| 检查结果 | 下一步 |
|---|---|
approval_required | 请操作员更新该账号和凭据的审批。 |
subscription_pending | 在 Meta 中将已批准应用订阅到该 WABA。 |
callback_pending | 在 Meta 中验证当前回调地址和令牌。 |
message_pending | 发送经过授权的测试文本,确认收到后再次检查。 |
credential_rejected / provider_unreachable | 核对权限和账号身份,或待 Meta 恢复后重试。这些结果不表示就绪。 |
POST 使用应用密钥,对原始请求字节验证 X-Hub-Signature-256,然后检查 WABA 和号码身份。每个通知最多 256 KiB,消息、条目或变更分别最多 20 项;超限批次和畸形文本会在写入前整体拒绝。重复 wamid 只创建一个条目并保留原始消息文件。其他消息类型和状态通知会忽略。签名无效返回 401;账号、审批或载荷被拒绝返回 403;JSON 无效返回 400;请求体超限返回 413;存储失败返回 500 或 503,供 Meta 重试。有效通知成功接收后返回 200。POST /crm/communications/connections 拒绝通用 WhatsApp 创建请求,返回 400 provider_setup_required;请使用提供方引导端点。POST /crm/communications/ingest 拒绝通过连接器令牌提交 WhatsApp,返回 403 provider_callback_required。
暂停或断开会停止新的投递;断开还会撤销保存的凭据。已有条目保留。恢复或更换凭据会使旧配置的就绪证明失效。移除审批会阻止激活和新请求;已执行中的请求使用其捕获的环境配置直至完成,但写入时仍会再次检查到期和数据库中的撤销状态。提供方权限、客户同意和真实端到端收件是独立的部署验收前提。
企业微信与 WhatsApp 可连接通道及提供方 Webhook(#4304)
GET /crm/integrations 为可用提供方返回声明式设置文档。WhatsApp 在没有当前组织审批时为 coming-soon;参见Direct Meta 设置。企业微信为客户自建应用路线——字段 corp_id(文本)与密封保存的 corp_secret、callback_token、encoding_aes_key;WhatsApp 为 Meta Cloud API 直连路线——phone_number_id、waba_id、app_id,以及密封保存的 access_token、app_secret、webhook_verify_token。POST /crm/integrations/wecom/connect 与 POST /crm/integrations/whatsapp/connect(所有者/管理员 manage,请求体与引导式连接一致)在写入任何内容之前先向提供方验证凭据:企业微信用企业凭据对调用 GET qyapi.weixin.qq.com/cgi-bin/gettoken,WhatsApp 用访问令牌读取 GET graph.facebook.com/v23.0/{phone_number_id}、WABA 号码列表及应用订阅。只有验证通过才创建连接;拒绝时返回 400 provider_verification_refused,携带封闭的 cause(credential_rejected 或 provider_unreachable)以及凭据被拒时需更正的 field——失败即关闭,提供方不可达同样不创建任何内容,任何响应都不会复述提交的值。已连接账号与邮箱账号同等:列表、检查、暂停、断开、重连;账号检查重复同一提供方调用,细节令牌为 wecom:<cause> / whatsapp:<cause>。
每个连接获得公开的、面向提供方的回调端点(无会话、无连接器令牌;路径中的连接 id 是唯一的 webhook 标识):GET|POST /crm/integrations/wecom/callback/:connectionId 与 GET|POST /crm/integrations/whatsapp/callback/:connectionId。企业微信 GET 为 URL 验证:msg_signature 须为 Token+timestamp+nonce+加密 echostr 的 sha1,时间戳在 ±300 秒内,EncodingAESKey 的 AES-256-CBC 帧须能解密,且接收 id 须等于该连接的企业 id——全部通过才回显解密后的明文。企业微信 POST 对 Encrypt 主体携带 msg_signature;验证、解密后的文本消息经由与 POST /crm/communications/ingest 相同的归一化写入(按企业微信 MsgId 幂等;成功 200 success,投递被隔离时 500)。WhatsApp GET 仅在 hub.mode=subscribe 且验证令牌与该连接匹配时回显 hub.challenge(200 text/plain;否则 403,绝不回显 challenge)。WhatsApp POST 要求 X-Hub-Signature-256: sha256=<hex>,即以 Meta app_secret 为键(不是 webhook 验证令牌)、对原始主体的 HMAC-SHA256;签名有效但 WABA entry.id 或 metadata.phone_number_id 指向另一账号的通知被拒绝(403),文本消息按 wamid 幂等写入。签名错误分别以 401(WhatsApp)/403(企业微信)拒绝且不写入任何内容;暂停的连接拒绝投递;未知、畸形或已断开的连接 id 的应答与未知路由完全一致,因此失效的 webhook 不确认任何存在。所有写入项目的组织只经该连接的 webhook 标识解析——签名验证之前不信任任何提供方载荷。
文件审阅批次
审阅页面自有的接口(#4412,属于 #4407)。一个批次就是一次上传动作所包含的文件(不论从哪个入口);业务人员在 /knowledge/file-review/<batchId>?file=k 按文件逐页审阅,每页一次保存,只归档该份文件。
POST /crm/document-review/batches 接收 {source, files:[{library_file_id, name, mime, file_kind?}]},其中 source 为 assistant、library 或 files_tab,file_kind 为 library(默认,EKB 资料库文件)或 customer_document。返回 200 {batch:{id}, items:[{id, library_file_id}]},并不写入任何文件的客户关联。拒绍方式:400 invalid_source、400 files_required、400 too_many_files(超过 50 份)、400 invalid_file、400 invalid_file_kind。
GET /crm/document-review/batches/:id 返回 200 {batch, items},条目按待处理优先排序,每条携带带有来源标记的字段集。每个字段有且仅有一个来源:doc 读自文件、rule 由 Atlas 设定、you 等待业务人员填写。只有当字段的引用(阅读块序号与摘录)仍能在文件已存阅读结果中找到时,才标为 doc,并且每次读取都重新校验——阅读结果被修正或过期后,字段降为 you 且置空,并携带 dropped: "citation_unresolved",而不是保留已失效的摘录。阅读结果未写明的金额字段(amount、currency、received_amount)始终为 you 且置空,携带 dropped: "money_not_stated";绝不预填、绝不凭推测打标,也不允许取值 rule。未知或无权访问的批次返回 200 {batch: null, items: []};404 仅用于未指向任何对象的 URL。
PATCH /crm/document-review/batches/:id/items/:itemId/read 为内部接口,按请求体签名(沿用 next-action-mirror 的做法),由异步阅读作业调用。请求体 {org_id(必填——签名请求体是该回调唯一的组织范围), read_state, proposal_ref?},read_state 为 reading、read 或 could_not_read。组织只来自已签名的请求体,别无其他来源。拒绍方式:未签名 403 forbidden、400 invalid_request、400 invalid_read_state、503 authority_unavailable;其他组织的批次或已归档的页面返回 404 not_found——迟到的阅读结果绝不重开已保存的页面。
#4413 — 该回调还可携带 extraction?: {document_text, raw, model, filename},即作业产生的读取结果:模型所读的文本,以及每个字段一条带引文的候选值,CRM 会按同一文本划分出的块来校验它们。对于 library 条目,CRM 由此构建页面所需的带引文 DocumentProposal,并以资料库文件 id 为键存入 customer_document_proposals,因此 proposal_ref: "doc:<library_file_id>" 的解析与客户文档完全一致。它在没有客户的前提下构建——在助手中投递的文件是在任何人说明归属之前就被读取的——所以来自文档的字段带引文,而来自 Atlas 自身记录的额外信息(匹配的商机、重复提醒)缺席,而不是猜测。对于 customer_document 条目则忽略该读取结果,因为那个入口自己的读取已经带着客户上下文构建。若缺少 extraction,条目虽标记为 read 但句柄背后无物,此时 GET /crm/document-review/batches/:id 会将该条目报为 could_not_read 并附 read_note: "reading_unavailable",而不是展示一个打着已读标记的空页面。已签名请求体最多 64 000 字符,其中 document_text 上限为 20 000。
POST /crm/document-review/batches/:id/items/:itemId/read-now 是文件页签入口自己的读取(#4414),使用会话验证,由客户端发起:审阅页面会对每个仍处于 read_state: "reading" 且 proposal_ref 为空的 customer_document 条目调用它,每次最多四个并行 —— 每个此类页面都会被读取,但五十份文件的批次不会同时启动五十个读取。它存在的原因是:从文件页签上传的文件没有任何读取者 —— 异步读取作业只由助手的文件交接入队,而该入口并不经过它,所以条目会一直停在 reading、proposal_ref 为 NULL。它运行 CRM 自有的文档提议读取(与“从文件”入口相同的读取,针对 customer_document_proposals),且在请求内完成 —— 60–90 秒,因为 Worker 会在响应后约三十秒切掉脉离工作 —— 随后将条目标记为 read 并写入 proposal_ref: "doc:<document_id>";读不了则标记为 could_not_read且不写 ref。它绝不写 agent_document_read_jobs:该表属于 agent。幂等性有三重:已结果或已归档的页面直接返回当前状态且不运行任何读取;已存在的提议被复用而不重读;其他调用方仍在读取中(reading 提议行不足两分钟)则返回 202 {item} 仍为 reading,而不启动第二次读取。没有租约也没有重试行,因为这是请求范围内的工作:被放弃的请求会让条目留在 reading,下次打开页面时重新开始。返回 200 {item: {id, read_state, proposal_ref}}。拒绕方式:资料库文件返回 409 not_a_customer_document(其读取属于 agent);未知或无权读取的批次、未知条目、其他组织的批次、调用方无权查看其客户的文档,以及读取期间已被归档的页面,均返回 404 not_found;读取后仍未留下提议行则返回 503 read_unavailable。从文件页签删除的文档会连同其审阅页一起移除(#4412 UAT 第二轮):DELETE /crm/documents/:id 在同一次写入中删除该文档的审阅条目,并为该批次其余页面重新编号;GET /crm/document-review/batches/:id 不再返回 customer_documents 行已删除的条目;对此类条目调用 read-now 或 save 返回 404 not_found——页面显示“这份文件已不存在。”而非表单。
PATCH /crm/document-review/batches/:id/items/:itemId/fill 对应“应用到其余 N 份”。接收 {customer_answer?, customer_id?, deal_answer?, engagement_id?, pre_atlas_ref?},只填入该页,不保存任何内容——它不写入任何客户记录,响应也会回传 saved_at: null,让调用方自行核对。已按过保存的页面返回 404 not_found:填入绝不重开已归档的文件。拒绍方式:400 invalid_customer_answer、400 customer_required、400 invalid_customer、400 invalid_deal_answer、400 engagement_required。
确认新客户(STORY-148)。页面上明确的“确认客户”操作发送 {customer_answer: "customer", new_customer_name, deal_answer?, engagement_id?, pre_atlas_ref?},不带 customer_id。这是唯一会写入客户的填入请求:CRM 按 POST /crm/customers 的负责人规则创建客户(销售代表新建的客户归其本人),在同一个 D1 批处理中记录本次确认并把未保存的页面关联到该客户,返回 200 {item},其中带有新的 customer_id。同一页面、同一名称(忽略大小写、空格和标点)只创建一次:重试、重新加载或并发重复请求都返回同一客户,绝不创建第二个。文件本身仍未保存。拒绝方式:400 invalid_customer_name(为空或超过 200 个字符)、400 invalid_customer_answer(名称搭配其他答案,或同时带有 customer_id)、404 not_found(页面已保存,或调用方无权读取该批次)、409 confirmed_customer_unavailable(本次确认创建的客户已被删除或不再可见;不会重新创建)。
POST /crm/document-review/batches/:id/items/:itemId/save 是唯一的写入。它在一个动作里写入该文件的客户关联(资料库文件写入 customer_library_file_links——文件本体不移动,一条记录多个视角;客户文档则写在其 customer_documents 行上)、其业务答案,以及当金额与币种已知时的销售记录行。它不为批次内任何其他文件写入任何内容;在某份文件自己的保存按下之前,它没有为该文件写入任何内容。请求体携带页面上的答案,以及可选的 transaction(文档字段,包括业务人员修正过的值);修正值优先于阅读结果,且不带 doc 标记,因为那是人填的。所有来源(包括资料库文件)均可通过 document_kind 修正类型:quote、po、invoice、receipt、delivery_note、contract、spec、sample、other。已保存的分类优先于提取结果;只修改类型也可保存,未知类型返回 400 invalid_kind。保存绝不会在零写入的情况下完成:不会产生任何写入的请求以 400 拒绍,响应还携带实际写入行数 writes。修改已保存文件的客户会把文件及其销售记录行迁移到新客户,原客户处不留任何残留。当保存把 po 类文档绑定到某笔业务时,待核实的赢单核验行同步写入,并不启动、也不等待任何模型阅读——阅读在异步、可恢复的路径上进行,未能完成的阅读由现有核验 GET 结算为 cut_off。拒绍方式:400 customer_answer_required、400 invalid_customer_answer、400 customer_required、400 invalid_customer、400 deal_answer_required、400 invalid_deal_answer、400 engagement_required、400 invalid_engagement、400 pre_atlas_ref_too_long、400 nothing_to_write、400 invalid_transaction、400 invalid_amount、400 invalid_currency、400 invalid_date、400 invalid_status、400 invalid_kind、404 not_found。
所有路径均在边界处按组织隔离,每次读取除批次 id 外还按组织限定。表结构:0127_document_review_batch.sql 创建 document_review_batches 与 document_review_items,均由 CRM 拥有,增量且可重复执行;它们不对文件 id 建外键,因为生产环境的 EKB 是独立数据库(ADR 06),而该列按 file_kind 多态。每份文件的审阅行携带 lost_reason 列,此处写为 null,以使关联丢失那一故事无需再做迁移。
排序与标记。每条携带固定的 1 起始 position:页面顺序在创建时按上传顺序分配,并仅重排一次(待处理优先)——在所有文件的阅读均已结束且尚未保存任何内容时的首次读取——此后不再变动,以使 ?file=k 在业务人员处理过程中始终指向同一份文件;圆点仍反映实时状态。每条还携带 deal_seed,仅当阅读引用了某笔业务且该引用在当前阅读结果中仍能找到时非空(业务字段预选它并标为 doc,保存可用;绝不因为是客户唯一的开放业务而预选);customer_mark——读取匹配为 doc,由上传页面确定为 page,尚未回答为 you,而业务人员自己的答案为 null(不带任何标记);以及 proposed_new_customer_name,即阅读所命名、而 Atlas 尚无对应客户的买方名称,新客户表单由此预填。#4413 新增 proposed_customer_names——阅读在买方一侧命名、而 Atlas 尚无对应客户的所有公司,每个都作为单独的“新客户:<name>” 选项出现在客户选择器中,因此开给两家公司的发票会把选择权交给业务人员,而不是丢弃两个名称(proposed_new_customer_name 是其中第一个);以及 read_note,取值为 reading_unavailable、buyer_is_another_company、nothing_readable 或 null,说明为何一次已结束的阅读没有在页面上填入任何值。什么都不说的页面,和坏掉的页面无法区分。
授权按调用人而非仅按组织判定。创建时每个文件源都必须对调用人可见(客户文档通过其客户,资料库文件通过作出该答案的成员或其关联客户),否则在批次建立之前就以 400 invalid_file 拒绍。GET 对调用人无权查看的客户不返回 customer_name、deals 与 deal_seed,只返回文件及其字段。POST …/save 除所选客户外还校验源文档当前的客户,否则返回 404 not_found,使审阅页面无法重新归档其他成员的文档。
保存返回 200 {item, writes, sales_history_written, amount_pending}。writes 计算实际变更的行数,对已接受的保存永不为零;sales_history_written 与 amount_pending 让调用方能说出“已归档,并生成销售记录”或“已归档,在填入金额前不会生成销售记录”,而不是让“已归档”暗示一条从未写入的记录。对已归档页面的完全相同的重复保存以 400 nothing_changed 拒绍:把所有写入器当空操作再跑一遍并返回 200,等于告诉业务人员这份文档被归档了两次。修改客户文档的客户时,文档行与其销售记录行在同一个 D1 批次里一起迁移;除非所选业务属于新客户,否则清空业务答案。
GET /crm/document-review/items/for-file?document_id=<id> 或 ?library_file_id=<id>——二者只能具其一,因为两类 id 位于不同存储——返回 200 {item: {batch_id, item_id, position}},或 200 {item: null}。只持有文件 id 的界面由此打开该文件的审阅页:“我的文档”的“编辑”,以及金额空白时的“补填金额”。同时给出两个 id 或一个都不给,返回 400 invalid_request。没有审阅页的文件、其他组织的文件、以及调用人无权查看的文件,均返回 {item: null}——故意用同一种答案,因为能区分它们的响应就是对其他成员文档的存在性探测。
批次不是全组织可读的。只有创建它的成员,或能看到每一个文件源的成员,才可读取——是“每一个”而非“任意一个”,因为一个批次就是一次上传动作,其页面是整体阅读的。持有 drb_ id 并不等于获得授权:若无此规则,同事就能读到个人“我的文档”文件本身的字段——单号、日期、金额——这是上述逐条隐藏所不覆盖的。批次 GET(返回 200 {batch: null, items: []})、fill 与 save(404 not_found)以及 for-file 查询({item: null})遵循同一规则。
页面顺序由阅读回调确定,而不是由 GET 确定——GET 是只读的。当最后一份未保存文件的阅读落地时,未保存的条目在已保存条目未占用的位置上按待处理优先重排;已保存的文件保持其位置,而已保存但阅读从未落地的文件不会把整批永远固定在上传顺序。每批只确定一次。
关联丢失(#4416)。删除客户(DELETE /crm/customers/:id)或业务(DELETE /crm/engagements/:id)后,归档在其下的文件不再指向已不存在的对象。删除成功之后(被拒绝的删除不会改动任何文件),每份受影响的文件保留其本体与此前已读取的取值,仅失去那条已消失的关联,并在 document_review_items.lost_reason 上记录原因:JSON {kind, name, removed_by, removed_at},审阅页面将其呈现为“原属 Nordlicht Outdoor GmbH,于 10 月 2 日由 jane@forth.ai 删除”。本身没有审阅页面的文件会获得一个,放入按成员、按天的批次中,其 source 为 lost_association;GET 会返回该取值,但 POST /crm/document-review/batches 永不接受它,对三个入口之外的取值仍返回 400 invalid_source。删除业务会清空文件上的 engagement_id 与 deal_answer,客户答案保持不变。删除客户会清空资料库文件在 customer_library_file_links 上的答案(customer_id、deal_answer、engagement_id、pre_atlas_ref 置空),但保留该行,因此其已存的阅读结果(read_json)不会丢失。state 保持原样,不会被改为 not_customer_related——那是代替业务人员作决定;customer_id 为空的行在所有读取处都读作尚未作答,因为只有 state = not_customer_related 或已设置 customer_id 才算已作答。文件本体仍在 EKB,也仍出现在我的文件中。新建的页面以文件自身的名称为标题:资料库文件通过 EKB 解析(CRM 不存储文件名),EKB 无法给出时回退到文件 id;只要该文件曾被阅读过,read_state 就是 read,仅在从未阅读时才是 could_not_read。已被软删除的文件既不会被转为待处理,也不会获得页面。customer_documents 行仍随其客户一同删除,因为其 customer_id 对 atlas_customers 为 NOT NULL。保存任何答案(包括“与客户无关”)都会清空 lost_reason——刻意选择“与客户无关”是一个答案,而不是丢失——因此待处理集合正是 lost_reason IS NOT NULL AND saved_at IS NULL。
GET /crm/internal/document-review/needs-you?org_id=<org> 为内部接口,按 URL 签名(#4415,属 #4407),由 agent Worker 每五分钟的任务扫描调用。该扫描只通过本路径读取那三张文件表,绝不直接查询它们——这是与同文件内其他触发器有意不同的做法。组织来自已签名的 URL,签名覆盖整个 URL,因此 org_id 无法被替换。now=<epoch-ms> 由调用方指定自己的时钟,以便一次扫描对所有组织采用同一时刻。返回 200 {members:[{member, counts:{unsaved, blank_money, lost_association}, total, first:{batch_id, index}|null, lost:{kind, name}|null}]}。响应还携带 truncated与 uploader(第二个候选承办人:上传该批文件的人,仅在其不同于 member 时返回)。因为 member 是客户当前的所有者,而所有者邮箱并不等于成员资格:已离职的销售仍会留在客户记录上,而 Tasks 会拒绍非成员承办人。没有第二个候选人时,该成员的文件将在每个周期里对所有人隐形。agent 同时发送两个候选人,由 Tasks 自己的成员检查挑选——与已退役的单文件任务做法一致,其中 index 是文件自身已存的审阅位置(即 ?file=k 所指向的页码),直接读自记录,绝不重算。两种模式,其差别关乎安全:发现模式(不传 member)列出有待处理文件的成员,最多 50 位,列表被截断时设 truncated: true;调用方可据此建任务,但绝不可将其中的缺席当作零。指名模式(member=<email>,可重复最多 50 次)只回答所指名的成员,并为每位返回一行,包括 total: 0;该显式零是关闭每日任务的唯一依据,因此扫描会按名询问它要关闭的成员,而不是从缺失的行推测出零。三类原因互斥,因此 total 是文件数,任务正文的各行之和不会超过标题中的数字:已失去关联的文件计入 lost_association,即使它同时尚未保存。unsaved 包括:尚未按下保存的审阅页、没有业务答案的 customer_documents 行,以及已存到客户但没有业务答案的 customer_library_file_links 行——已有审阅条目的文件只按该条目计数,绝不重复。blank_money 是已归档的 po 或 invoice 审阅页但没有销售记录行,即文件未写明金额时保存所留下的空缺。已软删除的文档从所有原因中排除——不存在的文件不是任何人的待处理工作;lost_association 采用关联丢失那一故事自身的条件lost_reason IS NOT NULL AND saved_at IS NULL。每类原因均在与单文件任务相同的一小时宽限后才生效。计数对每位成员均按销售本人可见范围应用:客户不归其所有、或客户已进入公共池/已归档的文件不计入,因为任务绝不应指向承办人打不开的文件。拒绍方式:未签名 403 forbidden、缺少 org_id 时 400 invalid_request、503 authority_unavailable。由它驱动的任务以 trigger:doc-review:<member>:<utc-day> 为键,并在 metadata 中携带自己的跳转链接;trigger:doc-answer:<document_id> 不再新建,已打开的任务仍由其原有扫描关闭。
文档产品明细:GET 为每个项目增加可选的 document_kind 和 product_lines。每行包含 id 和七个解析后的字段:description、part_number、customer_part_number、internal_part_number、quantity、unit_price、line_amount。每个字段携带 value、origin、citation、how 和 dropped;未说明的值保持 null。中性料号不代表客户或我方的编码;客户料号和内部料号需要原文明确支持。
现有 Save 请求接受可选的 product_lines,最多 100 行,每行包含 id 和上述七个键,其值为字符串或 null。数量和单价使用非负十进制字符串,最多六位小数;行金额允许十二位小数。不要发送数字类型或指数形式。省略数组保留已保存明细;发送 [] 清空;null 或格式错误返回 400 invalid_product_lines。不完整的行不阻止归档。服务端解析来源:未修改的原文字段保留引用,修改标记为 you。明细按组织和文件保存,在另一批次重新打开仍保留,并沿用文件可见性检查。
产品小计使用精确十进制运算,与文档总额分开展示;缺少金额时保持未知。系统不推断税费、折扣或币种。报价单保存产品不会创建财务交易,即使旧客户端提交了交易金额;报价单审阅隐藏付款状态和已收金额。现有采购单、发票交易路径保持独立。旧建议没有产品明细仍可读取,旧客户端省略明细不会将其删除。
审阅尚未保存且没有已保存产品快照时,GET 可恢复此前遗漏的数字字段:原文明示产品表头,料号、描述及按顺序排列的三个数字单元格被拆为连续文本块时,返回值引用实际数字块。此读取不写入数据,也不调用 AI。重复标识、冲突值、行不匹配、数字不完整或表格不完整时仍保持未解析。已保存的空列表和已清空字段始终有效;保存未修改的恢复值时保留原文引用。
文件审阅备注:Save 接受可选顶层 note(去除首尾空白后最多 200 字符的字符串,或 null)。省略时保留原备注;null 明确清空,也会覆盖原文件中的备注。类型错误或超长返回 400 invalid_note。未提供顶层属性时接受非 null 的旧版 transaction.note;顶层属性优先。已保存备注以 you 来源且无引用返回,按组织和源文件隔离,在新审阅批次中仍保留。未编辑的文件备注保持原始来源。保存备注不会创建财务交易或 Communications 记录。
产品表中的 description 可跨连续文本块换行;数量与价格仍须由唯一料号所在的完整表格行支持。跨块描述的引用会重新核验全部连续片段;片段缺失或变化时不再作为有效来源。此修复也适用于旧校验器因行结构不匹配而拒绝的数值,但不会覆盖人工保存的数据。
报价保存请求可包含可选的 quotation_fields 对象,其中可传入 occurred_at、reference、amount 和 currency。省略字段保留原值;显式 null 仅清空该字段。例如 {"quotation_fields":{"reference":"QUO-2","amount":null}} 修改编号并清空金额,同时保留日期和币种。这些字段存入按文件隔离的报价快照,不写入销售交易。用户修改和显式 null 在重新打开审阅及“报价”页签中具有优先权。兼容旧版的报价 transaction 修改仅应用非 null 值;quotation_fields 中传入的字段优先。未修改的文档值保留引文。重复保存相同值返回 400 nothing_changed,保存时间不变。非报价文件传入该对象返回 400 quotation_fields_not_applicable。快照和审阅项的保存标记在同一 D1 批次中写入。重新归档后使用文件当前的客户和交易答案。
自定义类别:保存接受 document_category,去除首尾空白后最多 80 个字符,类型为 document_kind: "other"。省略保留原类别,null 清空。同一公司账号内不区分大小写复用类别。批次 GET 增加 custom_categories,每个项目增加 document_category。类别创建、确认分类与保存标记在同一 D1 批次写入。非报价单接受 review_fields,可包含 occurred_at、reference、amount、currency;省略保留,null 清空。这些字段是文档元数据;资料库文件和自定义类别不会创建财务交易。
GET /crm/customers/:id/reviewed-documents 返回已保存文档的 {items: [...]},包含 file_id、file_kind、name、document_kind、document_category、fields、saved_at 与当前商机答案。查询遵守组织、客户及原文件的实时访问权限;空集合或无权访问返回 200 {items: []}。重新分类会将文档移动到相应类型视图,文档金额不累计到销售或收款总额。历史文件没有保存快照时使用可用的原文值。
测量知识启用影响
此 staging 运维盘点返回数量、固定原因代码和摘要,不会启用知识页面。需要当前租户的有效会话及实时 knowledge:write 和 knowledgeFiles:read 权限。平台会话、委托令牌和客户端指定组织均被拒绝。读取前和返回前分别通过 Auth 验证当前权限。
GET /knowledge/census?family=page&limit=1
GET /knowledge/census?family=page&cursor=OPAQUE_CURSOR
GET /knowledge/census?family=impact
bun scripts/knowledge-census.ts --expected-sha EXACT_STAGING_SHA --sessions PRIVATE_JSON_FILE私有 JSON 文件将各租户 ID 映射到本地 Cookie 文件路径,每个 Cookie 文件只包含对应租户的 atlas_session=... 请求头。两类文件都须保密。命令使用现有 Cloudflare 运维身份及获准的有界 staging D1 读取器枚举租户 ID,源内容不会离开 EKB。命令拒绝重定向并验证 staging EKB 的精确版本。
类别包括 page、answer、book、book_legacy、file_derived、legacy_file_derived、file_derived_history。每页固定 limit=1。加密游标绑定租户、操作者和类别,一小时后过期;过期后重新运行。impact 统计当前已发布的政策和流程,并列出运作段落、运作沙盒上下文、语义搜索、派生元数据及关联投影的损失。
响应字段为 schemaVersion、orgId、buildSha、buildRef、rolloutMode、family、total、scanned、counts、reasons、digest、nextCursor、impact,不含标题、正文、来源链或来源标识。命令核对总数并重复完整盘点。租户授权缺失或任何拒绝、不完整结果均报告 complete:false;数据变化报告 drift:true,两者都不能生成批准摘要。两轮相同不等于原子快照,启用前须立即重查。
分类表示审核者权限下、以记录所有者身份检查的来源链,不代表每位用户均可访问。可重建内容须生成新草稿并通过审核。产品负责人须接受实际扣留内容及功能损失,否则保持 R2 未启用,等待 STRUCTURE021。Migration0041 在未启用时也会保留被替换或删除的派生历史;core 启用后不能退回 inactive。本报告不授予启用权限。
这是通用 REST/MCP/CLI 对等要求下的管理员运维例外:仓库命令使用同一所有者接口,不新增公共 MCP 工具或 Atlas 动作目录条目。身份或权限拒绝为 401/403,查询或游标无效为 400,启用状态变化为 409,权限服务、存储或版本不可用为 503。修复前提后重新运行,不得通过直接读取内容 SQL 绕过。源码文档不代表已经部署。
impact 也使用加密游标,每页检查一个当前发布指针。total 包括所有当前指针;policy、workflow、other 仅统计该页仍处于 released 状态的条目,命令负责累加。摘要绑定精确指针、发布身份、版本、正文原始字节以及运作和撤回状态。数量相同的发布替换也会被检测到。发布缺失或指针不匹配时拒绝报告。
Knowledge 页面与来源权限
此接口已实现但默认未启用。只有在完整的目标数据普查和产品影响审核通过后才可开放。本文不代表已在 staging 或生产环境上线。
GET /knowledge/pages?limit=50
POST /knowledge/pages
GET /knowledge/pages/:id?view=approved
GET /knowledge/pages/:id/revisions
POST /knowledge/pages/:id/revisions
GET /knowledge/pages/:id/revisions/:revision
POST /knowledge/pages/:id/decisionsGET 列表接受 limit(默认 50,上限 100)、cursor;页面列表另接受 view=approved|current,不接受 scope 筛选。结果为 {items,nextCursor},无权访问的行不会出现在游标中。详情为 {page,revision},单版本为 {revision}。创建和追加版本返回 201;决定返回 200。
PageCreate: {scope,title,content,sources,origin,answerId?,idempotencyKey}
PageUpdate: {expectedRevision,title,content,sources,idempotencyKey}
PageDecision: {expectedRevision,decision,reason?,idempotencyKey}
ObjectRef: {owner,kind,id,revision?}scope 为 personal|company;origin 为 authored|saved_answer;decision 为 submit|approve|reject|retire。saved_answer 必须引用当前用户持有的 answerId,正文和完整来源由服务端取得;调用者不能伪造或缩减来源。批准后编辑生成新草稿,原批准版本继续可读;审批必须匹配当前提交版本。REST action 统一使用 /assistant/v1/actions/<action>,不会直接访问 Worker 域名。
ekb.page.list, ekb.page.read, ekb.page.create, ekb.page.update, ekb.page.decide, ekb.page.revisions.list, ekb.page.revisions.read.
个人页面仅限所有者;公司页面的贡献者需要当前 knowledgeFiles 写权限,审核需要 knowledge 写权限。普通公司读者只能看到已批准版本;view=current 仅供作者或审核者。每次读取及重试都重新检查全部来源。来源被撤回、修正或无权访问时,整份衍生内容不可用,包括摘要、引用、相关标题和数量。没有完整来源记录的历史内容以及 Sales 依赖内容保持不可用。core 回退仅从当前原始片段进行关键词检索;纯语义检索返回 source_unavailable,未验证的向量排序不会进入模型上下文。原始文件标题、正文片段、下载和原有权限不变。
读取需要 ekb:read;返回页面内容的 delegated 写操作同时需要明确授予 ekb:read 和 ekb:write。idempotencyKey 是 EKB 操作标识,不能与提案或确认请求的 idempotency_key 混用。保持相同正文和标识重试;409 conflict 表示需要重新读取并审核当前版本。404 source_unavailable 不披露不可读来源;503 read_unavailable 表示无法确认读取结果。内部适配器可使用签名 URL 的 receiptOnly=true 仅恢复既有回执;无回执时返回 404,绝不产生新操作。
内部 Service Binding 就绪检查:GET /ekb/internal/knowledge-pages/readiness,需要 audience=ekb、action=ekb.page.readiness 的当前签名 principal,不接受普通 Cookie 或 Bearer。只返回 {schemaVersion:1,enabled:boolean},不返回租户内容。发现和提案在每次请求时检查此状态,执行仍由 EKB 再次把关。
语义检索覆盖情况与重试(STORY-157)
GET /knowledge/files/:id 为每种类型的 ready 文件在 item 中新增 semantic_coverage:{"state", "embedded", "total"}。total 为文件的段落数,embedded 为已存入语义检索的段落数。state 取值:pending(等待嵌入任务,包括刚发布的文件)、partial(total 段中仅 embedded 段被覆盖)、failed(当前版本的任务失败)、complete(已覆盖)或 none(文件没有可检索的文本)。草稿、处理中和失败的文件不含此字段;确认、重试、重新抽取与更新(PATCH)的响应在返回就绪文件时会带上此字段。该读取沿用详情本身的可见性:调用者无权查看的文件返回 404,绝不透露其覆盖情况。表格的 xlsx_index.coverage 保持不变。
POST /knowledge/library/files/:id/embedding/retry(无请求体)为失败文件的当前版本重新排入嵌入任务。仅限管理员:其他角色返回 403,且不做任何更改。调用者无权查看或不存在的文件返回 404。仅当 semantic_coverage.state 为 failed 时接受;否则返回 409,error 为 not_failed(文件未就绪时为 wrong_status)。成功时返回 200 {"semantic_coverage": {"state": "pending", "embedded", "total"}};任务随后在队列中运行,完成后详情读取显示 complete 或 partial。503 queue_unavailable 表示任务未能发送;文件保持 pending,五分钟一次的回填会在一小时内重新发送。这是会话(网页)路由,没有对应的 MCP 工具或 CLI 命令。
来源优先:两步式沙盒回答(STORY-155)
POST /knowledge/sandbox/ask 的请求不变,默认仍返回与以前相同的单个 JSON 答案。若调用方希望在答案写出之前先拿到找到的文档,请发送 Accept: application/x-ndjson。此时响应为 200,content-type: application/x-ndjson; charset=utf-8,每行一个 JSON 对象:
{"step":"sources","sources":[{"file_id","file_name","passage_index","snippet","sheet"?,"range"?}],"scope":{…}}—— 召回一完成就写出,早于模型撰写答案。每个来源都是答案将据以撰写的段落,形状与closest相同;只有表格行才带sheet和range。{"step":"answer",…,"server_timing":"scope;dur=…, …"}—— 与 JSON 响应完全相同的正文,外加 JSON 响应中由Server-Timing响应头携带的各阶段耗时(正文开始发送后无法再发送响应头)。
若在来源行之后作答失败(例如 409 source_invalidated),最后一行为 {"step":"error","status":409,"error":"source_invalidated",…},绝不会返回半个答案。在召回之前就结束的提问只写最后一行,状态码与 JSON 响应相同:400 invalid_request 之类的拒绝为 {"step":"error","status":400,…};表格路由或空范围为一行 {"step":"answer",…},并带 Server-Timing 响应头。没有最后一行就结束的流表示连接已中断:保留来源并重新提问。助手、MCP 工具和 CLI 不发送该请求头,行为不变。这是会话(网页)路由的可选项,没有对应的 MCP 工具或 CLI 命令。
审核客户电子表格
向助理上传包含明确公司和联系人表头的单个 XLSX 或 CSV 文件,将进入客户批量审核。联系人名录的“市场”映射到 company.main_markets(主要市场);“类型”(如品牌商、商超连锁)作为业务信息保存在 company.background,不会设置生命周期字段 customer_type。
PATCH /crm/intake-batches/:id/review 接受 {version, changes: {candidate_id: {corrections, cleared_fields, match_choice, append_contacts, selected, skipped, reviewed}}}。修正使用提议字段路径。空修正字符串撤销该覆盖值;cleared_fields 是明确清空字段的完整数组。append_contacts: true 将已批准的联系人添加到现有客户,保留主要联系人并跳过已存在的邮箱。通过 corrections 批准联系人姓名及需要复制的可选字段。现有公司字段仅在明确批准后更改。
编辑后和提交前都会在调用者可访问的客户范围内重新检查匹配。发现匹配时,须明确选择 match_choice: {customer_id, choice: "update"}、"create_separately" 或跳过。匹配变化会返回 invalid_match_choice 或 candidate_changed:刷新、比较详情并重新选择。审核本身不会创建客户或联系人。确认导入时,使用当前版本、所选候选 ID、审核指纹和幂等键调用 POST /crm/intake-batches/:id/commit。打开结果中的客户链接,查看主要市场、背景和联系人。
reviewed: true 单独记录“保存并确认”的审核状态,与选择导入无关。后续字段或匹配修改会取消审核状态,除非同时重新确认。导入包含全部剩余有效行;未审核的徽章会标红,用户可选择“仍然导入”。未解决的客户名称或重复匹配仍会阻止导入。仅选择导入不会标记为已审核。
发票状态与滚动窗口恢复时间
GET /governance/platform/billing供平台超级管理员读取,可选orgId指定组织。返回的发票、付款、分配及分配撤销列表各最多500条,审计事件最多1000条。每张发票的状态按读取时点的完整组织内分配、撤销及已生效核销历史计算,不受返回列表条数限制。未来生效的核销以及读取时点之后的活动不影响该状态。不增加分页或响应字段。
GET /governance/billing/windows允许具备账单读取权限的成员查看本人账户;具备账单写入权限的管理员还可查看组织账本中最多200个用量身份。账户窗口按同一邮箱跨组织汇总已记录且非空的customer_credits,包括预留仍待结算但已计价的用量。现有套餐选择及调用准入规则保持不变。每个窗口返回{usedCredits,limitCredits,resetAt}。达到限额时,resetAt表示在没有新增已记录用量的前提下,足够用量移出窗口、使总用量严格低于限额的最早毫秒。窗口包含截止时点,因此须在相关用量时间加窗口时长后再加一毫秒。未达限额时,它表示下一批正用量移出窗口的时间。没有正用量、未配置套餐或限额为零时,resetAt:null不承诺自动恢复。
内部Service Binding路由POST /governance/internal/credit/reservations可能返回409 account_window_exceeded及{window,usedCredits,limitCredits,resetAt};公共客户端不能调用此路由。原因仍标明第一个阻断窗口(先检查5小时),而resetAt须等待所有阻断账户窗口解除;任一窗口无法通过用量移出恢复时,该值为null。没有恢复时间时,请联系管理员查看账户限额。新增用量可能推迟预计时间;组织、余额及其他限额仍可能阻止调用。
账单与助手将预计恢复时间向上取整至显示的分钟,确保“起”时间不会早于API时点。下一批用量移出窗口的时间显示为“最迟于”。
工作台任务(Tasks v2)
Tasks v2 第一版为每个人提供一个工作台数据源,并用四条规则取代嘈杂的自动触发:久未联系(TT-01)、需要下一步的商机(TT-03)、缺少必填信息的客户(TT-07),以及现有的每日文件审核。任务由 agent 创建和关闭;CRM 根据组织设置计算应办事项;网页只读取一个数据源。所有路由的组织都来自会话或签名 URL,从不取自请求体或查询参数;没有内容时,列表一律返回 200 和空集合。
工作台数据源
GET /crm/workbench/feed?done=today|week(会话;任何成员)返回调用者本人的 {rows, reach_out_more, suggestions, housekeeping}。响应还带有 reply_within_hours,即组织在「设置 › 任务」中设定的回复时限(邮件等待超过该时限的回复行显示为逾期;读取该设置失败时不返回此字段);回复行的 email.triage 带有 asked_by:当客户邮件明确要求在某个日期前办理时为 {at, quote}(at 为毫秒时间戳,取组织时区内该日的最后一毫秒,邮件写明具体时间时取该时间;quote 为邮件原文),否则为 null。rows 是调用者的任务,用调用者本人的会话从 Tasks 服务读取:所有未完成任务,加上自 UTC 零点起(done=today,默认)或最近七天内(done=week)关闭的任务。每行为 {task_id, kind, type, section, status, title_i18n, reason_i18n?, params, customer, stake, due_at, route, closed_at, closed_by}。kind 为 reply、gone_quiet、deals_next_step、missing_details、doc_review 或 task;section 为 reply、coming_up、reach_out 或 housekeeping。title_i18n 和 reason_i18n 为 {en, zh},每个字符串只有一种语言;params 携带原始输入(customer_name、count、days_since、interval_days)。未完成的计数行显示实时数量。stake 为 {open_deal, class_rank};行按重要程度或到期日排序,从不按经过的时间排序。调用者看不到的客户,customer 为 null。任务关联了商机的行还会带 deal:{id,title}(否则为 null);只关联商机的任务取该商机的客户。housekeeping 为实时统计的 {deals_next_step, missing_details, doc_review}。suggestions 现在始终为 []:工作台“建议”区块已于 2026-10-12 下线,该字段仅为兼容旧客户端而保留(GET /crm/suggestions 不变)。reach_out_more 已弃用,恒为 0:自 2026-10-15 起,每位需要联系的客户都会有任务(不再限五位),工作台分页显示。 回复同步邮件的 reply 行还带有 email;当组织开启收件箱分拣时,email.triage 为 {topic, urgent, suspicious},即收件箱为该邮件显示的同一组答案(工作台据此分组):topic 为 inquiry、quotation、sample、purchase_order、invoice_payment、logistics、after_sales、supplier、promotions、job_application、notice 或 other,邮件尚未分拣时为 null;紧急度为 4 或 5 时 urgent 为 true;suspicious 为可疑答案。收件箱分拣关闭时省略 triage。
需要下一步的商机
GET /crm/deals/needs-next-step(会话)返回调用者本人未结束商机的 {items, by_stage}:不在赢单或输单阶段,客户为启用、已分配且非流失状态,下一步为空,且没有未到期的等待日期。每项为 {deal_id, title, customer:{id,name}, stage, gap, proposal}。gap 在询价阶段为 {kind:"inquiry_missing", missing, known}(只列出没有当前需求事实的 product、quantity、timeline);需求确认阶段既无已发送报价、也无已寄出样品时为 {kind:"no_quote_or_sample"};其他阶段(包括自定义阶段)为 {kind:"no_rule"}。proposal 来自模板,不经 AI:{text_i18n, completion_condition, source},询价追问为 communication_logged,报价为 quotation_sent;需要本人填写时为 null。采用时通过 POST /tasks 以该完成条件创建任务。最多描述 100 项;by_stage 统计全部。
PUT /crm/opportunities/:id/waiting 接收 {until, reason}(未来的毫秒时间戳,最多 366 天后;理由非空且不超过 500 个字符),或 null 以清除。在该时间之前,商机不计入数量;GET /crm/engagements/:id 返回 waiting_until 和 waiting_reason。销售只能对本人客户的商机设置;经理和管理员可对本组织任何商机设置。未知、其他组织或不可见的商机返回 404 not_found;请求体错误返回 400 invalid_waiting_until、400 invalid_waiting_reason 或 400 invalid_body。迁移 0142_engagement_waiting_until.sql 新增这两列。
缺少必填信息的客户
GET /crm/customers/missing-details(会话)返回 {required, items}:调用者本人名下、至少缺少一项组织必填信息的启用客户,最多 200 位。每项为 {customer_id, name, missing, values, suggestions}。信息项为 contact_channel(有电话或邮箱的主要联系人)、country_market(国家或主要市场)、product_interest(产品,或已确认的意向或偏好事实)和 class(lead 以外的客户等级)。suggestions 只来自已有的待确认事实,形如 {value, source_label},source_label 为 file、note、ai 或 manual;在本人通过常规客户与联系人接口保存之前,不写入任何内容。组织不要求任何信息时,required 和 items 均为空。响应还带 class_assignable:组织的客户等级只有未定级(lead)和丢单时为 false,此时 class 不列入 required、也不计数,因为没有人能填写它。GET /crm/settings/tasks 返回同样的 class_assignable,供设置页说明原因;已保存的设置本身不变。
需要你处理的文件
GET /crm/workbench/files(会话;任何成员)返回 {items, truncated}:调用者本人“还有 N 份文件需要你处理”计数背后的文件,每份文件一项,按时间从早到晚,最多 500 项(truncated 表示列表被截断;计数本身从不截断)。列表与计数使用同一个判定条件,因此始终一致:未保存的文件、已归档但没有销售记录的采购单或发票、客户或业务已被删除的文件;新文件有一小时宽限期;文件计入客户当前负责人名下(没有客户时计入上传者),客户在公海或已归档、文件已删除时不计入。每项为 {id, cause, batch_id, item_id, index, file_kind, file_id, name, mime, created_at, saved_at, customer, customer_answer, deal_answer, lost}。cause 为 unsaved、blank_money 或 lost_association;最后一种带 lost:{kind, name, removed_at}。batch_id、item_id 和 index 指向该文件的核对页(/knowledge/file-review/<batch_id>?file=<index>),核对批次出现之前归档的文件为 null。此接口不写入任何内容:答案通过核对页自身的 POST /crm/document-review/batches/:id/items/:item_id/save 保存。没有待处理文件时返回 200 {items: [], truncated: false}。
打开与移出文件(STORY-189)。POST /crm/workbench/files/open 携带 {id}(列表中某一行的 id),返回“查看”要打开的核对页。已有核对页的文件原样返回该页:200 {state:"ready", batch_id, item_id}。核对批次出现之前归档的文件(doc: 或 link: 行)会按上传时的方式新建一页,沿用已保存的客户答案并保留文件原有的时间,因此计数既不减少也不重复;再次打开同一行返回同一页。客户文档的核对页由页面自身的 POST /crm/document-review/batches/:id/items/:item_id/read-now 开始读取,已有的读取结果会被复用。原件已无法读取(存储的文件内容缺失,或资料库文件对上传者本人已不存在)时返回 200 {state:"gone"},不会新建任何内容。POST /crm/workbench/files/retire 携带 {id} 将这类文件移出列表和计数,不改动任何答案,返回 200 {retired:true};该核对页被标记为已移出(迁移 0147_document_review_item_retired.sql)。原件仍可读取时返回 409 original_readable,无法检查时返回 503 original_unchecked。两个接口只作用于调用者本人的列表:其他成员或其他组织的行、或已处理的行返回 404 not_found;缺少 id 返回 400 invalid_request。
任务设置
GET /crm/settings/tasks(任何成员)返回 {config:{reply_within_hours, required_customer_details}, can_edit, updated_at}。首次保存前读取默认值:24 小时和 ["contact_channel","country_market","product_interest","class"],updated_at:null。PUT /crm/settings/tasks 接收同样的 config 字段,需要 pipeline 写权限,与活动标准相同(否则 403 forbidden)。不合规的值会被拒绝而不是被截断:400 invalid_reply_within_hours(1 到 168 的整数小时)、400 invalid_required_customer_details 或 400 invalid_body。空的信息列表会关闭缺少信息规则。被设为必填的信息项,也不再产生同一字段的 profile_gap_ 建议。
排期任务
GET /crm/settings/scheduled-tasks(会话;管理员,销售与 CRM 设置权限,销售人员返回 403 forbidden)返回 {config, can_edit, updated_at, classes:[{key, label}], today}。config 为 {holiday_greetings:{enabled, holidays:{<holiday>:{on, instruction}}, classes, china_only, days_ahead}, reminders:[{id, enabled, title, instructions, date, repeat, for, created_by, customers}]}:中国法定节假日(new_year、spring_festival、qingming、labour_day、dragon_boat、mid_autumn、national_day),days_ahead 为 3、5、8 或 14,repeat 为 once、week、month 或 year,for 为 me 或 every_rep,customers 为 {mode:"none"}、{mode:"all"} 或 {mode:"classes", classes}。classes 是组织的客户等级(不含丢单)。首次保存前读取默认值,updated_at:null;读取从不写入。PUT /crm/settings/scheduled-tasks 接收 {config, expected_updated_at?},返回同样的结构。不合规的值会被拒绝而不是被截断(400,附校验错误码,如 invalid_days_ahead 或 invalid_reminder_date);当 expected_updated_at(加载时的值;原先没有记录时为 null)与已保存的不一致时,返回 409 stale_settings 且不保存任何内容。未带 created_by 的提醒会记为保存者。巡检通过签名的内部 Service Binding 路由读取排期与每次运行的客户:GET /crm/internal/task-rules/scheduled-settings?org_id=<org> 和 GET /crm/internal/task-rules/scheduled-customers?org_id=<org>&classes=<csv|all>&china_only=0|1[&member=<email>],二者不对外公开:启用、已分配、未丢单且有负责人的客户,每位成员最多 200 位、最多 100 位成员,被截断时带 truncated。每次排期运行为每位销售人员只生成一个任务;工作台把它列在“已排期”下(kind:"scheduled",分区 coming_up,并带 scheduled:{rule, holiday, customer_count})。任务页使用会话路由 GET /agent/assistant/scheduled-tasks/:task_id(仅任务负责人;{task_id, rule, holiday, title, instruction, occurrence, due_at, status, items:[{customer_id, name, contact, email, class_key, status, draft}]})、POST /agent/assistant/scheduled-tasks/:task_id/items/:customer_id/draft(携带 {lang:"en"|"zh-CN", redraft?},立即为该客户起草)、PATCH /agent/assistant/scheduled-tasks/:task_id/items/:customer_id(携带 {subject, body})、POST /agent/assistant/scheduled-tasks/:task_id/items/:customer_id/sent(我已发送:在该客户上记录一次 email_sent 联系)和 .../skip,二者均返回 {item, task_closed}(每位客户都已发送或跳过时任务关闭),GET /agent/assistant/scheduled-tasks/:task_id/sender-projection 和 POST /agent/assistant/scheduled-tasks/:task_id/items/:customer_id/send(从 Atlas 发送,每位客户确认一次;见下文“定时任务”),以及无客户提醒使用的 POST /agent/assistant/scheduled-tasks/:task_id/done。POST /agent/assistant/scheduled-tasks/assess(管理员)携带 {title, instructions, customers},在保存提醒前返回 {needs_customers, advice, suggested};模型不可用时返回 needs_customers:false 和 advice:null。Atlas 绝不自动发送:销售人员点击“发送”(从 Atlas 发出,需确认),或用自己的邮箱发出后点“我已发送”。
计入的联系
活动标准的 email_received 开关打开时(未设置视为打开),收到的客户邮件计为联系:即有人通过 POST /crm/inbox/links/confirm 或 POST /crm/inbox/links/confirm-exact 关联到该客户的、最新的一封来自真人(非 automated、非 failed_check)的来信。该关联比客户的最近联系更新时,CRM 会前移最近联系,并通知 Tasks 已记录一次沟通,从而以 closed_by event:communication_logged:email 关闭该客户未完成的久未联系任务。POST /crm/customers/:id/timeline 现在还会返回新记录的 id,即 201 {ok:true, id};记录的联系会把类型告知 Tasks,因此由它关闭的任务显示为例如 event:communication_logged:email_sent。
轮到谁(第 1b 版)
第 1b 版用“轮到谁”取代“需要下一步的商机”。每段客户往来要么轮到我们(客户在等我们),要么轮到客户(我们在等客户),由任一渠道上最近一次计入的联系决定:已关联到客户的来信或收到的消息,轮到我们;发出的邮件(包括“我已发送”)或发出的消息,轮到客户;通话、会议或备注按“下一步轮到谁”的回答。POST /crm/customers/:id/timeline 在 call、meeting 或 note 上接受可选的 next_move:"me"(客户在等我们)或 "customer"(我们在等客户);不填视为轮到客户。其他取值,或在其他类型上填写 next_move,返回 400 invalid_next_move,不写入任何内容。选择 "me" 时,记录仍会前移最近联系,但不会通知 Tasks 已记录沟通,因此由它产生的回复任务保持未完成。email_sent 可带 "scheduled": true,表示为排期任务(节日问候或管理员提醒)发出的邮件:它会前移最近联系,久未联系会计入它,但不计入“轮到谁”,也不会通知 Tasks 已记录沟通,因此未完成的“等待你回复”任务保持未完成。在其他类型上填写 scheduled,或取值不是布尔值,返回 400 invalid_scheduled,不写入任何内容。记录还可带可选的 thread_key,即它所回复的邮件会话(一个邮件会话就是一段往来):在某个邮件会话的回复任务上点“我已发送”时会带上它,Tasks 随后只关闭该会话的“等待你回复”任务,绝不关闭同一客户其他会话的任务。该值不存储;不带它的记录仍像以前一样关闭该客户的等待任务。取值不是长度不超过 200 个字符的非空字符串时,返回 400 invalid_thread_key,不写入任何内容。迁移 0144_timeline_next_move.sql 新增该列,GET /crm/customers/:id/timeline 在每条记录上返回 next_move(未填时为 null)。轮到我们且超过组织的回复时限时,产生“等待你回复”任务;只有轮到客户且超过等级间隔时,才会产生久未联系任务;从未联系、且有未结束商机或仍是潜在客户的客户,会在“主动联系”中产生“首次联系”任务。“需要下一步的商机”计数已停用:housekeeping.deals_next_step 恒为 0,数据源中不再有 kind 为 deals_next_step 的行;该字段和 GET /crm/deals/needs-next-step 为旧客户端保留。kind 为 reply 的行带 params.channel(email、message、call、meeting 或 note)和 params.trigger_contact {channel, at, from, subject, snippet};kind 为 first_contact 的行位于 reach_out。每个关联客户的行都带 deal:{id,title,stage}(任务自己的商机,或该客户进展最远的未结束商机)和 stage_gap_i18n,即进入下一阶段的一句话(例如“进入「评估」:发送报价或寄出样品。”),没有时为 null。用“新建任务”表单中“给客户发邮件”选项创建的任务(metadata.launcher 为 assistant_email)还带 request:{note, at},即此人写给 Atlas 的说明(最多 500 个字符)和任务创建时间(毫秒时间戳,或 null);其他行没有该字段。agent 通过签名的内部接口 GET /crm/internal/task-rules/our-move、GET /crm/internal/task-rules/contact 和 GET /crm/internal/task-rules/stage-gap 读取这些内容。
Agent 巡检计数(内部)
以下是供 agent 任务巡检使用的内部 Service Binding 接口,公开客户端无法调用。两者都对完整 URL 签名,组织只取自 URL:GET /crm/internal/task-rules/gone-quiet?org_id=<org> 和 GET /crm/internal/task-rules/counts?org_id=<org>,均可带可重复的 member=<email> 和 now=<epoch-ms>。久未联系返回 {members:[{member, customers, more}], truncated};每位客户为 {customer_id, name, class_key, interval_days, days_since, spell_at, has_open_deal}:距最近一次计入的联系的天数超过当前活动标准中该等级的间隔即为久未联系,按重要程度取前五位(先有未结束商机,再按 A 到 C 等级)。计数返回 {members:[{member, deals_next_step:{total, by_stage}, missing_details:{total}}], truncated, complete}。不带 member 时最多列出 50 位成员,被截断时设置 truncated;带 member 时每位指定成员都有一行,包括零,只有这种回答可以用来关闭任务。complete:false 表示某项计数失败,不关闭任何任务。错误:400 invalid_request、403 forbidden、503 authority_unavailable。任务键:trigger:gone-quiet:<customer>:<spell_at>、trigger:deals-next-step:<member>:<utc-day>、trigger:missing-details:<member>:<utc-day>。
Tasks 服务
GET /tasks 接受 closed_after=<epoch-ms>,返回在该时刻或之后关闭的任务(用于“已完成”)。POST /tasks/admin/tasks-v2-reset?dry_run=1&created_before=<epoch-ms>&cursor= 是仅限平台超级管理员会话、仅在预发布环境使用的一次性操作,在生产环境会被拒绝。它取消截止时间之前创建的、或由已停用触发器创建的未完成任务,每页 200 条,closed_by 为 reset:tasks-v2,返回 {dry_run, by_org:[{org_id, by_source, count}], remaining, cursor};以 dry_run=0 和返回的 cursor 重复调用,直到 remaining 为 0。由 agent 关闭的计数任务显示为 agent:task_triggers#count_zero、#superseded 或 #not_owed。
工作台全部回复
只读的 GET /agent/assistant/task-drafts/:task_id/reply-all 为旧客户端使用来源邮箱预览 {cc: string[]}。工作台对 POST /agent/assistant/task-drafts/:task_id/reply-all 发送精确的 {sender_connection_id},指定用户选择的发送邮箱。调用者的实时发送邮箱投影必须包含该连接且状态为 ready;不可用、停用、其他组织或 Atlas 备用地址返回 409 reply_all_unavailable,不返回收件人。无效请求体或多余字段返回 400 invalid_request。来源仍为调用者可见、正在回复的原邮件,草稿必须未发送且就绪,原发件人与草稿 To 一致。仅使用原邮件可见的 To/Cc,不读取密送。排除当前所选发送邮箱的地址及主要收件人,按区分本地部分大小写、不区分域名大小写的规则去重;其他来源地址仍保留。异步读取后重新校验所选邮箱状态及地址,并保留来源、负责人和草稿校验。预览不更改草稿;检查并用现有收件人 PATCH 快照保存后,再单独确认发送。来源或草稿变化返回 404 not_found 或 409 conflict。不会查询客户邮箱、推测别名或改用 Reply-To。
从收件箱发送新邮件
收件箱的新邮件窗口发送手写邮件,不创建工作台任务。GET /agent/assistant/compose/sender 返回当前登录者的发件人视图({state:"accounts", accounts, default_connection_id} 或 {state:"none"});无法读取时返回 503 sender_unavailable。POST /agent/assistant/compose/send 只接受 {request_id, to, cc?, bcc?, subject, body, sender_connection_id?, expected_from_address, override_reason?}:一个 to 地址、cc/bcc 地址列表、非空的单行主题和非空正文;其他情况返回 400 invalid_request。这两个路由属于收件箱:组织的 inbox 功能关闭时均返回 404 not_found。发送走与工作台草稿相同的受控路径——策略准入、对这些文字的发件人绑定、CRM 的持久发送命令——发出的副本会出现在“已发送”。策略准入会看到邮件的收件客户:每个 To、Cc 和 Bcc 地址都按联系人的邮箱精确匹配到本组织的客户,并对每个匹配到的客户各做一次准入(没有匹配时按无客户做一次),因此按客户类别设定的规则与该客户的工作台草稿完全一致;任何一次拒绝(例如 409 policy_blocked)都会拒绝发送。Atlas 无法确定这些客户时不会发送:503 recipients_unverified。只使用调用者有权发信的已连接邮箱:Atlas 备用地址、未就绪的邮箱、他人的邮箱,或与 expected_from_address 不同的地址,都会在发出前返回 409 approval_refused。同一 request_id 再次确认最多只发送一次,并返回第一次尝试的结果;已尝试发送后用同一 request_id 提交不同的文字,返回 409 request_reused,不会发送。只有 200({outbox, command_id})表示已发出;503 send_uncertain、409 send_in_flight 和 502 send_failed 表示应先查看“已发送”再重写。不会创建 Tasks 任务、客户时间线条目或今日草稿;该准备行不会通过 /prepared/:id 提供,CRM 也只以作者身份读取其信封。
您的邮件签名
每人一个纯文本签名,在“设置 → 个人”中设置。GET /agent/assistant/signature 返回当前登录者自己的 {signature, signature_zh}:signature 是英文(默认)签名,signature_zh 是中文签名(未设置时各为 null)。PUT /agent/assistant/signature 接受 {signature?: string | null, signature_zh?: string | null},两者至少提供一个;未提供的字段保持不变,任一字段无效则全部不修改。换行统一处理,去掉行尾空格和首尾空行,空文本或 null 表示删除该签名;返回保存后的 {signature, signature_zh}。签名最多 600 个字符、12 行;超出或请求体既无 signature 也无 signature_zh 时返回 400 invalid_request;无法保存时返回 503 signature_unavailable。组织和人员只取自会话,任何人都无法读取或修改他人的签名。设置签名后,AI 回复草稿以该签名原样结尾(中文草稿使用 signature_zh,未设置中文签名时回退到 signature;英文草稿始终使用 signature),替代 Atlas 自带的 Best Regards, / 此致敬礼 落款(模型自己写的结束语会被去掉);通过 POST /agent/assistant/task-drafts/:task_id/sender 更换发件邮箱不会改动签名;收件箱的新邮件窗口会预先带上与界面语言对应的签名,回退规则相同。未设置时,草稿保留固定落款和发件人姓名。
确认绑定原始邮件的回复收件人
GET /crm/inbox/items/:id/reply-source 在收件箱功能、当前 Auth 成员角色、邮箱 read_review、客户归属和现有禁止起草策略下读取一封原始入站邮件。私有且不缓存的结果包含 source(item_id、connection_id、content_hash)、from、to 和 review_required,不包含原始邮件、存储路径或密送信息。没有 Reply-To 时使用 From;同域内不同的单一有效 Reply-To 必须明确确认。跨域、重复、无效、含糊或不可用的邮件头均拒绝。无权读取或不存在时返回 404 item_not_found;无效或变更时返回 409 source_mime_invalid、409 source_changed 或 409 source_policy。现有策略无法支持时,请使用外部邮件客户端。
GET /agent/assistant/task-drafts/:task_id/reply-target 返回精确未发送草稿的 {target, revision}。POST /agent/assistant/task-drafts/:task_id/reply-target 仅接受 {source, expected_revision},由服务器推导收件人;任意 To 字段返回 400 invalid_request。确认“使用 Reply-To 地址”前会显示原始发件人和回复地址,发送确认仍单独进行。过期确认返回 409 conflict 或 409 reply_source_changed;缺少确认返回 409 reply_review_required。全部回复使用已确认的主收件人。本地部分区分大小写,域名不区分大小写,不推测别名。
回复审批绑定 v4 的摘要固定 reply_source,与出站身份的 reply_to 和转发的 forward_source 分开。审批、重试、发送及实时审批归属查询均重新检查来源、客户归属和当前权限。签名请求携带已验证的操作者,仅作身份归属。只有已验证邮箱的 CRM 命令发送支持 v4,并在 DATA 前再次检查;Atlas 代发和旧 SMTP 路径拒绝 v4。v2/v3 规范字节及发送行为不变。Auth 邮箱权限响应新增当前成员角色;回复、转发和附件原始邮件读取在缺少角色或仅为支持身份时拒绝。应先部署 Auth 和兼容的 Agent 审批归属路由,再部署 CRM。不新增发送权限范围。
现有单一编辑器仍允许客户关联回复显式修改收件人。修改 To 的 PATCH /agent/assistant/task-drafts/:task_id 包含 {to, expected_reply_revision},使用已保存且有权读取草稿的 draft.reply_review.revision。服务器记录精确手动 To、操作人、客户及原始邮件证据;客户端不能提交手动模式标志。draft.reply_review.manual_to 表示这项独立选择;原始 From、Reply-To 及邮件头确认状态不变。仅修改正文的保存不提交 To,不能确认 Reply-To 或生成手动选择。缺少证据或版本返回 409 reply_review_required,旧版本返回 409 conflict。原文变化、读取或客户负责人权限撤销及现有来源策略仍会拒绝。审批、重试及 DATA 前最终所有者查询会将实际收件人和选择与最新自有 Agent 草稿核对;v4 已绑定实际 To 与来源,v2/v3 字节不变。未关联回复不能使用手动选择。同一来源的文字修改和重新起草保留显式选择,来源变化使其失效。显式“使用 Reply-To 地址”或“使用原始发件人”清除手动选择并恢复来源收件人。手动主收件人不同时,自动全部回复不可用并返回 409 reply_all_manual_recipient;恢复来源主收件人后可重新使用。不推测别名,不扩大发送权限。
GET /internal/mail-reply-task?org_id=&task_id= 是签名且按组织隔离的 Tasks 服务绑定查询,用于当前开放回复任务的负责人和规范入站邮件身份。所有绑定来源的 v4 回复必须有已知自有任务桥接、精确任务来源及实际可空客户快照,并重新核对 Tasks 与 Agent 选择。元数据不能覆盖矛盾的规范来源;c:customer 回退与实际可空客户字段分别绑定。关联任务缺少账户字段不授予账户权限,CRM 独立核对来源邮箱。首次收件人解析失败即终止发送或重试,不能跳过策略检查或修改审批。先部署 Tasks 查询再部署 Agent;缺少查询时安全拒绝。普通 v2 非邮件或外联保持不变。
手动转发草稿
会话调用者通过 POST /agent/assistant/forward-tasks,提交 {source_item_id,request_id},从当前有权读取的邮件创建独立、私有且指派给自己的手动转发任务;已回复邮件也可转发。新意图使用新的 UUID request_id;重试同一次创建保留该身份。重试恢复同一任务;同一身份改用另一原邮件返回 409 conflict。响应为 {task_id,item,draft}:创建时 201,恢复时 200。不调用模型,任务标题和元数据不包含原邮件内容。
保存的草稿使用 basis:manual_forward、空 paragraphs、既有 to、cc、bcc、subject,以及 forward:{source:{item_id,connection_id,content_hash},body,attachments,available_attachments}。初始 To 为空,Cc/Bcc 为空,默认选中所有支持的原附件。完整纯文本引用包含 From/To/Cc/主题/日期/正文,不投影结构化 Bcc。附件元数据为 {id,version,content_hash,part_id,filename,size_bytes},文件字节和存储位置不离开 CRM。available_attachments 保留同一代的不可变清单,移除后仍可重新选择。
既有 PATCH /agent/assistant/task-drafts/:task_id 在转发模式接受 to、body、attachments:[{id,version,content_hash}],以及主题/Cc/Bcc。每次编辑必须携带 expected_forward:{to,subject,body,cc,bcc,attachments},精确匹配先前完整保存状态。过期编辑返回 409 conflict,保留保存的草稿。编辑可保存空 To;发送需要一个裸邮箱地址。正文精确保留,不去除首尾空白,不采用回复段落限制。原文/正文超过 1,000,000 个 UTF-8 字节,或草稿/合并审批载荷超过 1,500,000 个序列化字节时返回 409 forward_too_large,不截断。
POST /agent/assistant/task-drafts/:task_id/send 在单独确认前必须携带完整精确的 expected:{to,subject,body,from_address,cc,bcc,attachments,source},包括空数组。转发只使用已连接发送邮箱及绑定原邮件身份的 v3 审批;既有回复/主动联系的 v2 规范字节不变。原邮件读取权限与目标邮箱发送权限独立,在 SMTP DATA 前再次检查,无附件时亦然。原邮件代次或安全策略变化则拒绝,不自动替换或重试。尝试发送后锁定草稿,沿用既有 prepared 显式重试路由。转发不携带原邮件 In-Reply-To/References,不记录客户联系/时间线,也不完成原回复,只完成自己的任务并保留未关联客户的 Atlas Sent 副本。重新生成、发件人重新签名及“我已发送”返回 409 forward_manual_only。缺失或无权读取返回 404 not_found;原邮件安全策略返回 409 source_policy 和 no_draft。GET /internal/mail-forward-task?org_id=&task_id= 是签名 Tasks Service Binding 权限查询,不是公共客户端接口。
工作台抄送与密送
现有 PATCH /agent/assistant/task-drafts/:task_id 支持 cc 和 bcc 数组,每栏最多 50 个不含显示名称的 ASCII 邮箱地址。修改收件人时必须提交与已保存列表一致的 expected_recipients: {cc, bcc};旧版本返回 409 conflict,无效地址或缺少预期值返回 400 invalid_request。沿用草稿归属与来源读取权限。刷新或重新起草后保留已保存的收件人,不新增草稿存储。
发送确认通过 expected.cc 和 expected.bcc 提交当前显示的确切列表。有附加收件人的草稿如果缺少预期列表或列表已变化,会在发送前返回 409 draft_changed。批准与重试同时比对内容和全部收件人;密送地址不出现在收件人可见的 MIME 或已发送副本头部。Atlas 备用发送与旧版 SMTP 会在提交前返回 approval_required/recipient_transport_unavailable,不会忽略抄送或密送;请选择已连接的发送邮箱或移除附加收件人。此候选版本依赖已批准收件人传输的部署;从原邮件生成的回复全部、转发及附件选择属于后续流程。
Agent 草稿与立即运行
POST /agent/assistant/task-drafts 携带 {task_id}(会话;任务负责人)以幂等方式创建或返回该任务的工作项和草稿:{item, draft},开始起草前 draft 为 null。GET /agent/assistant/task-drafts/:task_id 读取;PATCH /agent/assistant/task-drafts/:task_id 携带 {subject?, paragraphs?, to?} 保存修改(to 须为单个邮箱地址,否则 400 invalid_recipient;未关联客户的回复只允许原邮件发件人,否则 409 recipient_locked);POST /agent/assistant/task-drafts/:task_id/redraft 重新起草。草稿为 {to, subject, paragraphs:[{text, cite?}], basis, status, error?};basis 为 thread 或 plain_checkin,status 为 drafting、ready 或 failed,cite 指向 Atlas 实际读取过的商机、时间线记录、报价、订单或档案。起草经由受治理的 AI 网关,并归属于打开任务的人。POST /agent/assistant/task-drafts/:task_id/sent(我已发送,适用于用自己的邮件客户端回复的情况)在客户上记录一次 email_sent 联系,返回 {timeline_id, task_closed}。首次联系、久未联系和下一步任务的草稿发给客户下有邮箱的联系人(优先主联系人),并以其姓名称呼。若没有任何联系人有邮箱,Atlas 不起草,也不调用模型:草稿为 ready,带 needs_contact: true,to 和 paragraphs 为空;PATCH 和 send 返回 409 needs_contact,sent 返回 409 no_recipient(这类草稿没有收件人时一律如此)。先用 POST /crm/contacts 添加联系人,再调用 redraft。GET /agent/assistant/task-drafts/:task_id/sender-projection 说明由谁发出:{state:"none", from_address?}(Atlas 地址;没有地址表示 Atlas 无法发送)或 {state:"accounts", accounts:[{state, connection_id, kind, from_address?}], default_connection_id}。POST /agent/assistant/task-drafts/:task_id/send 携带 {sender_connection_id?, override_reason?, expected?:{subject, body, to, from_address?}} 从 Atlas 发出就绪草稿(若提供 expected 且与服务端内容不同,则返回 409 draft_changed 或 approval_refused 并以 detail: sender_changed 说明,不会发送任何内容):屏幕上的主题、正文和收件人由调用者在同一请求中确认,经对外沟通准入后,通过已连接邮箱(或 Atlas 地址)发出并带回复线索;sent 时返回 {item, draft, task_closed, policy?, override?},其中 draft.send 为 {prepared_id, outcome, command_id?, outbox_id?, at},记录一条 email_sent 联系并由 Tasks 关闭任务。其他结果沿用 prepared-work 发送词汇(send_uncertain 503、send_failed 502、send_in_flight 409、approval_refused 409 并以 detail 说明发信拒绝原因、rate_limited 429)并附带 draft;一旦尝试过发送,草稿即只读(PATCH、redraft、sent 和再次 send 均返回 409 send_attempted 并附 prepared_id),唯一的后续途径是 POST /agent/prepared/:prepared_id/retry-send。Atlas 绝不自动重试。经已连接邮箱成功发出的邮件还会作为该会话的发出项保存在收件箱中(comm_items,方向 outbound,并关联到客户);经 Atlas 地址回退发出的邮件没有邮箱,不保存副本。回复草稿还带有 asks:[{text, source|null, quote|null, covered_by, blank, status}](最多 6 项):所回复邮件中的每个请求或问题、回答它的段落序号,以及 status(covered 或 needs_you);记录中无法回答时,正文保留一个空位,例如 [交期:请确认](blank),该项在编辑补全前保持 needs_you(方括号内仍含"请确认"等字样,或仍以该空位的话题开头,都视为未填写),且只要仍有空位,send 就返回 409 placeholder_in_text。每个需附带的文件也算作一项,带 need(其在 needs_documents 中的序号),状态随该文件需求而定(标记为不需要时为 not_needed)。草稿以发送邮箱的显示名称署名(起草时为调用者的默认邮箱),没有时使用账户名称;发送邮箱变更时,POST /agent/assistant/task-drafts/:task_id/sender 携带 {sender_connection_id: string|null}(草稿所有者;须为调用者自己可用的发件邮箱,null 表示不使用邮箱)重新署名并返回 {item, draft}:只更改落款中的姓名一行,若落款已被人工修改则保持不变(404 unknown_sender,503 sender_unavailable)。当回复需要附带文件(客户索取或回复中承诺的报价单、形式发票、规格书、证书、产品目录、发票或装箱单)时,草稿带有 needs_documents:[{kind, label, source:{kind, id, label, at?}|null, quote|null, status, placeholder, document?}](最多 3 项),正文中保留一个醒目的空位,例如 [attach quotation];Atlas 从不生成文件本身。PATCH /agent/assistant/task-drafts/:task_id/needs/:index(草稿所有者)携带 {status:"attached", document_ref:{kind:"customer_document"|"library_file"|"upload", id, name}, placeholder, kind}、{status:"not_needed", placeholder, kind} 或 {status:"open", placeholder, kind}(移除)处理一项需求,返回 {item, draft};placeholder 和 kind 是窗口显示时的需求,不再一致时返回 409 need_changed。附上文件后,只把对应空位替换为"the attached <label> (<文件名>)",文件名取自该文件自己的记录(经过清理);not_needed 会删去包含空位的那句话,open 恢复该需求改动过的内容。同类的两项需求使用不同的空位([attach quotation 2])。客户文件必须属于该任务的客户,资料库文件必须是调用者可读的,否则返回 403 document_forbidden 且不做任何改动。upload 指用户从电脑添加的附件,必须是该草稿自己的 uploads 之一(否则返回 409 upload_not_attached),文件名取自草稿。以此方式处理的需求在该文件仍在草稿上时不会阻止 send(邮件会带上该文件);从 uploads 中移除该文件后,需求重新打开,空位恢复到正文中。仍有未处理的需求时 send 返回 409 documents_unresolved;已附上文件时返回 409 attachments_unsupported,主题或正文中仍有 [attach …] 空位时返回 409 placeholder_in_text(从 Atlas 发送暂不支持附件:请复制草稿,用自己的邮箱发送)。send 请求体携带 send_without_document: true 表示用户已明确确认不带这些文件发送,并在同一次调用中完成:它只豁免 documents_unresolved 和 attachments_unsupported(邮件发出时不带文件),并删去包含每个未处理需求自身 [attach …] 空位的那句话,删法与 not_needed 相同(同一句中还含有其他需求空位时保留该句)。需求保持原状态,其名称记在本次发送的 draft.send.without_documents 上,草稿正文变为实际发出的文字。若提供 expected,它对应删句之前所保存的草稿。删句之后其他检查照常进行:仍有其他空位时返回 409 placeholder_in_text,draft_changed、发件人和策略拒绝均不变;发送被拒绝时草稿不做任何改动。不携带该字段,或为 false 或 null 时,上述拒绝不变;其他取值返回 400 invalid_request。sent 可携带 {documents?:{"<index>":"attached"|"not_needed"}} 回答仍未处理的需求,并在联系记录中逐项记下("Attached: Quotation")。POST /agent/assistant/reply-tasks 携带 {comm_item_id}(会话)为该邮件所在邮件串创建、找到或重新打开唯一的回复任务——无论是否已关联客户(STORY-194)——返回 {task_id, created, reopened}(新建时 201)。该邮件已被回复时——由本任务或巡检任务从 Atlas 发出,或该邮件串已显示为已回复——已回复的任务保持关闭作为记录,并像 Gmail 一样再发起一次回复:新建任务(201,created: true),其草稿与普通回复一样经由受治理的 AI 网关起草(basis 为 thread 或 thread_only,status 先为 drafting 后为 ready),收件人、主题和回复头与普通回复相同。模型会被告知该邮件串已回复,并读取我们之前发出的回复,因此起草的是对该回复的跟进,而不是再次回答同一封邮件;记录中没有的信息留作空白,由本人在发送前补全。redraft 重新起草跟进。此次变更前保存的草稿可能仍为 basis manual_reply(由本人撰写,起初只有其签名):仍可读取和发送,正文除签名外未写内容时 send 返回 409 draft_empty,redraft 会将其替换为 AI 起草的跟进。发送后任务关闭;该邮件串有更新的来信等待回复时,以 superseded 关闭(该来信另有自己的草拟任务)。后续回复任务未发送即关闭后再次调用时,重新打开最新的那一个并重新生成草稿(200,reopened: true),不另建任务。调用者必须能阅读该邮件串的邮箱(否则 404 not_found,与其他组织的邮件相同)。已关联的邮件串交给客户负责人(前提是其能阅读该邮箱;其他人得到 403 not_owner 并附 owner_email);未关联的邮件串,或负责人无法阅读该邮箱时,交给调用者。每个邮件串同时只有一个未完成的回复任务:已由他人持有时返回 409 assigned_elsewhere 并附 assignee_email;自动邮件或未验证发件人返回 409 not_person;400 invalid_request;503 crm_unavailable 或 tasks_unavailable。巡检会为 Atlas 判断需要回复的每个邮件串自动创建同一个任务(键 reply:<thread_key>:comm-<最新来信 id>),交给客户负责人(其能阅读该邮箱时),否则交给邮箱所有者;回复时限只用于标记逾期。GET /agent/assistant/reply-tasks/:task_id/assignees(会话;能阅读该邮件串的人)返回 {assignees:[{email}]},即能阅读该邮箱的人;任务不是邮件串回复任务或调用者无法阅读时返回 404 not_found。POST /agent/assistant/reply-tasks/:task_id/assign 携带 {assignee_email}(会话;仅当前负责人)把未完成的任务交给同事,返回 {task_id, assignee_email};400 invalid_request、403 not_assignee、404 not_found,以及 409 task_closed、cannot_read_mailbox(对方无法阅读该邮箱)或 assignee_outside_org。完整读取显示邮件串已被回复(之后有发出的邮件或“标记为已回复”)时,任务以关闭原因 answered 关闭;读取缺失或不完整时不会关闭任何任务。未关联客户的邮件串,草稿只依据该邮件串,价格、交期和数量留作 […: please confirm] 空位;发件人验证失败、回复地址不同、付款信息变更或被标记可疑时,任务照常创建但不带草稿,no_draft 为 sender_check_failed、reply_to_mismatch、payment_change 或 suspicious。这些路由背后的 CRM 与 Tasks 签名读取是内部 Service Binding 接口,不对外公开。POST /agent/assistant/task-triggers/run(所有者或管理员;本组织;生产环境拒绝)立即为调用者的组织运行任务巡检;连续运行两次不会产生新任务。POST /agent/admin/work-items-v2-reset?dry_run=1&created_before=<epoch-ms>&cursor= 是仅限平台超级管理员会话、仅在预发布环境使用的一次性操作,在生产环境或未设置环境时会被拒绝。它放弃截止时间之前创建、且没有任务或来源不是 v2 触发任务的未完成工作项(v2 触发任务的工作项会保留,并随任务一起关闭),每页 200 条,status_reason 为 reset for tasks v2,返回 {dry_run, by_org:[{org_id, by_type, count}], remaining, cursor};以 dry_run=0 和返回的 cursor 重复调用,直到 remaining 为 0。
定时任务(节日问候与提醒)
管理员在“设置 › 客户管理 › 定时任务”中设置日期。每个组织每天一次(同一天仅在设置变更后再次运行),agent 的扫描通过 Tasks 支柱为每位销售每次运行创建一个任务,键为 trigger:scheduled:<rule>:<occurrence>:<member>(<rule> 为 holiday.<id> 或 reminder.<id>),completion_condition 为 rep_confirms,metadata.scheduled = {rule, holiday, reminder_id, occurrence, customer_count, instruction}。节日任务在节日前 days_ahead 天出现,前一天到期;提醒在日期前 7 天出现,当天到期。客户读取失败或被截断时,不为可能被截断的成员创建任务,下一次运行重试;扫描从不关闭任何任务。Atlas 从不自动发送任何邮件。
以下为会话路由,仅限任务的负责人(他人的任务或没有定时任务键的任务返回 404 not_found)。GET /agent/assistant/scheduled-tasks/:task_id?lang=en|zh-CN 返回 {task_id, rule, holiday, title, instruction, occurrence, due_at, status, items:[{customer_id, name, contact, email, class_key, status, draft, send?}]};条目 status 为 pending、drafting、ready、failed、sent 或 skipped,draft 为 {subject, body, language, why} 或 null。POST /agent/assistant/scheduled-tasks/:task_id/items/:customer_id/draft 携带 {lang, redraft?},通过受治理的 AI 网关为该客户起草邮件(用量归属调用者)并返回该条目(已就绪的草稿原样返回,除非 redraft 为 true;模型失败时条目为 failed)。管理员的指令作为不可信内容,排在 Atlas 固定规则之后交给模型(使用联系人的语言;最多引用一条客户记录中的真实信息;不编造事实、价格、日期、折扣或承诺;销售姓名只出现在落款)。PATCH /agent/assistant/scheduled-tasks/:task_id/items/:customer_id 携带 {subject, body} 保存销售的修改(409 drafting、not_editable 或 send_attempted)。GET /agent/assistant/scheduled-tasks/:task_id/sender-projection 返回调用者的发件人选择,结构与任务草稿的发件人投影相同({state:"none", from_address?} 或 {state:"accounts", accounts, default_connection_id};503 sender_unavailable)。POST /agent/assistant/scheduled-tasks/:task_id/items/:customer_id/send 携带 {sender_connection_id?, override_reason?, expected?:{subject, body, to, from_address?}}(“发送”,在对话框中确认),通过与任务草稿相同的审批 → 发送流程,从销售的邮箱(或 Atlas 地址)把该客户的草稿发到条目上显示的地址。条目的 email 和 contact 实时读取:优先为普通外联所用的联系人(有邮箱的主要联系人,否则最早的联系人),否则为公司自身邮箱(不署名),否则为 null(此时“发送”返回 409 no_recipient);草稿称呼该联系人。仅当任务未关闭、条目已就绪且有草稿、未发送、未跳过、未尝试过发送且邮箱有效时才会发出。拒绝:409 draft_not_ready、already_sent、skipped、send_attempted、no_recipient、recipient_changed(草稿是为另一位收件人写的,请重新起草)、no_subject、draft_empty、placeholder_in_text、draft_changed(条目内容已不是窗口显示的内容)、sending(另一次点击或另一个标签页正在发送)或 task_closed,均附带当前 item;403 customer_unreadable(运行后该客户已改派给他人);400 invalid_request;503 send_unavailable;以及发送方自身的拒绝,如 409 approval_refused。结果为 sent 时返回 {item, task_closed, policy?, override?},item.status 为 sent,item.send = {prepared_id, outcome, command_id?, outbox_id?, at}。其他结果返回发送方的错误 JSON(error、detail、command_id)并附带记录了 send 的 item:草稿随即锁定(PATCH 和重新起草返回 409 send_attempted 及 prepared_id),直到通过 POST /agent/prepared/:prepared_id/retry-send 明确重试;重试成功后,下一次 GET 显示该条目为 sent。“跳过”和“我已发送”仍然可用。从 Atlas 发出的问候与“我已发送”记录方式完全相同:一条标记为 scheduled 的 email_sent 时间线记录,最近联系时间会更新,且不会关闭该客户未完成的回复或跟进任务。POST /agent/assistant/scheduled-tasks/:task_id/items/:customer_id/sent(“我已发送”,从销售自己的邮箱发出)通过 POST /crm/customers/:id/timeline 记录一条 email_sent 联系并返回 {item, task_closed};重复点击不会重复记录。POST /agent/assistant/scheduled-tasks/:task_id/items/:customer_id/skip 返回相同结构。所有客户均已发送或跳过时任务关闭(closed_by 为 agent:scheduled_tasks)。POST /agent/assistant/scheduled-tasks/:task_id/done 关闭没有客户的提醒并返回 {task_closed:true}(否则 409 has_customers)。拒绝:409 task_closed 或 conflict,503 crm_unavailable 或 tasks_unavailable。POST /agent/assistant/scheduled-tasks/assess(管理员或所有者)携带 {title, instructions, customers, lang?},返回 {needs_customers, advice, suggested}:当指令要求 Atlas 给客户写信但 customers.mode 为 none 时,needs_customers 为 true,并附简短建议和建议的客户选择;任何 AI 故障时返回 {needs_customers:false, advice:null, suggested:null},从不阻止保存。POST /agent/assistant/task-triggers/run 也会运行此扫描,并接受 {today?:"YYYY-MM-DD"} 模拟某一天(生产环境除外);其返回包含 scheduled。
限定范围的自动化测试会话
限定范围的自动化测试会话: custom broker, activation fields, scope and rollback / 自定义代理、启用字段、范围和回退。
切换已登录的账号
打开组织菜单 → 账号,选择在本浏览器验证登录过的账号。账号列表与组织成员身份分开,最多保留四个账号;添加更多账号时替换最早保存的账号。添加账号仍须经过原有邮箱验证码或 Google 登录。继续前可取消操作、处理未发送内容,或明确丢弃后继续。切换会关闭其他 Atlas 标签页的当前工作区,停止旧请求和数据流,清除本地草稿与工作区状态,再打开中立入口;已保存的会话及账号专属偏好会保留,只能通过当前核验身份读取。不会自动发送任何内容。更换账号需要 HTTPS 和浏览器 Web Locks 支持。
GET /auth/accounts 是以 Cookie 验证身份的只读起点,返回 {items:[{id,email,name,orgId,current}]}。列表只包含本浏览器持有、尚未过期或撤销且仍有权限的会话;没有账号时返回 200 {items:[]}。JSON 不返回凭据。最多四个账号 Cookie 仅限当前主机,设有 HttpOnly、SameSite=Lax,HTTPS 下另设 Secure;账号令牌不会写入 JavaScript 存储。
| 操作 | 请求 | 确认后的结果 |
|---|---|---|
| 切换账号 | POST /auth/accounts/switch,提交当前列表中的 {accountId} | 严格核验实时角色、权限、成员资格及组织状态后启用已有会话,保留原到期时间。来源会话在账号库中原子替换,同一来源的并发切换不会同时成功。 |
| 添加账号 | POST /auth/accounts/add,提交 {} | 在账号库中保留当前账号,结束当前浏览器上下文,再进入原有 /login。取消或登录失败不会授予新身份。 |
| 退出当前账号 | POST /auth/accounts/logout,提交 {} | 撤销并移除当前浏览器持有的该账号,保留其他账号;可在登录页重新选择。 |
| 退出本浏览器全部账号 | POST /auth/accounts/logout-all,提交 {} | 撤销浏览器持有的全部会话并清除当前及已保存的账号 Cookie。此操作不同于 POST /auth/sessions/revoke-all,后者退出当前邮箱在所有设备上的会话。 |
写请求须提供 Content-Type: application/json,且 Origin 必须与公开站点完全一致;委托令牌、邮箱或角色声明不能用来冒充登录。若提供 x-atlas-account-context,其值为 JSON [email,orgId];过时标签页的上下文不匹配时直接拒绝。成功返回 {ok:true};失败包括 400 invalid_request、403 forbidden、403 account_unavailable、409 account_conflict 和 503 account_storage_unavailable。全部响应设为 Cache-Control: no-store。失败不会宣称切换成功,请重新打开登录页核验实际会话。邮箱验证码、Google 登录及原生客户端的 POST /auth/logout 流程仍然可用。测试会话不进入此账号库,只能使用单独的授权范围内选择器。
助手会话查看(候选版本,支持人员访问受启用策略控制)
在「Ask Atlas」中打开自己会话的查看诊断信息,再点击加载脱敏会话,按需查看可用的消息正文。系统不自动批量读取,也不提供会话内容搜索、批量导出或新的正文存储。本节说明候选源码行为,不代表已在预发布或生产环境上线。
GET /agent/assistant/threads/:id/diagnostics 返回 {diagnostics, policy, grants, audit, attribution, window}。GET /agent/assistant/threads/:id/review 向经过验证的会话负责人(包括普通成员)返回 {review, policy, attribution, window}。组织管理员身份不能用于查看其他成员的正文。服务端逐页并在响应前复核会话归属、租户及当前邮件来源权限。响应使用 Cache-Control: no-store。
会话视图仅包含允许展示的用户与助手正文、运行/序号/时间证据,以及限定类别的工具状态。不包含内部规划与思维链、工具输入输出、附件正文或导入的历史轮次。系统在拼接文本片段后,谨慎遮蔽响应中符合已知特征的秘密信息;脱敏内容仍属私人信息。该处理不保证识别每一项秘密,也不修改现有的私人会话原始记录。outcome: unknown 是有意保留的未知状态:end_turn 只说明回复结束,不证明用户目标已经达成。序号间隔可能来自预留区间,不能据此判断数据丢失。
读取复用现有会话日志,最多 10 页、每页 500 条事件,接口时间预算为 10 秒,来源数据上限为 2,000,000 字节。每条消息最多展示 32,000 个字符,脱敏投影视图上限为 256,000 字节;诊断响应另有 1,000,000 字节上限。工作量上限、缺失起始事件、不完整读取、时间窗外内容及未知历史留存均保留覆盖范围说明。来源读取或授权失败时,不返回已缓存的部分私人内容。
POST /agent/assistant/threads/:id/support-grants 要求浏览器同源请求、application/json、最多 2,048 字节的正文,以及准确字段 {reviewerEmail, expiresInMinutes, scope?, noticeAccepted:true}。查看人员邮箱须规范为小写;有效期为 5–1440 的整数分钟数。scope 默认为 metadata,也可明确选择 transcript;两种权限互不包含。负责人须勾选相应范围的告知说明,并确认指定人员及有效期。最多保留十项有效的分范围授权。DELETE /agent/assistant/threads/:id/support-grants?reviewerEmail=reviewer%40example.test&scope=transcript 仅撤销指定范围,策略关闭后仍可撤销。两种变更均返回 204;删除会话会移除授权及本地访问记录。
平台标签页面的支持人员会话查看,可查找已向当前支持人员授予有效、准确正文查看权限且已编入索引的会话。输入组织,可选填负责人邮箱、本地更新时间范围及已观察到的尝试状态,再点击查找获授权的会话。可点击「上一页」或「继续查找」,并选择查看会话;加载正文仍需另行明确操作。负责人自己的会话发现继续使用现有私人会话历史。
GET /agent/platform/assistant-reviews 必须提供 orgId;可选筛选项包括 ownerEmail、from 和 to(十进制毫秒时间戳,须在策略读取时间窗内,不能晚于当前时间)、outcome 及不透明的 cursor。索引按顺序分十个固定批次,每批最多考察十个候选项,一次有限范围查找最多考察 100 项。第 1–9 批始终返回固定大小的不透明继续令牌,即使没有获授权或已编入索引的记录也一样;仅第 10 批返回空游标。此固定填充规则防止从是否能继续查找推断无权查看的索引是否存在。空批次既不能证明会话不存在,也不能证明存在无权访问的会话;达到查找上限不代表已列出全部会话。响应为 {items, nextCursor, coverage},每项仅有 {threadId, orgId, ownerEmail, updatedAt, outcome},覆盖范围为 coverage:{source:"retained_snapshot_index",complete:false}。列表不包含标题、正文、推断意图或全局总数。每位负责人最多保留 20 项快照;孤立的原始日志及已裁剪快照不在索引内。加密游标绑定查询范围,不泄露无权查看的候选信息。返回每个会话仍须具备当前准确正文授权、已启用的支持正文策略、当前超级管理员权限、无邮件衍生来源,以及已确认持久化的访问审计。仅在名单中的策略不授予列表权限。outcome 表示最近观察到的尝试状态(answered、failed、cancelled、interrupted、awaiting_tool、running 或 unknown),不能推断用户目标是否成功。无权读取的列表与获授权但为空的列表均返回 200 {items:[]}。
平台标签页面也接受准确的组织 ID、会话 ID 和负责人邮箱,不会列出所有用户的会话。GET /agent/platform/assistant-diagnostics?orgId=ORG&threadId=THREAD&ownerEmail=OWNER 查看元数据;GET /agent/platform/assistant-review?orgId=ORG&threadId=THREAD&ownerEmail=OWNER 查看脱敏正文。两者均要求当前有效的超级管理员身份、对应范围内未过期的准确负责人授权、已获批准并启用的策略,以及在任何内容返回前得到持久化确认的 Governance 访问审计。支持人员访问一律拒绝邮件衍生来源。会话标签的查看权限本身不授予这两个接口的权限。
ASSISTANT_DIAGNOSTICS_POLICY 默认值为 windowDays:30、supportReviewEnabled:false、transcriptReviewEnabled:false、purgeEnabled:false。读取时间窗允许 1–90 天,仅限制可见范围,不删除历史数据,不启用按期物理清理或历史删除。启用元数据访问需要 supportApprovalReference;启用正文访问另需 transcriptApprovalReference,且须同时启用元数据策略。配置不等于批准:没有另行批准的具体启用范围,不得打开支持人员访问。指定获授权超级管理员策略是另一种可另行约定的方案,并非已实现的全局正文权限。此实现不启用历史清理、回填、真实内容分类或导出。
错误包括用于无法访问的私人目标或权限的统一 404 not_found,以及 400 invalid_request、403 same_origin_required、409 support_review_disabled、503 audit_unavailable 或 503 unavailable。无法查看时,请核实准确目标及当前授权,不应扩大租户范围或绕过审计。启用验收仅使用明确获授权的合成会话。
保留反馈与来源隐私
POST /knowledge/sandbox/ask 接受可选字符串 scope_label 和 scope_descriptor。提问和评分须发送相同的范围文字,先去除首尾空白,再截取至最多 200 个字符。完成的答案可能包含不透明的 feedback_proof_id。请将其与该答案一起保留,并在 POST /knowledge/feedback 中原样发送,同时保持问题、答案、来源和范围一致。不得用于其他答案、范围、用户或组织。服务器核验访问权限;凭证不授予访问权限。
提供无效凭证时,接口返回 400 invalid_feedback_proof,不创建反馈。旧答案缺少凭证、旧评分请求不含凭证时仍可提交。来源访问未经证明的反馈会保留,供现有 Knowledge 审核人员查看,但对其他读者隐藏。删除来源或使用相同 ID 重新上传,不会扩大早期反馈的可见范围。有权限的读者仍可查看记录的答案及来源名称;已删除的来源标记为 available:false。
合并客户
需要当前客户访问权限。读取与审阅使用 crm:read;提交、继续及取消使用 crm:write,仍须通过 CRM 的当前角色和客户权限检查。组织和操作人来自已验证的凭据,输入不能包含 org_id。此说明对应本地候选功能,不代表已在生产环境开放;完整流程验收前不得启用。
- 选取 2–10 个客户,先提交
customer_ids和可选的survivor_id进行只读审阅。 - 按返回的
selection修改选择,再次审阅完整选择。查看每个值的来源、时间、冲突、关联记录及负责人影响。latest不会代替必须明确作出的决定;时间相同、主联系人、计划及缺失决定等阻碍须先解决。choices和custom_field_sources的值是来源客户 ID。 - 向操作人展示最终名称、保留的原始资料数量、负责人及访问变化,取得明确确认。发送前持久保存唯一的
request_id、完整选择、preview_digest和expires_at。提交时加上confirmed:true;不能用通用--yes跳过审阅。 - 持续查看同一操作;只有
state:complete表示已发布。pending仍在处理,blocked需要按阻碍处理。响应丢失时用同一request_id查询,不能另建请求;继续操作只用已有operation_id,不传替代选择。
审阅过期或数据变化后须重新审阅,不能绕过 preview_digest。只有进入 apply 前才可请求取消;以 state:cancelled 为准,取消不是撤销。已开始应用的操作应继续恢复。完成后从合并历史查看保留的关联原始资料;原始资料只读,内容仍按当前访问权限过滤,无自动 Undo。
| 方法与路径 | 输入或结果 |
|---|---|
POST /crm/customers/merge/preview | customer_ids, survivor_id? | CustomerMergeSelection |
POST /crm/customers/merge | CustomerMergePublicCommit |
GET /crm/customers/merge/requests/:request_id | CustomerMergePublicOperation |
GET /crm/customers/merge/operations/:id | CustomerMergePublicOperation |
POST /crm/customers/merge/operations/:id/resume | |
POST /crm/customers/merge/operations/:id/cancel | |
GET /crm/customers/:id/merge-history | CustomerMergeHistory |
GET /crm/customers/merge/operations/:id/originals/:source_id | CustomerMergeOriginal |
完整选择示例(需按返回的真实客户、联系人及字段来源修改;并非可直接提交的已确认操作):
{
"customer_ids": ["CUSTOMER_A", "CUSTOMER_B"],
"survivor_id": "CUSTOMER_A",
"mode": "latest",
"choices": {},
"owner_email": "owner@example.com",
"primary_contact_id": null,
"custom_field_sources": {},
"plan_customer_id": null
}所有选择字段均必填。primary_contact_id 和 plan_customer_id 只在审阅允许时为 null。commit 在完整选择上增加 request_id、64 位小写十六进制 preview_digest、原样保留的 expires_at(毫秒)及 confirmed:true。初次提交返回 202 和完整操作对象。resume/cancel 不接受替代计划。操作结果包含 operation_id、request_id、state、phase、survivor_id、blockers、retryable 和脱敏的 receipts,不含私有快照。原始资料返回 read_only:true,仅含按当前权限过滤的显示内容。
内部 Service Binding 协议(仅维护人员) 内部 Studio 任务回执 Studio 调度与共享命令状态 员工配置草稿 同草稿评审提案 内部员工私有 Memory 绑定
遇到 409 merge_blocked、preview_stale、preview_expired 或请求查询返回 404 时,仍须保留同一 request_id;并发提交可能已经获准执行。重新审阅并明确确认后,继续使用同一编号重试。遇到 idempotency_conflict 时,查询同一编号并恢复已有操作,不能替换已保存的计划。只有确认 state:complete 或 state:cancelled 后才可结束使用该编号。
内部客户合并协议
以下接口仅供经过签名验证的 Worker Service Binding 调用,不是公开 REST 接口。浏览器、MCP、CLI 及手动 HTTP 调用方不能直接调用;请使用公开客户合并流程。请求以 internalAuthHeadersWithBody 签名,接收方在解析及访问数据前以 verifyInternalRequestWithBody 验证完整请求体。内部 org_id 只能来自已验证的调用者上下文,不能让公开输入决定租户。
准确字段、联合类型、校验及摘要规则以源码 packages/atlas-core/src/customer-merge-protocol.ts 为准,以下仅为接口索引。P 是 CustomerMergePlan;I 是 CustomerMergeIdentity。参与者为 Tasks、Agent 及 EKB;CRM 拥有 admission、publication、subjects、opportunities;task-bindings 由 Tasks 拥有。
| 方法与内部路径 | 请求 → 响应 |
|---|---|
POST /internal/customer-merge/preview | CustomerMergeOwnerPreviewRequest → CustomerMergeOwnerPreview |
POST /internal/customer-merge/prepare | {protocol:1,phase:"prepare",plan:P,plan_digest} → CustomerMergeReceipt |
POST /internal/customer-merge/apply | {phase:"apply",identity:I} → CustomerMergeReceipt |
POST /internal/customer-merge/status | {phase:"status",identity:I} → CustomerMergeOwnerStatus |
POST /internal/customer-merge/release | {phase:"release",identity:I} → CustomerMergeReleaseResponse |
POST /internal/customer-merge/admission | {protocol:1,org_id,customer_ids} → {protocol:1,customers} |
POST /internal/customer-merge/publication | I → {identity:I,state} |
POST /internal/customer-merge/subjects | {protocol:1,org_id,customer_ids,subjects:[{kind,id}]} → {protocol:1,request_digest,revision,bindings:[{kind,id,customer_id}]} |
POST /internal/customer-merge/opportunities | {protocol:1,org_id,customer_ids} → {protocol:1,request_digest,revision,opportunity_ids} |
POST /internal/customer-merge/task-bindings | {protocol:1,org_id,task_ids} → {protocol:1,request_digest,revision,bindings} |
Preview 是只读检查,不预留操作。prepare 固定已确认的计划及每个所有者的 revision。apply/status/release 只接受原有身份,不接受替代计划。CustomerMergeReceipt 绑定所有者、完整身份、阶段、expected_revision、revision、counts、state,并包含 receipt_digest 和 receipt_mac;接收方同时核对实际 Service Binding 所有者,不能相信响应自报的所有者。状态只能返回已持久化回执。release 返回已发布后的 released 回执,或 CRM 已取消时的 CustomerMergeCancellationAck;没有独立的 cancel 阶段端点。取消回执使用 phase:"cancel",state:"cancelled",不伪造 prepare 回执。
admission 为每个客户返回 available、pending(带 operation_id)或 merged(另带 survivor_id)。publication 的状态为 preparing|prepared|applying|publish_ready|complete|cancelled;仅 CRM 的 complete 决定发布,所有者的 applied 回执不等于发布。无法核验发布时保持 pending,不泄露移动后的资料。
subjects 的 kind 为 customer|opportunity|comm_item;未知或跨组织主体使整个读取返回 404,只有已知但未关联的 comm_item 可以返回 customer_id:null。task-bindings 的每项为 {task_id,customer_id,related_customer_ids,source_id,merge_state},pending 另带 operation_id,merged 另带 operation_id、survivor_id,且 survivor_id 必须等于当前 customer_id。admission 和 task-bindings 的 ID 列表按顺序唯一排序,长度为 1–100;subjects 的 customer_ids 长度为 0–100,最多 100 个唯一主体。bindings 与请求逐项对应并绑定 request_digest、revision;缺失、乱序、摘要或身份不匹配一律拒绝,不能按 available 猜测。
CRM 的 opportunities 读取使用 CustomerMergeOpportunitiesRequest 和 CustomerMergeOpportunities:customer_ids 必须排序、唯一且包含 2–10 个客户;返回排序、唯一的 opportunity_ids,最多 2,000 项,并绑定 request_digest 与 revision。超过上限返回 409 snapshot_limit,不能截断后继续。
Tasks 的 related_customer_ids 是必填的排序唯一列表,长度为 0–2,包含当前直接客户及商机关联的全部客户。调用方须对其中每个客户检查 admission,不能只检查单个 customer_id。直接 customer_id 为 null 时,响应可用商机所属客户作为有效 customer_id;这不会改写存储的 null,也不会替换另一个独立客户关联。无关联时 effective customer_id 为 null,列表为空。关联读取失败或存在 pending 时,不得把任务当作可安全继续。
合并前核验当前成员角色
Auth 的现有内部 POST /internal/members/eligibility 同样只接受签名 Service Binding 请求。省略新选项时,{org,email} 仍只返回 {eligible}。明确发送 {org,email,include_role:true} 时返回 {eligible,role},其中 role 是当前解析的 rep|manager|admin|null;无有效资格时 role 为 null。CRM 在合并前重新读取当前成员及角色,不能依赖旧会话角色、客户端声明或缓存结果。该核验不以 salesCrm 设置作为准入门槛,也不授予额外客户或分配权限。
已预留任务清单
经过签名的 POST /internal/customer-merge/task-inventory 接受完整合并身份 I。Tasks 核验该操作已 prepared、applied 或 released,返回 {protocol:1,identity:I,task_ids}。任务 ID 仅来自不可变的预留快照,须排序、唯一且最多 2,000 项。未知身份、未预留及已取消的操作均被拒绝。这不是公开任务查询。Agent 在 Tasks 预留后,为这些任务 ID 建立自身预留,包括尚无 Agent 草稿的任务,避免迟到的草稿绕过合并。调用方以预期身份调用 parseCustomerMergeTaskInventory 校验。
审核已发布邮件中的选定业务事实
部署组合版 CRM 代码前,依次应用 0163_selected_email_facts.sql、0164_selected_email_analysis_admissions.sql、0165_selected_email_merge_fences.sql。变更结构前须核实实际迁移记录,并确认没有待完成的客户合并。新增写入围栏保留不可变审批回执和分析准入记录;不会重启结果未知的付费分析,也不改写已发布的0157迁移。
POST /crm/customers/:customerId/email-facts/analyze 仅接受 {comm_item_id,publication_id},返回 200 {written,items,skipped}。无需手工提供事实或正文,即可从选定的已发布邮件生成待审核建议。使用现有发布确认响应中的 publication.id。该操作复用受治理的提取流程,按当前组织模型策略、额度预留及操作者身份记录用量;只分析已发布正文中的直接陈述,最多 8,000 个字符。业务专用提示词限定输出范围,服务端再次核验原文引用、直接陈述及新增关系,并在提取后重查权限和原始来源、客户状态。不支持的建议被跳过,没有可用信息时返回空列表。模型或策略失败返回 503 extraction_unavailable,不回退或写入事实。结果始终待审核,须另外明确选择、预览并批准。
这些后端接口只处理一封已导入的收件邮件和一个明确选定的现有客户。登录用户必须仍有有效成员身份、crm:read、crm:write 以及该客户的访问权限。先通过现有 POST /crm/communications/:id/publish 分享确认流程,把选定证据发布到该客户。关联客户本身不授予邮箱权限。这些接口不会发布证据、创建权限或扫描邮件;只有明确调用 analyze 才会通过治理网关使用模型。已发布副本按当前 CRM 权限读取,邮箱权限随后撤销不影响该副本;移除发布后则拒绝使用。
POST /crm/customers/:customerId/email-facts/prepare 接受 {comm_item_id,publication_id,facts:[{key,value,category,citation}]},返回 200 {written,items},仅包含该发布副本的待审核事实。值必须出现在正文直接陈述的原文引用中。类别仅支持 intent、preference、other;键仅支持 main_markets、products、brands、requirements、shipping_address、purchase_intent、communication_preference、business_background。拒绝历史引用、转发、虚构及个人信息;已有确认值的键会跳过,原值保持不变。现有受治理的定时建议也复用这些待审核事实,但必须先通过当前任务授权和发布来源检查。
POST /crm/customers/:customerId/email-facts/preview 接受 {comm_item_id,publication_id,fact_ids},需明确选择 1–20 个不同 ID,返回 {customer_id,comm_item_id,publication_id,review_digest,facts}。每条事实包含 {id,key,value,category,citation,status}。预览不修改数据。POST /crm/customers/:customerId/email-facts/approve 接受相同选择以及 {review_digest,idempotency_key,approved:true},重新检查权限和来源、客户、事实状态,并在一个事务中提交全部选定新增事实及不可变回执。返回 200 {receipt,destination,replayed},其中 destination 是现有事实行的当前回读。回执字段为 {id,customer_id,publication_id,comm_item_id,fact_ids,review_digest,approved_by,approved_at}。重试时保持选择、摘要和键完全相同,即返回原回执。通用事实确认、编辑和批量确认不能绕过该流程。
拒绝结果:400 invalid_selection、invalid_business_evidence 或 explicit_approval_required;403 permission_refused;404 not_found、published_source_not_found 或 fact_not_found;409 stale_review、verified_value_exists 或 idempotency_conflict;503 receipt_unavailable。过期或并发编辑会拒绝整次审批。无法确认发布权限时拒绝访问。不覆盖或替代任何已确认值,不支持自动合并、历史批量处理或新建客户。类型见 packages/atlas-core/src/selected-email-facts.ts。部署 CRM 前须应用 0163_selected_email_facts.sql。源码文档不代表预发布或生产环境已上线。
对于旧版定时流程生成、值中带有 Email citation (comm_…) 的 AI 事实,通用确认、编辑及批量确认也会拒绝。缺少正式来源记录或发布已移除时,不能绕过选择审批。该措施只拒绝写入,不重写或删除历史数据;普通非邮件事实保留现有操作。
分析按操作者、发布副本、客户及已确认业务事实的精确上下文持久化执行准入。相同请求复用已完成工作,回读当前待审核行,并返回 written:0、replayed:true。来源、客户或上下文改变后使用独立准入。并发重复请求短暂等待;仍在运行或结果不确定时返回 409 analysis_in_progress。稍后重试相同请求可读取完成结果;超时或失败不会自动开启另一次付费执行。终止失败保持 503 extraction_unavailable。每次付费重试及路由回退都会在额度预留和提供商请求前重查来源、权限。待审核事实与准入完成状态在同一事务提交。部署 CRM 前须按上文顺序应用三项迁移。
GET /crm/customers/:customerId/email-facts/analysis-status?comm_item_id=…&publication_id=… 是该分析的只读状态查询。它重新计算当前确切上下文摘要,返回 200 {admission,context_changed}:admission 为 null,或为 {state,created_at,updated_at,fact_count,skipped_count},其中 state 为 running、completed 或 failed(fact_count/skipped_count 仅在完成后返回);当当前上下文没有准入记录、但同一操作者、客户与邮件存在较早记录时,context_changed 为 true。该读取不写入任何数据、不持有租约、不会准入或重启付费分析,也不会调用模型:打开、刷新或反复检查状态都不会产生新的花费。它使用与该接口族相同的会话权限,拒绝时返回 400 invalid_selection、403 permission_refused 或 404 published_source_not_found。用它区分仍在运行的分析与已确定失败的分析,区分已生成待批准建议的完成运行与未发现新事实的完成运行。
内部评估清理:POST /crm/eval/teardown 仅供非生产环境通过签名的 Service Binding 调用,公共网关禁止访问。它随其他评估数据清理可删除的邮件事实来源记录,但保留 customer_email_fact_receipts 和 customer_email_analysis_admissions。清单中这两张表的 removed:0 表示未删除,remaining 显示实际保留数量并计入总数。总数非零不能证明评估环境已清空。清理不会重置付费分析准入,也不会删除不可变回执。
内部 Studio 任务创建回执
此 Service Binding 接口不向公共 REST、CLI 或 MCP 客户端开放,也不启用 Studio 能力或新增任务生产者。
POST /tasks/internal/studio/review-receipt 接受规范的 StudioWaitOwnerBinding。组织和操作者仅取自已验证、绑定请求正文且有效期为 30 秒的主体断言:audience 为 tasks,surface 为 rest,action 为 agent.team.change,scope 为 tasks:write。Agent 签名前重新核验当前 Auth 权限和 Team 来源可见性;Tasks 另行核验当前任务可见性。仅有 Cookie 或内部 HMAC 均不足以访问;平台会话和定时执行主体也被拒绝。
技术存储开关 STUDIO_REVIEW_STORAGE_VERSION=atlas.studio.task-review-storage.v1 默认不设置。未设置时,既有任务创建流程保持原样,回执接口返回 503。明确指定的本地 development 环境可创建新增回执表;已部署环境只读取已迁移的表结构。本变更不应用线上迁移,也不设置部署配置。存储准入后,既有签名 Team 工作创建分支在同一 D1 事务中写入创建凭据。
成功读取返回规范的 StudioItemResponse,包含匹配的 request_id,以及已确认的 Tasks 效果;原始任务不存在或当前不可见时返回 item:null。任务被编辑后,仍按不可变凭据核对原提案、雇员、业务键和载荷摘要。请求无效返回 400,断言或正文绑定无效返回 401,回执绑定冲突返回 409,存储未准入或不可用返回 503。Tasks 的当前客户与商机合并屏障仍有效;customer_merge_pending 或 customer_merged 返回 409。空结果、失败或超时均保留原命令的未知状态:只核对同一个业务键,不能盲目再次创建任务。
Studio 调度:能力发现与共享命令状态
Agent Worker 在既有已验证主体闸门之后挂载规范 Studio 接口。GET /agent/studio/capabilities 是 B00 契约上的只读发现:普通会话(或带 crm:read 的已内省委托令牌)得到关闭的能力矩阵;不宣传任何已连接操作,上游持有者失败保留其归属状态(503 credential_store_unavailable、403 organisation_suspended、401 authentication_required)。
GET /agent/studio/commands/:command_id 在两个独立存储的 Agent 所有者(员工配置回执与图预览命令)之间对账同一命令 ID。所有权由按组织/行动者限定的回执表确立,绝不凭 ID 猜测:两个所有者都找到的命令是真实的跨存储冲突,回答 409 idempotency_conflict;恰由一个所有者找到的命令返回该所有者的规范状态;两者都未知的命令返回条目包装的 not_observed 状态,它不证明任何执行事实。所有 Studio 响应携带 Cache-Control: no-store 与 Vary: Cookie, Authorization。
员工配置草稿(原生所有者路由)
挂载路由遵循冻结的 B00 线上契约与 PR5392 原生动作:GET /agent/studio/staff-configurations?limit=10、POST /agent/studio/staff-configurations、GET /agent/studio/staff-configurations/:definition_id/draft、PATCH /agent/studio/staff-configurations/:definition_id/draft 与 POST /agent/studio/staff-configurations/:definition_id/draft/validate。创建与修补接受带 expected_hire_revision 的严格员工信封,并返回条目包装的 StudioCommandStatus,绝不假设草稿已保存。
授权走既有原生 Team 路径:普通会话、当前 Auth 成员、实时雇佣可见性与写操作的 hire.configure 闸门;读取需要当前成员资格。六个引用字段必须与原生目录对确切雇佣修订的当前解析一致——过期、外部或夹具引脚被拒绝。委托持有者写入不可用(B20 待定)。存储是增量的 0043_studio_staff_configurations.sql 表;未采纳存储时路由回答 503 dependency_unavailable。错误使用规范 Studio 代码(400 invalid_definition、401 authentication_required、403 permission_denied、404 source_unavailable、409 revision_conflict)。
同草稿评审提案(公共适配器)
GET /agent/studio/draft-proposals/:proposal_id、POST /agent/studio/draft-proposals/:proposal_id/apply、POST /agent/studio/draft-proposals/:proposal_id/discard 与 POST /agent/studio/draft-proposals/:proposal_id/reconcile 通过挂载调度提供规范 B12 投影。决策只接受 {review_digest};读取不接受查询参数。响应不包含私有模型/运行来源与日志历史。
POST /agent/studio/draft-proposals(生成)与 .../undo 保持 409 decision_required:其受信的调用方保留创建命令上下文是发布决策,不是路由正文。Apply 额外需要显式 STUDIO_PROPOSAL_APPLY 选项;全部操作需要持久日志选项 STUDIO_PROPOSAL_JOURNAL=atlas.studio.proposal-journal.d1.v1 及图形式候选的 STUDIO_PROPOSAL_GRAPH_LIMITS——缺失时路由回答 503 dependency_unavailable 或 409 decision_required,绝不合成回退。日志、创建意图与删除围栏位于增量的 0044_studio_proposal_journals.sql 表,按组织/行动者哈希限定作用域;不确定结果回答 409 delivery_uncertain 且 retry: read_status。
内部员工私有 Memory 绑定
Memory 通过既有 Service Binding 向 Agent 提供 POST /internal/studio/staff-memory 与 POST /internal/studio/staff-memory/content。请求按调用方签名(kid/密钥轮换、正文 HMAC、30 秒窗口);组织/行动者仅出现在签名目标查询中。原生员工策略默认不可用,任何调用方都不能通过正文字段启用:没有受批准的策略读取器,绑定在任何数据库使用之前回答 decision_required。内容读取保留原生已评审资格、用途、敏感度、有效期与保留约束;私有片段始终是不可信数据。存储是增量的 0009_studio_staff_memory.sql 表。没有公开 Memory 路由。
个人助手身份
个人助手身份是 Agent 服务持有的展示元数据:每人一个名称和头像,不是执行代理,不授予角色或权限。使用 agent.team.inspect / agent_team_inspect 读取 {"view":"assistants"} 查看目录;响应包含 setupSchema: atlas.assistant-setup.v1、固定的 head 总助理身份(标签 Head Assistant / 总助理)以及调用者本人的自定义助手。头像键为 human-01 至 human-08。身份及其提案仅本人可见:他人的提案返回 404 not_found。
通过 agent.team.change 创建或修改:{operation:"assistant.create",identity:{name,avatarKey}},或 {operation:"assistant.update",assistantId,revision,identity:{...}}。新建命名助手会自动沿用 Atlas 现有的任务能力解析:组织启用的技能、可调用的已连接插件工具、内置功能和有权读取的知识;已发布的组织指导和当前用户的生效个人记忆仍由原有模块提供。明确的中英文工作名称会推断工作重点;名称不明确或包含冲突职责时,在创建提案前返回 400 assistant_purpose_required(status: rejected)— 可通过可选字段 identity.purpose 提供一条去除首尾空格、1–400 字且不含控制字符的描述。已保存的命名身份可包含 setup: {mode:"automatic",focus,source:"name"|"clarified"};旧身份不含该字段。只改头像保留工作重点;改名会重新推断或要求澄清,明确填写的用途优先。工作重点仅作为不受信任的用户数据引导任务技能选择,不授予权限、不调用模型、不发布记忆。
两个操作都经由治理账本提案,保持 awaiting_confirmation 直到确认;提案与确认各需一个 Idempotency-Key。v2 应用在成员的一次操作内自动完成创建确认,修改仍保留明确的核对步骤。固定的总助理身份不可修改:对 head 执行 assistant.update 会被拒绝。存储为增量式:迁移 0049_agent_assistant_identities.sql 与 0051_agent_assistant_setups.sql 创建 Agent 自有表;缺表时返回 503 team_unavailable,应用迁移后恢复,且迁移本身不写入任何行。CLI 使用 atlas assistants inspect/propose/confirm — atlas team 命令继续有效,使用相同的 API 与权限。创建身份不会创建执行代理、调用模型或授予权限。
与具名助理进行私人对话
源代码协议为 atlas.assistant-conversations.v1。使用前须部署对应 Agent 代码,依次应用 Agent 迁移 0049_agent_assistant_identities.sql、0050_assistant_conversations.sql,并具备有效的组织会话、本人助理身份及明确开放的 ai-team 资格。接口不会开启账号权限或功能标记。名称和头像仅用于展示,不代表员工职权;目录的 conversationStatus 枚举保持不变。
- 读取
GET /agent/assistant/conversation-capabilities?assistant_id=asst_…。仅在版本匹配且返回available:true、history:events后启用自定义助理聊天。身份或资格不可用时为available:false,依赖故障为 503。 - 使用
POST /agent/assistant/threads,提交{assistant_id,client_thread_id,title}。标题可省略,最多 200 字符;创建键为 1–160 位 ASCII 字母、数字、下划线或连字符。返回200 {item:{id,assistant_id,title,archivedAt,messages:[]}}。重试保留相同键和输入;输入变化返回409 idempotency_conflict。线程 ID 由服务端生成;保留值head不能直接作为线程 ID。 - 列表为
GET /agent/assistant/threads?assistant_id=asst_…,描述记录为GET /agent/assistant/threads/:id?assistant_id=asst_…。自定义线程的描述记录消息数组为空,正文须通过事件恢复。默认及总助理列表保留既有个人历史,并排除自定义线程。自定义线程 PUT 替换返回409 authoritative_history_required。 - 发送
POST /agent/chat,提交{assistant_id,thread_id,client_turn_id,messages:[{role:"user",content:[{type:"text",text:"检查我的下一步"}]}]}。发送键为 1–64 位 ASCII 字母、数字、下划线或连字符。SSE 保留既有帧及run_id。早期客户端消息不会成为历史,权威上下文来自服务端日志。既有模型网关、计量、准入、来源权限及工具确认规则继续生效。 - 重连读取
GET /agent/assistant/threads/:id/events?assistant_id=asst_…&after=0,随后使用返回的head及可选wait=1。重复发送返回409 duplicate_send、run_id、send_status,不再次执行;仍有活跃轮次时返回409 conversation_busy。状态接口为GET /agent/assistant/threads/:id/sends/:client_turn_id?assistant_id=asst_…;状态包括reserved、running、closed、cancelled、interrupted_or_expired。结果已不在保留范围内时,不能据此认定外部动作失败。 - 停止使用
POST /agent/assistant/threads/:id/runs/:run_id/stop?assistant_id=asst_…;删除使用DELETE /agent/assistant/threads/:id?assistant_id=asst_…。空闲中断轮次可在 45 秒后被封闭,延迟到达的开启请求不能绕过日志执行。恢复仅重读状态,不承诺外部动作恰好执行一次;发起另一操作前须核对该动作自己的权威回执。事件遵循既有保留限制;删除墓碑防止复活,清理中断可重试。
自定义线程的详情、事件、决定、状态、停止和删除请求均须匹配 assistant_id。跨用户或组织、身份缺失或不匹配、身份已移除、绑定已停用均返回 404 not_found;未授权集合返回 200 {items:[]}。自定义线程不开放既有支持复查范围,review、diagnostics、support-grant 路由拒绝访问。持久存储不可用时返回 503 conversation_unavailable。本文仅为接口说明,不证明已部署或通过线上验收。
搜索 Atlas
既有 chat 搜索排除自定义助理线程,包括其标题和数量;此动作不提供绑定助理身份的搜索。
使用 atlas.search 查找当前身份可访问的记录和内容。此接口只读取;它不会发送邮件、保存更改或调用 AI。以下说明对应待部署的候选实现,不代表线上验收已经通过。
POST /assistant/v1/actions/atlas.search
{"query":"电机","kinds":["customer","task"],"limit":10}输入 query 为 1–100 个 UTF-16 单元,不允许控制字符;可选 kinds 为不重复的类别数组,limit 为每类别 1–20 条(默认 10)。类别:customer, contact, opportunity, task, work, mail, document, knowledge, chat, assistant.
返回 atlas.action.v1,其结果包含 data.query、items 和 sections。每个请求类别均有状态:complete 表示读取完成,unavailable 表示该来源或凭据不受支持,error 表示读取失败。hasMore 只统计可访问的匹配项。一个类别失败不影响其他类别;可使用原查询重试。
聊天搜索使用 GET /crm/communications/authority/readable?strict=1。已确认的拒绝或空授权集返回 200 {items:[]};权限服务不可用、响应格式错误或存储失败返回 503。搜索将这些失败标为 error,不返回聊天条目或计数。返回结果前会再次检查邮箱授权;授权变化也会撤回该类别的结果。不带此选项的原有权限查询仍保留返回空集的行为。
入口沿用 crm:read。普通成员会话始终携带此传输 scope,不受 CRM 设置权限影响;各所属服务仍单独执行访问检查。任务另需 tasks:read,文件与知识另需 ekb:read。仅持有 EKB 或 Tasks scope 的令牌应使用原有领域接口。委托令牌不能通过此接口读取私人 chat 或 work;它们沿用普通会话权限。assistant 仍返回 unavailable:助理身份目录尚未提供绑定身份的搜索协议。邮件索引未就绪或失效时,mail 返回 unavailable;不会把摘要搜索伪装成完整正文搜索。
每条结果仅含 kind,id,title 及需要时的 subtitle,snippet,customerId,connectionId,threadKey,fileId,sourceId。标题和副标题最多 200 个单元,摘要最多 300;结果不携带任意网址或执行回调。查询在所属服务的结果上限之前匹配,仍受组织、负责人、任务隐私、邮箱授权、聊天来源及知识可见性限制。
所属服务使用内部签名 GET /atlas-search?kind=…&q=…&limit=…;CRM、Tasks 和 EKB 的此接口仅供 Service Binding,不接受外部身份参数。公共客户端使用上述 action。
另行授权应用 0167_comm_mail_search_text.sql 并发布 CRM 后,现有 CRM 定时处理器会在已配置的 cron 周期中,自动为已启用 Inbox 的组织调用 runScheduledMailContentBackfill。每次最多处理 10 条、耗时预算 5 秒;持久游标支持续跑和重试,不让失败来源阻塞后续来源。缺少迁移时维护保持停用。检查仅含计数的 [crm.cron] mail content backfill 日志;遍历结束不等于所有正文都已成功索引。未完成、解析失败或已失效的可读来源仍使邮件类别不可用。新导入邮件自动建立索引;源更改、丢弃或删除会使旧索引失效。搜索不会触发索引。使用实时搜索前,请分别核验部署版本和可读内容的索引就绪状态。
邮件导入和旧邮件回填使用带 SHA-256 摘要的独立不可变存储键。发布时须一并包含导入器、附件及回复读取器兼容支持,以及第二版索引和回填逻辑。第一版索引须重建后才可用。回填会固定当前保留的快照,无法恢复历史可变存储键竞争中已被覆盖的原始字节;旧对象会保留。回滚时必须保留新存储键的读取兼容支持,优先向前修复。
助手自动能力配置
新建命名助手会自动沿用 Atlas 现有的任务能力解析:组织启用的技能、可调用的已连接插件工具、内置功能和有权读取的知识。已发布的组织指导和当前用户的生效个人记忆仍由原有模块提供。无需手动分配能力;不会安装服务、增加权限或发布记忆。
目录通过 setupSchema: atlas.assistant-setup.v1 声明支持。明确的中英文工作名称会推断工作重点;名称不明确或包含冲突职责时,在创建提案前返回 400 assistant_purpose_required,响应包含 status: rejected 和 error.code: assistant_purpose_required。请通过可选字段 identity.purpose 提供一条去除首尾空格、1–400 字且不含控制字符的描述。核对工作重点后明确确认。已保存的命名身份可包含 setup: {mode: automatic, focus, source: name|clarified};旧身份不含该字段。只改头像保留工作重点;改名会重新推断或要求澄清,明确填写的用途优先。工作重点仅作为不受信任的用户数据引导任务技能选择,不授予权限。
共享治理追踪保留运行身份、用量及策略等元数据,不包含私人对话正文、计划或工具输入输出。本人对话历史保持可用。
私有助理历史的兼容性
以 asstchat_ 开头的对话编号保留给个人命名助理。旧版对话、聊天、审核、诊断、支持、搜索和后台路径不可读取或更改这些对话。旧列表、清理和审核发现会在分页前排除它们。现有总助理历史保持可用。命名助理端点需要单独的版本化能力及当前所有者权限;旧服务器不可回退到普通聊天。
助理属性
GET /agent/assistant/properties/:assistantId?locale=en 返回 atlas.assistant-properties.v1,仅限当前组织及当前用户拥有的 asst_… 助理。使用 locale=zh-CN 获取中文指令及技能名称。必须使用有效浏览器会话,且已验证邮箱的域名必须完全等于 forth.ai、forthai.work 或 forthai.cn。支持已验证的普通及平台人员会话,包括安全的角色预览。平台权限不会授予读取他人助理的权限;Bearer 凭据、委托令牌及 Agent 身份均被拒绝。
只读 model、instructions、skills、tools 及 knowledge 区段包含状态、来源、选择方式、配置文本及有上限的名称/ID 列表。configured、unconfigured 和 unavailable 分别表示已知配置、未配置及无法读取。按任务选择的内容会明确标注;最多返回 100 个当前用户有权访问的有效知识来源元数据。个人资料库及私人连接器来源仍在具体任务中读取。不返回服务商凭据、私人对话、个人记忆或检索段落;查看属性不会调用模型、执行工具、写入配置或连接服务。
响应设置 Cache-Control: private, no-store。他人或不存在的助理 ID 返回 404;不符合域名要求返回 403;权限变化返回 authority_changed;助理配置变化返回 assistant_changed。Agent 会在读取元数据前,将当前 Auth 角色、范围及有效权限与会话对比,读取后再次核验 Auth;过期声明会在来源查询前返回 403 authority_changed。另外,在元数据读取前后,均通过不带角色预览的 Auth /me 核验当前 Owner 身份、用户、组织、有效权限及到期时间。平台基础角色、当前成员角色与生效管理员权限分别核验,并检查当前平台身份;省略 owner 表示非 Owner,不沿用 Cookie 声明。随后应用现有角色预览限制,核验请求不会采用轮换的 Cookie。技能、工具或来源目录中任何条目格式错误(包括部分错误)都会使对应区段显示 unavailable;只有有效的空目录才表示已配置且没有条目。此接口不提供 Head:桌面主进程读取其实际本地运行时配置,不启动引擎或登录。Head 未指定固定模型,也不会自动获得 Agent 技能、工具或知识访问权。
可恢复的商机与报价写入
STORY-V2-ERP-03 为现有 CRM 写入路由新增两个可选字段;两者缺省即忽略,既有调用行为完全不变。expected_updated_at(整数 epoch 毫秒)被 POST /crm/engagements/:id/stage 与 PATCH /crm/engagements/:id 接受:它是客户端决策时看到的行版本。若商机此后已变化(customer_engagements.updated_at 不同),写入在任何历史行、时间线行或列更新之前即被拒绝:409 {"error":"snapshot_expired","current_updated_at":<ms>} —— 调用方保留其 intended 修改,必须重新读取商机(响应体给出当前版本)并在刷新审核后重新提交。非法值(非整数、负数)返回 400 invalid_expected_updated_at。该校验与既有规则叠加:丢单原因校验、stage_unchanged 与 Atlas 受保护更新路径均不变。
request_key(8–80 个字符,以字母或数字开头,其余为 A-Za-z0-9_-)被 POST /crm/customers/:id/engagements 与 POST /crm/engagements/:id/quotations 接受,使创建可安全重放:写入超时或应用重启的客户端以相同主体与相同 key 重新提交,即可读回已保存的记录 —— 200,附带的商机/报价带 "deduplicated": true —— 而不会创建第二个商机或第二份报价草稿。key 按组织隔离(两个组织可各自使用同一 key);非法 key 在任何执行前返回 400 invalid_request_key;并发提交中的 key 返回 409 request_key_in_flight(用同一 key 重试);已创建记录其后被删除的 key 返回 409 request_key_spent 并要求换用新 key —— 绝不静默重建。回执保存在增量迁移 0170_deal_command_receipts.sql(表 deal_command_receipts,UNIQUE (org_id, request_key));上述任何拒绝都不会泄露金额、参与者或关联数量。