mcpField
P
portal-external-app

portal-external-app

github.com/xgent-ai开发1155 次安装今天更新
代码仓库

关于 portal-external-app

为开发者提供外部服务类应用接入门户的判断流程、契约和交付检查指南,适用于服务运行在其他代码仓库并以独立镜像交付的场景。内容覆盖认证、健康检查、注册发布及部署迁移等环节。

主要功能

  1. 01识别应用形态与调用方式
  2. 02资源服务器认证契约
  3. 03健康检查响应校验
  4. 04注册发布与租户授权流程
  5. 05部署前数据库迁移要求
  6. 06环境变量与平台服务配置

适用场景

  1. 01评审外部团队提交的应用镜像
  2. 02接入门户认证和服务账号
  3. 03排查健康检查或发布问题
  4. 04规划环境变量与数据库迁移

MCP 相关

参考内容涉及通过 CLI、MCP 或个人自动化调用应用,以及门户多服务用户密钥和服务发现;未说明该 skill 本身提供 MCP 服务。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/xgent-ai/skills --skill portal-external-app

安装后在 Claude Code 中输入/portal-external-app确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
cli-user-keys.md
config-placement.md
contract-doc-template.md
end-user-auth.md
end-user-lifecycle.md
end-users.md
files-member-docs.md
files-stable-links.md
health-contract.md
integration-contract.md
native-clients.md
public-entrypoints.md
quota-and-seats.md
registration-and-onebox.md
scripts/
health-verdict.ts
verify-health.ts

详细介绍

来自项目 README

portal-external-app · 外部镜像服务类应用接入

外部镜像 App = 服务端在别的 repo(任意语言/栈)、独立镜像交付、常驻(不走按需缩零)。鉴权与业务 API 遵循门户运行时契约;/health 支持平铺 JSON 与 Envelope,检测规则与平台运行时一致。本文件是判断与流程;具体契约按需读 references/(可整目录拷到 App repo 独立使用)。

/apps/<key>/ 与 /svc/<key>/ 是线上 URL;本文的 references/ 和 scripts/ 相对于本 skill 目录。

品牌图:在开发者应用的 App 概览上传 SVG / PNG / WebP(≤2 MiB),取得门户平台存储的 /api/assets/app-logo/<listingKey>/<sha256>.<ext> URL,回填源清单 logoUrl 并递增版本。只能引用本 App 这一路径;外链、data URI、/apps/<key>/ 图、查询串与其他 App key 均拒。icon 仍填命名字形,导航不显示品牌图;品牌图原比例原色展示,坏图回退字形。需要已配置的平台自用存储池与显式 developer:action:branding.manage + 维护者资格,租户 App 编辑者不能改此字段。

按任务读参考

任务读
实现/评审资源服务器(四道闸、health、env、认证面划界、配置面)references/integration-contract.md
实现 /health、出仓迁移、发布镜像或健康检查失败(必读)references/health-contract.md:两种正确实现、实际判据、发布前检查命令
写 manifest、注册布线、一盒联调、排查references/registration-and-onebox.md
匿名页、后端直出 HTML、公开站点/独立域名、旧链接或 listingKey 迁移references/public-entrypoints.md:现有 /apps、/svc、Sites 三种通路及各自责任
将本 App 的图片/视频/PDF 发布为不过期、可吊销的固定内容 URLreferences/files-stable-links.md:特权 scope 申请、服务态签发、轮询、公开读取与生命周期
服务账号写进应用空间的文件,要让作者本人在文件管理里看得到references/files-member-docs.md:onBehalfOf 署名、成员只读可见、成员内容隐私开关
门户成员的原生客户端(iOS/Android/桌面)登录与刷新、设备凭证、停用后失效时效、服务端成员查询与事件references/native-clients.md
顾客/学生等终端用户的完整能力、租户用户中心、托管注册登录、账号与隐私治理references/end-users.md:先区分成员与终端身份,再按任务读认证协议或生命周期接口
用户以 CLI、MCP、个人自动化调用 App,或接入门户多服务用户密钥与服务发现references/cli-user-keys.md:受审 cliAccess 声明、标准绑定与通用目录;不改门户业务代码
为外部服务写/审对接契约文档references/contract-doc-template.md
有「每租户最多几个 X」的配额诉求(选模型、数值谁配、已用怎么来)references/quota-and-seats.md
一个配置项该放哪(环境变量 / 平台级或租户级管理页 / 应用内设置 / 用户偏好),requiredEnvNotes 与 adminPages 怎么写references/config-placement.md

先判形态(决定后面走哪条线)

  • 有用户前端?→ type:"micro"(前端 dist 同源托管 /apps/<key>/,前端开发见 portal-micro-app skill);无前端 → type:"service"(无头:不露卡、不可打开,仅作被调用方,控制台可治理)。
  • 谁调它?→ 自己前端(用户态 TDT;原生客户端用授权码 + PKCE 自取同一种 TDT,见 native-clients.md)/ 其他 App 后端(令牌交换,见 portal-app-exchange skill)/ 其他服务无用户直调(服务账号 client_credentials)。三种来路授权姿势不同(integration-contract.md §3 的表)。
    • ⚠️ 第三种里,如果调你的是租户自己的程序(采集器、脚本、CI),租户管理员可以在门户 /admin/service-accounts 自助开凭证,不用找平台管理员代建。但它只能勾到你 manifest scopes 里的 scope —— 只写在 serviceScopes / privilegedServiceScopes 里的那条, 租户永远勾不到(症状:功能上线了、租户面却没有任何可选项)。判断口径与两条路的差别见 integration-contract.md §3.1。
  • 它自带认证体系(API-Key/节点 Token/gRPC)?→ 按 integration-contract.md §6 划界:门户面必走 TDT 四道闸;自有节点面保持原认证但不得进 /svc(/svc 只转发 HTTP :8080)。
  • 用户是租户旗下网站/App 的顾客、学生等终端用户?→ 先读 终端身份。它是 kind=enduser,不使用门户成员的 /oauth/*、成员角色或席位;不能把普通后台路由一起开放给它。

交付物评审 checklist(收外部团队东西时逐项验)

  1. 镜像:监听 8080、网络别名 <listingKey>-server、amd64+arm64、不烘焙 .env、docker save tar + sha256 或私有 registry;
  2. app.manifest.json(外部团队 repo 里的单一事实源);
  3. env 契约表:镜像实际读取的变量名(镜像自有前缀与门户契约别名的映射必须写清;有的镜像不读裸 PORT);
  4. 迁移入口:迁移文件在镜像里、二进制上有一个迁移 argv(见下节)。「迁移脚本留在自己 repo 里」= 不合格;
  5. 运维口径:是否需每租户 bootstrap(需要则 tenants.id 必须 = 门户租户 UUID);/health 必须通过 健康契约验收,按平台同一判据验证正常与依赖故障;信封响应不能只检查 HTTP 200;
  6. 是否复用平台能力:文件按下文「文件与对象存储」选择 Files 或平台 objectStorage,后者须明确 App 自管的租户隔离、计量与清理;通知、审计、定时任务、配额仍按下面两张表复用平台面;
  7. 租户自助凭证:这个服务有没有「租户拿自己的程序直接调你」的场景(日志/指标摄取、批量导入、CI 回写)?有就检查那条 scope 在不在 manifest 的 scopes 里 —— 不在的话租户面开不出凭证,只能退回「找平台管理员代建」(见 integration-contract.md §3.1);
  8. 配额诉求:有没有「每租户能建多少」这类上限?有就走平台套餐(见 references/quota-and-seats.md),不接受对方自建; 角色本身该由对方在自己的 app.manifest.json 里声明 seatRoles;serviceScopes 里出现 seats.read 仍是当场打回的信号,但对方可以在 privilegedServiceScopes 里申请它(带 reason,进「发布审核」逐条确认批准后授予);
  9. 对接契约文档:按 references/contract-doc-template.md 的结构与精度。没有这份文档的接入不算完成——新服务(如任务网关)要先补。
  10. 终端用户:如有注册、登录、找回、密码管理、注销或个人数据请求,按 终端用户参考 核对身份 owner、realm、profile/client、服务权限、容量与业务数据分工;区分管理 UI、App API 和本人流程,不把成员 OAuth 当顾客认证。

资源服务器硬契约(外部实现最常炸的四处)

Bun / TypeScript 后端不自己实现第 1 条:直接用 @xgent/portal-server-sdk 的 createPortalAuthenticator, 信封解包、主体与凭证类型校验、缓存与故障映射都在里面;其他语言才按下面的契约自己实现。 @xgent/* 从平台私有包仓安装,配法见 xgent-app-release skill「第 0 步」;仓里还没有 .xgent-registry.env ⇒ 停下,请开发者先去门户「开发者应用」生成配置文件。不许 vendor: 不把 SDK 的产物或源码拷进仓,不用 file: / link: / tar 依赖顶替。收外部团队交付时,看到 vendor/ 下有 @xgent/* 就打回。

  1. 自省信封解包:门户响应必须先检查 ok === true 且 data 为对象,再校验 data;失败信封或缺 data 返回 503,不能回退到顶层。只有合法 active:false 才表示凭证失效。TDT 的 exp 必填;无过期长期 key 才可省略 exp。主体/凭证类型矩阵、ACL 范围与缓存预算见 integration-contract.md §3;
  2. /health 按响应形状判定:HTTP 非 2xx 不通过;2xx 响应顶层有 ok 时,必须 ok === true && data.db === true;没有顶层 ok 时按 2xx 判健康。平铺 {"service":"my-app","db":"ok"} 与信封 {"ok":true,"data":{"db":true}} 都支持,不按内建/外部 App 区分。平铺依赖故障须用 HTTP 503 表达;不要给字符串状态直接套 ok(...)。完整示例与同源检查脚本见 health-contract.md。
  3. 门户三变量 all-or-nothing:自省地址 + SA clientId + secret 全缺→鉴权停用 503;缺一→启动 fail-fast 打印缺失项。
  4. 平台级跨租户闸:如有跨租户路由,只认服务端自省返回的 claims.isPlatformAdmin === true。它由 Portal 按用户实时计算、不在 JWT 里;role / bypass 只属于当前租户,服务态恒为 false,不接受前端自报或 Cookie 转发。

完整契约(四道闸、三种令牌来路、审计、划界)见 references/integration-contract.md。

一盒初始化与升级

宿主 skill 与镜像分别更新:先 npx skills add XGENT-ai/skills 更新 portal-dev-setup, 从实际安装目录核对 onebox.sh 的 SHA-256,再按发布说明的匹配镜像操作。 requiredEnv[].generate 的最低门户提交 / APP_VERSION、一盒 tag、公开 skill 提交和脚本摘要 目前均未发布;完整证据状态见 portal-dev-setup §6,不能默认当前 latest 已支持。 首次联调用 portal-dev-setup 的 onebox.sh init → up;已有一盒换版本用 upgrade [--image <ref>] → up;沿用 latest 先 pull 更新缓存,已加载的本地新镜像可离线升级。 不使用 init --force 代替升级。升级保留配置、generated/、backup/、 端口与卷;up 仍重种门户库,不重种 App 自己的库。自带对象存储为 RustFS,保留 MINIO_* 配置与 minio:9000 网络别名;控制台路径为 /rustfs/console/。存量对象由切换脚本复制、核对后才切换, 切换后有新写入时不能直接回旧卷,细节见 registration-and-onebox.md。

注册布线(细节见 registration-and-onebox.md)

  • dev / 一盒:bun run register-app <manifest>(幂等,生产拒跑)= upsert listing + 直写服务账号 + 写发起方 App Secret + 写 /svc 白名单 map。⚠️ exchangeInitiatorSecret 的哈希在首个租户安装时还没有任何实例可布——先安装 App,再重跑一次 register-app,否则发起交换 401(之后装的租户会随安装自动补布,见 registration-and-onebox.md §2.2)。日志里 wired into N … 的 N 是「改了几个实例」,0 也可能是「本来就对」——判据是比哈希,见 registration-and-onebox.md §2.2。
  • ⚠️ 发布 ≠ 租户能看见:租户市场对 tenant_listing_grants 是 fail-closed。dev 的 register-app 会自动授予当时已存在的租户(非 service 型,输出里有 租户可用应用: 一行);生产不自动(提案批准 / 控制台导入都不授予——语义是平台管理员显式勾选)。症状是「控制台显示已上架、租户市场里却根本看不到」——去 平台 › 租户管理 › 租户 › 「可用应用」勾上保存(registration-and-onebox.md §2.1)。
  • 生产(标准路径):清单事实源在对方仓的 app.manifest.json,经 发布提案到达:平台管理员在 控制台 › 应用市场 › 接入新应用 按 key 签发 xrel_ 令牌 (无行先建 draft 占位),对方 publish --manifest … --dist … --image … ⇒ 首次必进 「发布审核」队列 ⇒ 批准后更新清单、服务账号与已装租户,后端和网关分别交付。 SA / 交换密钥一次性回显给审批人;requiredEnv[].generate 声明的独立密钥永不回显。 平台运行配置由管理员维护,App 团队无需修改门户代码。 特权服务态 scope 经 manifest 的 privilegedServiceScopes 申请、逐项审批后授予; 用量指标经 manifest usageMetrics 声明(治理档)。批准不等于部署与网关已经生效, 交付判据见 公开入口。
  • 后续版本:对方 CI 自助(xgent-app-release skill)——无治理变更自动通过, 治理变更进「发布审核」等平台批准。
  • scope 三规则:平台基础 scope ∪ 自己命名空间(含连字符→下划线变体)∪ 已声明 exchangeTargets 的目标命名空间;越界 VALIDATION_FAILED。
  • secret 一致红线:平台落库的服务账号 secret 与镜像 env 里的必须一致;漂移症状是自省 401 或「跨应用授权缺失」(实为 SECRET_INVALID),排查先 sha256 比对两侧。

数据库迁移:平台在部署时替你跑

划分决定一切细则:schema 是平台级的(随一次部署发生一次),数据行是租户级的(随安装发生)。 所以 DDL 属于部署步骤 —— 不属于容器启动,也不属于逐租户安装。

外部团队声明 deployDescriptor.migrateArgs(平台管理员填),控制器在换容器之前用 同镜像 + 同 env 跑一次性容器(--rm;pm2 / compose / K8s 三个 flavor 都实现了):

job(deploy|redeploy)
 ├─ ① migrateArgs 非空 → <entrypoint> <migrateArgs>;失败 ⇒ 任务红 + 旧容器继续服务
 ├─ ② ensureServiceUp     ← 到这一步才换容器
 ├─ ③ waitHealthy(180s)   └─ ④ openRoute

为什么不是「启动时自迁移」:失败会变成崩溃循环(要 docker logs 才知道为什么), 还要挤 180 秒健康窗口。放在换容器前,失败时旧版仍在服务,且是面板上一个有名字、 带对方 stderr 的步骤。

为什么必须在镜像里:迁移要在库所在的地方跑,而跨越边界的产物只有镜像;对方 CI 连不到(也不该连到)门户的库。且镜像 = 那一版代码,它要求的 schema 与它同源同版本 —— 分开走就会漂。门户永远不执行由 App 投递进来的脚本(宿主机侧的 migrateScript 只对 仓内 built-in 开放,descriptorToService() 对 config App 钉死为 null)。

要求外部团队做到四条:迁移打进镜像 + 一个迁移 argv(如 --role migrate)· pg_advisory_lock 包住 · 失败非零退出别自愈 · expand-contract(迁移跑在换容器前,那一刻旧版本还在服务)。

⚠️ migrateArgs 只有门户部署了对应版本才生效。反过来的顺序(先填字段、门户没更新) 会被 jsonb 原样存下、无人读、不报错 —— 与被删掉的 migrate/provision/bootstrap 三个死布尔同病。次序固定:门户先部署 → 对方出镜像 → 才填字段。

⚠️ 每租户 bootstrap 目前没有出口:bootstrapScript 对 descriptor App 恒为 null, 门户不会替外部服务建租户行。若对方的表 FK 自己的 tenants,接入时必须显式定方案 (对方开 provisioning 端点 / 首次使用自建 / 门户加钩子),否则症状可能是静默写 0 行 (INSERT … WHERE EXISTS (SELECT 1 FROM tenants …) 这种写法不会报错)。

改动启动必需 env 的键名,必须发版前知会门户

descriptor.env / envFile 是平台管理员治理字段,manifest 永远带不进值(提交即拒), requiredEnv 只交键名。键名集合的变化属治理档:写进 manifest 的 requiredEnv 一变 (新增/改名/删除),提案就转入「发布审核」,管理员批准前先看到新键名 —— 这是把 「知会门户」变成流程内动作的那道门。CR-3 之后它还是一道真闸:批准前门户拿这张清单比对 descriptor.env ∪ envFile 的键集合(只看键在不在,不读值),缺了就拒绝生效 (REQUIRED_ENV_MISSING,提案留 pending);审批屏逐键标状态,非密钥可当场填。改名要写成 { key, renamedFrom } 一条,门户会把旧键在 descriptor.env 里的值搬过去。普通条目在 envFile 中的旧名仍判缺;带 generate 的条目可沿用可读文件中的旧名,App 必须兼容消费, 不改文件、不复制值、不生成新主键。旧键已由平台保管时拒绝改名。仍有一件它管不到:对方 不写进 requiredEnv 的静默改名依旧无门可拦 —— 症状是服务 换版后 refusing to start,而发布方那侧只看到成功。2026-08-19 知识库把 XGENT_OPENAI_* 整族改成 XGENT_EMBED_* 就是这么崩了几小时的;只改其中一个 key 更坏 —— 服务起得来, 但 base URL/model/dims 静默回落默认值。所以收交付物时仍要求:改必需 env 必须同步改 manifest 的 requiredEnv(改名走 renamedFrom),并把「本版新增/改名的必需 env」列进 checklist。 业务参数别塞进 requiredEnv:审批人只懂环境,不懂你的业务。能在代码里给缺省值就不进清单; 确实要让平台部署时覆盖,就在顶层 requiredEnvNotes({ 键名: 一句说明 },≤200 字,只给需要解释的键写) 里写清填什么、不确定时填什么,审批屏会贴在键名旁。放哪一层的完整判断见下文「配置放哪一层」。发布面本身 (令牌、tag 不可变、--wait / --wait-review / status)见 xgent-app-release skill。

值从哪来:键名约定与独立密钥生成
档键值从哪来
平台注入PORT · <PREFIX>_SERVER_PORT · PORTAL_INTROSPECT_URL · API_BASE_URL · PORTAL_BASE_URL · <PREFIX>_SA_CLIENT_ID · <PREFIX>_SA_CLIENT_SECRET · <PREFIX>_APP_SECRET(声明了 exchangeTargets 时)平台换版时自己算并注入,没有人手填
自动供给库连接串 · Redis 地址 · 对象存储五键(每应用一桶)· Kafka 五键批准时平台按登记的服务代建资源 / 取地址并注入
随机生成App 独立密钥,声明 { key, generate: { bytes: 32, encoding: "base64" } }批准时生成、加密保管;换版复用,值永不回显
自定其余环境依赖(第三方地址、第三方已有密钥…)平台管理员手填:非密钥进 descriptor.env,密钥进主机 envFile

generate 仅收 bytes(16–64 整数)与 encoding(标准带填充 base64 / 小写 hex)。 不可用于平台派生键或服务供给目标。套件共享后端里同名共享键只由一个成员 generate,其余成员以 字符串 requiredEnv 消费(先批生成者;两名生成者拒绝)。 已有主键或匹配保管值保留;规格冲突、来源冲突、不可读 envFile 均硬拦,不能填新值绕过。 generate 的增加、撤回、规格变化都需审批;已生成值不覆盖、不提供读取或轮换入口,丢失应恢复备份。 一盒新版 add 在共享的 <一盒目录>/compose.env 中首供并稳定复用,同名键共享同一值;主 App 与其他 App 均可能读到,适用于可信团队调试。生产则按 App、按 brand 隔离,不以一盒结果证明生产隔离。

业务参数不在这张表里:先给代码缺省值;要进清单就配 requiredEnvNotes(见上一节末尾与「配置放哪一层」)。

<PREFIX> = listingKey 全大写、连字符换下划线。PORTAL_BASE_URL 是门户公开源(也是调 别的 App /svc/<key> 的基址),API_BASE_URL 是内部 portal-api,不代理 /svc。

门户内 App 的服务地址由代码派生,不逐个要求平台手填。 以 Page Builder 为例:

原来的地址变量应用代码使用的默认基址
LLM_GATEWAY_SERVER_URL${PORTAL_BASE_URL}/svc/llm-gateway
FILES_SERVER_URL${PORTAL_BASE_URL}/svc/files

接入或审核时核对这几项:

  • 清单:requiredEnv 声明 PORTAL_BASE_URL,保留其他确实必需的键;上面两个可推导的 地址不再列为必填。平台注入的是门户基址,不会自动注入这两个旧变量,也不要把派生地址值写进 manifest。
  • 实现与迁移:代码去掉门户基址末尾的 / 后拼接 /svc/<key>,再追加具体 API 路径。 旧变量可保留为本地调试或特殊部署的可选覆盖项。代码默认值、启动校验、清单与新镜像同步交付, 不能只删 requiredEnv;平台侧核对已有覆盖值是否保留,并在未设置旧变量时验证实际调用。
  • 权限:统一入口不替代鉴权。代表用户调用仍需 exchangeTargets、目标 scopes 与令牌交换; 后台调用仍用获准的服务账号权限,具体姿态见下文「App 型公共服务」。

这条约定针对门户内的 App 服务;第三方 API 等无法从门户基址推导的地址仍按需声明。 发布操作见 xgent-app-release skill「调用门户内的其他 App:从 PORTAL_BASE_URL 派生地址」。

服务怎么绑到键:键名按约定命名(<PREFIX>_DATABASE_URL / REDIS_CONN_STRING),再声明要哪条服务 ——

"requiredServices": [{ "kind": "postgres", "name": "main" }]

⚠️ requiredServices 的每一条只收 name / kind / note,多一个字段整份清单直接拒收。 只写 requiredEnv 的约定名也认(用该类型的缺省服务)。键名不按约定来(ZO_META_POSTGRES_DSN 这种上游定死的名字)也能发,只是平台管理员要在审批屏上逐个点一下「从平台服务取值」, 每次发版多等一轮。收交付物时把这一条当作可读性问题看:不是错,是白等。

Kafka:声明 requiredServices: [{"kind":"kafka","name":"main"}],并在 requiredEnv 列出 <PREFIX>_KAFKA_BROKERS / _KAFKA_USERNAME / _KAFKA_PASSWORD / _KAFKA_SASL_MECHANISM / _KAFKA_TOPIC_PREFIX(每项均带 <PREFIX>)。机制固定 SCRAM-SHA-512,用户名 app_<key> 把连字符改为下划线;主题、消费组、事务 ID 的前缀 app.<listingKey>. 保留连字符。 App 显式建主题、副本数 1,不能更改保留期或增加分区;不申请管理员密码。平台是单节点 at-least-once 消息服务,不是业务记录系统,App 负责重复消费、重连与租户隔离。 一盒新版 add 自动供给相同权限并沿用已有密码,但整份 compose.env 仍注入所有容器, 不提供容器间密钥隔离。完整联调步骤见 注册布线与一盒。

⚠️ 服务账号密钥不再要求进 envFile:门户保管并在换版时注入容器,明文只在批准那次一次性 回显(留给对方本地联调)。收交付物时别再问对方「密钥写进 envFile 了吗」。 另外,平台级能力(如日志写入)会直接出现在对方的服务令牌里,不需要在 manifest 里申请。

收交付物时一并核对「跑在哪、要什么服务」两节

manifest 还有两节同形的审批前置闸(与 requiredEnv 同一挂载点、同一失败语义):

  • deployRequirements({ region?, size?, gpu?, network? })—— 这个 App 的后端需要什么样的 机器。批准那一刻按本环境的资源池逐维匹配,不满足就拒绝生效;通过时把落点固定到命中的 那台。需要 deployDescriptor,且只收名字与档位 —— 网段 / IP / URL 提交即拒。
  • requiredServices([{ name, kind, note? }])—— 运行需要哪些外部服务。批准时比对 本环境已登记的「已具备服务」(控制台 › 平台设置 › App 清单目录),未登记或缺条目就 拒绝生效。登记表描述已有服务;支持自动供给的类型还会按所选档案为 App 建资源与注入约定键。

收交付物时按这两节问三个问题:它要跑在哪个网络里(对方写的名字与本环境登记的名字对不 对得上)、它连的那些基础服务本环境有没有并且登记了没有、以及远端主机上没有门户的 compose 网络 —— 一份在一盒里能跑的 descriptor,env 里写着 postgres:5432 时搬到远端 落点上是连不通的,那正是 requiredServices 要把「本环境有什么」摆到审批人眼前的原因。

被拒时对方只会收到一句泛化的「当前环境不满足…,提案已转入待审核队列」(池、网段、机器名 一个字都不出门户控制台)—— 所以差在哪要由门户侧的人告诉他们,别让对方对着一句话猜。

配置放哪一层

收交付物时逐项问「这个配置是哪一类」,完整判断表见 references/config-placement.md。要点:

  • 环境依赖进 requiredEnv;业务参数先给代码缺省值,确需覆盖才进清单并配 requiredEnvNotes——审批人只懂环境。
  • 全平台一份、要随时改 ⇒ 平台级管理页 adminPages.platform;每租户不同 ⇒ 租户级管理页 adminPages.tenant。 页面由 App 自己做,门户嵌进应用市场编辑页 / 租户应用配置,门户零改码(治理档,需审批)。
  • 应用内业务设置 ⇒ 应用内设置页 + aclManifest 授权;用户偏好 ⇒ App 自己按用户存。

无前端的服务型 App 没有页面可嵌:它的租户管理员配置仍要门户按 App 补三件(不是零改码)—— <key>-token 铸造端点 + <key>-config-api.ts + AppForm 配置 Section,参照 files / llm-gateway, 步骤见 portal-backend-app skill;接入时把这三件列入门户侧工作量。有前端的 App 一律走 adminPages。

先看平台有没有,别自建一套

外部服务最常见的浪费是把平台已经有的公共能力又实现一遍:自己存文件、自己发通知、 自己记审计、自己排定时任务。除了白干,还会让租户在两个地方看同一件事、让平台的治理面 (审计/用量/权限)出现盲区。动手写之前先在这两张表里找。

A. 门户基座能力 —— 用你自己的 TDT 直接调 /api/v1/*

PLATFORM_BASE_SCOPES 里的 scope 任何 App 都能在清单里直接声明,不需要跨应用授权、 不需要令牌交换。

你想做的scope端点
当前用户是谁userinfo.readGET /api/v1/userinfo
选人 / 读通讯录directory.read(邮箱是 PII,要另加 directory.email.read)GET /api/v1/directory/users
站内消息 / 通知notification.send(只发给自己)· notification.send.others(发给别的成员)POST /api/v1/notifications;服务态用 /api/v1/notifications/service
收件箱inbox.read—
统一审计audit.writePOST /api/v1/audit
定时任务scheduler.read / scheduler.write/api/v1/scheduler/tasks
结构化内容 / CMScontent.read / content.write/api/v1/content/{type}
用量计量usage.report(只收 namespace 前缀 == 你 azp 的 metricKey)POST /api/v1/usage/report
席位配额seats.read(SERVICE_ONLY,经 manifest privilegedServiceScopes 申请、审批授予,见下节)POST /api/v1/seats/quota
币种与汇率currency.readGET /api/v1/currency
判权限(PID)—POST /api/v1/acl/check
用户级设置settings.read / settings.write/api/v1/settings/me
Dashboard 卡片widget.write(用户态,服务态令牌推不了)PUT /api/v1/dashboard/widgets/:key/data;清单顶层 dashboardWidgets 声明(不是 dashboard.widgets)。有用户关心的状态就该暴露至少一个,前端用 SDK 推;完整契约见 portal-micro-app skill 的 Widget 一节
终端用户身份也是门户能力,但使用专门服务身份

注册、凭据、会话、条款与隐私接口在 /api/v1/end-users* 等基座路径,管理入口为「用户中心」。 它们不走 /svc/user-center,也不走成员的交换流程;endusers.* 和 privacy.requests.* 是 SERVICE_ONLY scope,按需经 privilegedServiceScopes 申请。终端身份不是成员通讯录, 登录、当前限制和完整分工见 终端用户参考。

B. App 型公共服务 —— 走 /svc/<key> + 令牌交换

这些是独立 App,不是基座端点:要在清单里声明 exchangeTargets,用交换来的 aud=<目标> 令牌调(机制与排错见 portal-app-exchange skill)。

服务listingKeyscope 命名空间
文件存储(用户空间 + 应用空间、直传、预览、缩略图)filesfiles.read / files.write / files.share
多模态解析(OCR / 文档结构化)omni-parseromni_parser.read / omni_parser.parse
大模型网关(OpenAI 兼容 /v1)llm-gatewayllm_gateway.read / llm_gateway.write

⚠️ 连字符 listingKey ↔ 下划线 scope:omni-parser 的 scope 是 omni_parser.*, llm-gateway 的是 llm_gateway.*。两个命名空间,写混了是 VALIDATION_FAILED。

文件与对象存储:两条接入路径
Files 文件服务objectStorage 基础设施
接入调 /svc/files,使用获准的用户态或服务态令牌清单声明 requiredServices: [{"kind":"objectStorage","name":"main"}],后端用 S3 SDK 直连;main 换成目标环境已登记的服务名
桶与租户托管模式使用租户池 role=tenant:多租户共用 bucket,默认按 t/<tenantId>/ 前缀隔离使用应用池 role=app:按 listingKey 每应用一个 bucket,该应用所有租户共用;租户安装时不另建桶
隔离与治理Files 按租户、空间与权限收口,提供文件配额、计量、预览和清理能力平台不自动加租户前缀;App 自行校验租户、组织对象 key,并负责权限、计量和生命周期;不自动纳入 Files 治理
适用用户/团队文件、需要平台文件治理的应用文件自带 S3 存储引擎、索引快照或缓存等由 App 自管的底层对象

Files 的「应用空间」是某个租户内的逻辑空间,不是应用专属 bucket。 租户在 Files 中自配的 bucket 与凭据优先于托管池;存储位置由 Files 解析,调用 App 不取得租户池的 S3 凭据。

应用池当前注入的是池级共用的数据面凭据,不发 per-App / per-tenant 子凭据;每应用独立桶 不代表云侧已做跨 App 权限隔离。声明与五项 S3 环境变量见 xgent-app-release skill 的 「对象存储:应用池直供 S3 五键」。五键按整组供给,不需要管理员逐键取值。

应受平台文件治理的字节走 Files;App 自管底层对象可使用平台 objectStorage。以下用户态、 服务态与空间选择仅适用于 Files,按这份字节归谁选择:

这份字节归谁姿态落在哪用户在文件管理里
归 App:系统产物、用户删不得、随 App 生命周期消失服务账号 client_credentials(无用户上下文)你自己的应用空间(不传 spaceId,懒创建)只有租户管理员看得到(要让作者看见用下一行)
归 App,但作者要看得见:文件由你掌控、用户删改不得,作者本人要在文件管理里找到它服务账号 + onBehalfOf=<成员的门户 user_id> 署名你自己的应用空间署名成员在「我的应用文件」里只读可见,其他成员看不到;读 代成员保存
归用户:用户主动另存、他看得见删得掉、算他的空间令牌交换换出的 aud=files 用户态令牌用户选的个人/团队空间(带 spaceId)就在他自己的空间里

公开固定内容 URL 是 Files 的特权服务态能力:申请 privilegedServiceScopes 中的 files.links.manage,审批后由本 App 服务账号发布自有应用文件;用户态令牌、目录授权和 files.read 都不能替代。签发通常先返回 creating,只有 ready 才有 URL;持链接即可匿名读, 不会自动过期但可吊销,也会随删源/应用卸载失效。需要此能力时读 长期链接接入,不要放进门户基座能力或普通用户交换流程。

⚠️ 别因为「要写」就默认必须服务态:用户态持有 files.write 完全合法 —— exchangeTargets 背书的是整个命名空间,平台不再按读写另行裁量。凡是「用户在你的界面里点一下、另存到我的 空间」这类功能只能用用户态:服务态写进去的是应用空间,不署名用户根本看不见自己刚存的 东西,署名了也只能只读查看,不在他自己的空间里、删不掉。用户态写也不丢归因 —— 交换出的令牌带 azp=<你的 key>,files 侧照样落 viaApp 溯源,按来源 App 聚合展示。

已经立过的红线(同一条道理,都是「平台有、别自建」)
  • 审计:不自建审计表/页,推 POST /api/v1/audit,租户在「设置 › 审计」一处看全。
  • 通讯录:不做「同步通讯录」到自己库,按需用选人组件挑。
  • 配额/套餐:见下一节。计费:平台级 Credit 服务,同样不自建。
  • 租户级配置:放门户的 App 配置页(AppForm Section),不是 App 内的设置页。
  • AI 功能:终端用户面不出现模型选择器,服务端按场景解析模型。

有配额需求?用平台的套餐管理,不许自建

红线:外部服务不自建配额/套餐/计费。 配额的唯一事实源是平台的套餐矩阵;对方在自己库里存 一份「本租户上限」,第一次调档就会与门户对不上,而且没有任何东西会告诉你它们分叉了。

  • 两个模型二选一,都只有 backed listing 可声明:seatBased(计门户成员用不用得了这个 App,门户在 TDT 签发时收口,App 侧零代码)/ seatRoles(App 自管账号或资源,门户只发数、 App 自己在建号路径上收口)。
  • 上面是 App 席位/业务资源模型;平台托管终端身份另用租户共享的 portal.end_users, 由平台在身份创建/删除时占用/归还,归档仍占额。不要拿成员席位代替或再扣一份身份容量, 见 终端用户容量。
  • role 由对方在 app.manifest.json 里声明(不需要门户改代码);各套餐的数值进不了 manifest,只有平台管理员在控制台能写 —— 应用侧一条写路径都没有。 这是有意的:数值是商务面,改它等于改可售卖档位。
  • 对接面只有一个:POST /api/v1/seats/quota(服务态令牌 + seats.read)。available === null ⇒ 该租户 unlimited,放行、不要收口 —— 把 null 当 0 是这个契约最容易踩的一处。
  • ⚠️ seats.read 是 SERVICE_ONLY_SCOPES:对方 manifest 里出现 serviceScopes: ["seats.read"] 会被拒收,评审时这是当场打回的信号,不是配置失误。正确路径是 privilegedServiceScopes: [{ "scope": "seats.read", "reason": "…" }] —— 这是申请不是授予: 必进「发布审核」,审批人逐条勾选确认后 SA 才拿到(reason 原文展示给审批人)。
  • 配额 ≠ 用量,别混:配额答「还能不能再建一个」,用量答「这段时间用了多少」,后者走 usageReporter + usage.report。计费方向另有平台级 Credit 服务,同样不自建。

真要落一版(选模型、写 manifest、配数值、上报已用、存量租户、三态处理、判定某个数该不该进 套餐表)⇒ 读 references/quota-and-seats.md。

一盒联调与冒烟

外部团队不用克隆本 repo:平台给两个门户镜像 + deploy/ 目录,对方出 manifest + compose.env + 自己的镜像。步骤、冒烟命令与排查速查表见 references/registration-and-onebox.md §4–§6。

评论

支持 Markdown