Skip to content

reports 能力不在 requires 里,但 10 个报表已声明、且 crm_case 的 allowExport 授权正是靠「报表面」立论 —— 工单导出可能根本没有落地面 #798

Description

@yinlianghui

发现于 #763 / PR #797(改写 guides/import-and-export.mdx 的导出段时,逐条核对「谁能导出什么」)。不在该 PR 的文件面内(涉及 src/** 与 objectstack.config.ts),单独记录。

实测

objectstack.config.ts 声明:

requires: ['automation', 'triggers', 'analytics', 'auth', 'ui', 'approvals', 'sharing']

平台常开的那批(PLATFORM_ALWAYS_ON_CAPABILITIES,来自 @objectstack/spec)是:

queue, job, cache, settings, email, storage, sms, sharing, messaging, analytics

两者的并集里没有 reports。reports 在 CLI 的 CAPABILITY_PROVIDERS 里是一个正常的能力项(@objectstack/plugin-reports → ReportsServicePlugin),也没有任何自动拉起:serve.ts 里唯一提到 'reports' 的地方是 NEEDS_JOB_OR_QUEUE = ['email', 'approvals', 'reports', 'auth'],那只在 requires 已经包含 reports 时才为它补 queue/job。

与此同时:

  • src/reports/ 下有 5 个文件、pnpm validate 报 10 Reports,全部作为元数据编译进产物;

  • src/reports/case.report.ts 的三个报表都建在 case_metrics 数据集上;

  • src/profiles/index.ts 里 allowExport 那条规则明写:

    a profile grants allowExport on an object IFF it already holds allowRead there AND the app ships an export surface for that object — a list view declaring exportOptions, or a report whose dataset is built on it.

    并由 test/authorization-coverage.test.ts 钉住。

crm_case 拿到 allowExport 的五个角色(sales_rep / sales_manager / service_agent / marketing_user 之外的 system_admin,以及前述四者中的四个)里,没有一个能通过列表视图导出工单:声明 exportOptions: ['csv', 'xlsx'] 的只有 account / contact / lead / opportunity 四个视图。也就是说 crm_case 那条授权的落地面只能是报表导出(ReportService.assertExportAllowed)。

问题

如果 reports 能力在默认启动下不加载,那么:

  1. 10 个报表元数据编译进产物但没有服务消费它们;
  2. crm_case 的 allowExport 授权指向一个不存在的面 —— 而 authorization-coverage 守卫认为它是有面的,因为守卫读的是「有没有一个报表建在该对象的数据集上」,而不是「报表服务在不在」。

守卫因此在这一格上是自洽但空转的:它验证的是元数据之间的一致性,不是能力是否被启用。pnpm validate 的 "Checking capability providers (#3366)" 也没有对此报警(本次 validate 全绿)。

待确认

没有实测跑过启动来确认报表面到底在不在(本单来自静态核对,且发起单的文件面只允许动 content/)。所以第一步应当是:起一个 objectstack start,看 /_console/ 下报表是否可达、GET 报表导出是否 200。三种可能:

  • 报表服务确实没加载 → 要么把 reports 加进 requires,要么承认工单导出不落地并调整 crm_case 的授权与守卫规则;
  • 另有路径加载了它(比如 ui 层或 analytics 侧带起来)→ 那么 src/profiles/index.ts 的规则注释应当写清楚这条依赖,否则下一个读的人会得出和我一样的结论;
  • 报表可达但导出走别的门 → 规则注释同样需要更新。

关联

