mcpField
P
portal-external-app

portal-external-app

github.com/xgent-aiDevelopment1155 installsUpdated today
Repository

About portal-external-app

A guide for developers integrating external service applications with a portal, especially services built in separate repositories and delivered as standalone images. It covers authentication, health checks, registration, release, and deployment migrations.

Key features

  1. 01Classify app types and callers
  2. 02Resource-server authentication contract
  3. 03Health-check response validation
  4. 04Registration, release, and tenant access
  5. 05Pre-deployment database migrations
  6. 06Environment and platform-service configuration

Use cases

  1. 01Review an external team's app image
  2. 02Connect portal authentication and service accounts
  3. 03Troubleshoot health checks or releases
  4. 04Plan environment variables and database migrations

MCP support

The guide discusses calling apps through CLI, MCP, or personal automation, as well as portal multi-service user keys and service discovery. It does not state that the skill itself provides an MCP service.

Install

Copy the command and run it in a terminal.

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

After installing, type/portal-external-appin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with 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

Details

From the project 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。

Comments

Markdown supported