Activity

  1. added
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 5, 2026
  2. self-assigned this
    on Aug 5, 2026
  3. yinlianghui commented on Aug 5, 2026

    @yinlianghui
    CollaboratorAuthor

    🔒 认领(修复线 PM 派单,测量先行):session_01VHrPAGEgFDoHjphqYG4BMa,分支 claude/issue-798-reports-capability,worktree hotcrm-issue-798。

    裁定(两分支预授权,以实测定):第一步照单里写的证法实测——起真实 dev server,确认 10 个已声明报表在运行时是否真的工作(渲染/取数)。(a) 若不工作:把 reports 加进 requires 即 restore-invariant(声明了 10 个报表就该能跑),补验证 + changeset,PR Fixes #798;顺带确认 crm_case 的 allowExport 立论(报表导出)是否随之成立。(b) 若工作正常(能力经其它途径自动拉起):不改 config,把实测结论回填本单(说明 requires 清单为何不需列它),转为修正 issue 前提或文档/立论侧的最小修——按实测写明。文件面:objectstack.config.ts(仅分支 a)、相关验证、changeset;⛔ 不升级 @objectstack/*、不动 releases/、不动 profiles 授权值。dev server 用后精确 PID 拆除(防 #777 的假绿陷阱)。


    Generated by Claude Code

  4. yinlianghui commented on Aug 5, 2026

    @yinlianghui
    CollaboratorAuthor

    实测结论:走分支 (b),本单前提不成立

    按单里写明的证法起了真实 dev server(objectstack dev -p 4798,17.0.0-rc.2,origin/main @ a98ddaa),两条推断都被证伪。PR #816 因此不动 objectstack.config.ts,只修被写错的立论文本,并补上真正缺的那格守卫。

    基线(reports 不在 requires)

    启动横幅:Plugins: 38 loaded,列表里没有 ReportsServicePlugin —— 这一半单子说对了。但接下来的推论不成立:

    探针 结果
    GET /api/v1/meta/report 200,10 项,与 src/reports/ 声明的十个逐一对上
    Console apps/app.objectstack.hotcrm/report/sla_performance 渲染,表格 5 行 / 20 单元格
    同上 cases_by_status_priority 渲染,22 行 / 87 单元格,6 个 Recharts 图元(柱图画出来了)
    同上 pipeline_coverage_by_quarter 渲染,4 行 / 28 单元格
    报表页的取数请求 POST /api/v1/analytics/dataset/query → 200,有行;全程零次 /api/v1/reports 调用
    GET /api/v1/reports(及 /:id、/:id/run、/:id/schedules) 501 NOT_IMPLEMENTED

    10 个报表不是没人消费的死元数据。 它们由 metadata API 供给、Console 的 report/:reportName 路由渲染、analytics(平台常开)取数;其中三个还由 src/apps/crm.app.ts:153-155 的导航直接挂出。

    关键的认知偏差在这里:reports 能力根本不是 report 元数据的消费者。 ReportService 只读 sys_saved_report 行(object_name + query_json,即对象 ObjectQL 查询),和 dataset 驱动的 ReportInput 是两套不相干的东西。

    反向验证(把 reports 加进 requires,重新 build + 重启)

    先定方向再跑:预期 10 个报表毫无变化。实测正是如此:

    • Plugins: 39 loaded,ReportsServicePlugin 出现;新增 sys_saved_report、sys_report_schedule 两个对象(88 → 90),外加一个 60 秒轮询的调度器。
    • 三个报表页的表格行数/单元格数/Recharts 图元数量与基线逐格相同,网络请求依旧零次 /api/v1/reports。GET /api/v1/meta/report 仍是同样的 10 项。
    • GET /api/v1/reports 501 → 200 {"data":[]}(空的保存报表列表)。
    • POST /api/v1/reports/sla_performance/run → 404 REPORT_NOT_FOUND —— 已声明报表的名字在这个服务里根本不可寻址。

    所以加它不是 restore-invariant,而是新开一套本应用零内容、零 UI、零业务拉力的能力面(保存的报表 + 邮件定时摘要)。按「核心能力优先、能力扩张要有真实业务拉力」的取舍,没有加。

    crm_case 的 allowExport 落在哪(第二条推断)

    批量导出在本应用只有一条服务端路由:GET /api/v1/data/:object/export(csv/xlsx/json),由 enforceExportPermission → security.canExport 把守(@objectstack/rest)。实测:

    • 持有授权的主体 → 200,返回真实 CSV(28 列表头 + 数据行)。
    • 无 allowExport 的普通用户 → 403 EXPORT_NOT_PERMITTED。
    • 加不加 reports 能力,这两个结果都不变。

    而列表视图的 exportOptions 不是第二扇门:Console 的 Export 按钮(data-adapter chunk)拼的就是 /api/v1/data/{object}/export?... 这同一条路由;exportOptions 只声明按钮提供哪些格式。所以 crm_case 的授权是活的、被真正执行的,只是没有列表按钮,得走数据 API。

    反过来,ReportService.assertExportAllowed 在本部署里根本没加载(501);即便加载了也够不着 —— 三个报表页实测都没有任何导出/下载入口(exportButtons: [],加载能力前后一致),而 ReportService 又只认 sys_saved_report。

    因此改了什么

    src/profiles/index.ts 里那句「Both bulk-egress doors ask before they read: the list views' built-in exportOptions, and ReportService.assertExportAllowed」两处都错:exportOptions 不是门,assertExportAllowed 在这个应用里不是任何一条授权的落点。本单正是照这句话读出来的,所以 PR #816 把它、test/authorization-coverage.test.ts 的同源 rationale、以及 content/docs/guides/import-and-export.mdx(含 zh-Hans / zh-Hant,原文告诉管理员工单的导出授权是空的)一起更正。授权值一个没动。

    守卫「自洽但空转」的判断对了一半:它读的确实是元数据之间的一致性——但它要守的东西本来就不依赖那个能力。真正缺的那格是可达性:对象自己的 enable 会不会把导出路由关掉(apiEnabled: false 或受限 apiMethods → 405,security.canExport 根本轮不到)。PR 补了这条守卫,用平台自己的 resolveEffectiveApiMethods / isApiOperationAllowed 判定(受限 CRUD 白名单其实会展开出 export,手写读 apiMethods 会冤枉四个对象)。把 crm_case 的 enable.apiEnabled 临时改 false 验证方向:1 failed | 29 passed,只有新守卫转红,原三条导出守卫读不到 enable 保持绿。

    留给维护者的一条观察(PR 未动)

    crm_case 在 exportSurfaces 里只由「报表 leg」承载,而报表页实测没有下载入口。授权是活的(走数据 API),但哪天删掉 case 报表,守卫会反过来要求把这条有用的授权一起删掉。注释里已写明;更稳妥是给 Cases 列表视图补一个 exportOptions,但那要动视图元数据,不在本单文件面内。

    另:#797 那段文档确实需要同步,已在 PR #816 内一并更正 —— 但方向不是「报表导出不落地」,而是「那条授权走的是数据 API,报表在这里根本不是导出面」。


    Generated by Claude Code

  5. yinlianghui commented on Aug 5, 2026

    @yinlianghui
    CollaboratorAuthor

    ✅ 验收通过 — PR #816 已转 ready 并挂 auto-merge(CI 9/9 绿)。premise_still_valid: false 的范本处置:测量先行推翻两条推断后,不修想象中的缺口,改修写错的话本身。

    判定要点:

    • 测量证据是决定性的:10 个报表活着(meta 全返、Console 渲染、analytics 取数 200);reports 能力与 report 元数据是两套不相干系统(前者只消费 sys_saved_report)。反向验证(把 reports 加进 requires)后三个报表页逐格数字不变、零次 /api/v1/reports 调用——「加了它不会恢复任何已声明的东西,只会多一套空能力面」这一句是拒改 config 的充分依据。
    • allowExport 半同样被证实为活的:唯一导出路由由 canExport 把守、curl 可达;exportOptions 只是按钮格式声明。三处错误立论(权威注释 / coverage rationale / import-and-export 文档三语——含 docs(guides): import-and-export 按实测改写——导入向导在列表视图而非 Setup → Data,Salesforce 迁移标注未落地 (#763) #797 刚按错误前提写入的那句)一并更正。
    • 真正缺的那格守卫补上了:allowExport 对象必须在 enable 保留 export API,否则 405 在授权检查之前——反向验证只红新守卫、旧三条保持绿,分工正确。
    • 授权值零改动、config 零改动、平台版本零改动。

    两个后续处置:(1) Cases 列表视图补 exportOptions(让活授权有 UI 入口、解除守卫对 report-leg 的单点依赖)→ PM 已立 #817 入队;(2) objectstack dev 静默复用旧 artifact 的启动期陷阱 → 与上游在案的 objectstack#5148(watcher 变体)同族,已按去重纪律在该单追评补充证据,不开新单。#798 随本 PR 关闭。


    Generated by Claude Code

  6. added and removed on Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions