openxiangda-cli 2.0.0-alpha.104 → 2.0.0-alpha.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +3 -0
- package/dist/base.js.map +1 -1
- package/dist/commands/spec.d.ts +34 -0
- package/dist/commands/spec.d.ts.map +1 -0
- package/dist/commands/spec.js +117 -0
- package/dist/commands/spec.js.map +1 -0
- package/package.json +5 -5
- package/template/AGENTS.md +5 -2
- package/template/README.md +5 -1
- package/template/apps/server/package.json +1 -1
- package/template/apps/web/e2e/resource-experience-fixture.tsx +186 -0
- package/template/apps/web/e2e/resources.spec.ts +181 -14
- package/template/apps/web/e2e/workflow.spec.ts +15 -0
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/resource-experience.e2e.html +15 -0
- package/template/apps/web/src/main.tsx +12 -1
- package/template/apps/web/test/contracts.test.ts +2 -0
- package/template/appspec/app.md +36 -0
- package/template/package.json +1 -1
- package/template/packages/contracts/src/generated.ts +2 -0
- package/template/scripts/verify-template-budget.mjs +1 -1
package/dist/base.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../src/base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAS,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,8BAA8B,EAAE,MAAM,yBAAyB,CAAC;AAGzE,eAAO,MAAM,cAAc;;CAE1B,CAAC;AAEF,8BAAsB,kBAAmB,SAAQ,OAAO;IACtD,MAAM,CAAC,cAAc,UAAQ;IAE7B,SAAS,CAAC,QAAQ,CAAC,QAAQ,iCAExB;cAEa,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;IAS1D,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,YAAY,CAAC,OAAO,CAAC;;;;;;;;;;;;;;IAc/C,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,OAAO;;;;;;;;;;;;;;IAsCpC,OAAO,CAAC,QAAQ;IAyBhB,OAAO,CAAC,gBAAgB;
|
|
1
|
+
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../src/base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAS,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,8BAA8B,EAAE,MAAM,yBAAyB,CAAC;AAGzE,eAAO,MAAM,cAAc;;CAE1B,CAAC;AAEF,8BAAsB,kBAAmB,SAAQ,OAAO;IACtD,MAAM,CAAC,cAAc,UAAQ;IAE7B,SAAS,CAAC,QAAQ,CAAC,QAAQ,iCAExB;cAEa,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;IAS1D,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,YAAY,CAAC,OAAO,CAAC;;;;;;;;;;;;;;IAc/C,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,OAAO;;;;;;;;;;;;;;IAsCpC,OAAO,CAAC,QAAQ;IAyBhB,OAAO,CAAC,gBAAgB;CAoCzB"}
|
package/dist/base.js
CHANGED
|
@@ -109,6 +109,7 @@ export class OpenXiangdaCommand extends Command {
|
|
|
109
109
|
stop: "openxiangda stop",
|
|
110
110
|
rollback: "openxiangda rollback --to <app-version-id>",
|
|
111
111
|
login: "openxiangda login --base-url <platform>",
|
|
112
|
+
spec: "openxiangda spec context --json",
|
|
112
113
|
};
|
|
113
114
|
const currentId = this.id;
|
|
114
115
|
if (currentId && currentCommand[currentId])
|
|
@@ -118,6 +119,8 @@ export class OpenXiangdaCommand extends Command {
|
|
|
118
119
|
}
|
|
119
120
|
if (/DEPLOYMENT|STATUS|LOG/.test(code))
|
|
120
121
|
return "openxiangda status";
|
|
122
|
+
if (/APPSPEC/.test(code))
|
|
123
|
+
return "openxiangda spec check";
|
|
121
124
|
if (/DEV/.test(code))
|
|
122
125
|
return "openxiangda dev";
|
|
123
126
|
return "openxiangda check";
|
package/dist/base.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base.js","sourceRoot":"","sources":["../src/base.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"base.js","sourceRoot":"","sources":["../src/base.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { OpenXiangdaCommand } from "../base.js";
|
|
2
|
+
export default class Spec extends OpenXiangdaCommand {
|
|
3
|
+
static summary: string;
|
|
4
|
+
static args: {
|
|
5
|
+
action: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
target: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
title: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
risk: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
summary: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
"current-spec": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
capabilities: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
requirements: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
resources: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
actions: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
cwd: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
};
|
|
19
|
+
run(): Promise<import("openxiangda-contracts").DevkitResult<unknown> | {
|
|
20
|
+
schemaVersion: string;
|
|
21
|
+
ok: boolean;
|
|
22
|
+
operation: string;
|
|
23
|
+
workspace: import("openxiangda-contracts").WorkspaceIdentity;
|
|
24
|
+
data: {} | null;
|
|
25
|
+
error: {
|
|
26
|
+
code: string;
|
|
27
|
+
message: string;
|
|
28
|
+
retryable: boolean;
|
|
29
|
+
remediation: string;
|
|
30
|
+
nextCommand: string | null;
|
|
31
|
+
} | null;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=spec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec.d.ts","sourceRoot":"","sources":["../../src/commands/spec.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAkB,MAAM,YAAY,CAAC;AAWhE,MAAM,CAAC,OAAO,OAAO,IAAK,SAAQ,kBAAkB;IAClD,MAAM,CAAC,OAAO,SAAiC;IAC/C,MAAM,CAAC,IAAI;;;MAMT;IACF,MAAM,CAAC,KAAK;;;;;;;;;;MAiCV;IAEI,GAAG;;;;;;;;;;;;;;CA4DV"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Args, Flags } from "@oclif/core";
|
|
2
|
+
import { OpenXiangdaCommand, workspaceFlags } from "../base.js";
|
|
3
|
+
const actions = [
|
|
4
|
+
"init",
|
|
5
|
+
"add-capability",
|
|
6
|
+
"new",
|
|
7
|
+
"context",
|
|
8
|
+
"check",
|
|
9
|
+
"close",
|
|
10
|
+
];
|
|
11
|
+
export default class Spec extends OpenXiangdaCommand {
|
|
12
|
+
static summary = "可选的 AppSpec 需求与变更辅助;不属于发布门禁";
|
|
13
|
+
static args = {
|
|
14
|
+
action: Args.string({ required: true, options: [...actions] }),
|
|
15
|
+
target: Args.string({
|
|
16
|
+
required: false,
|
|
17
|
+
summary: "capability/change id,或 context selector",
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
static flags = {
|
|
21
|
+
...workspaceFlags,
|
|
22
|
+
title: Flags.string({ summary: "能力或变更标题", helpValue: "<title>" }),
|
|
23
|
+
risk: Flags.string({
|
|
24
|
+
summary: "变更维护深度;默认 L1",
|
|
25
|
+
options: ["L1", "L2", "L3"],
|
|
26
|
+
helpValue: "<L1|L2|L3>",
|
|
27
|
+
}),
|
|
28
|
+
summary: Flags.string({
|
|
29
|
+
summary: "问题摘要或关闭摘要",
|
|
30
|
+
helpValue: "<text>",
|
|
31
|
+
}),
|
|
32
|
+
"current-spec": Flags.string({
|
|
33
|
+
summary: "关闭时由用户确认当前规格已合并或不适用",
|
|
34
|
+
options: ["merged", "not-applicable"],
|
|
35
|
+
helpValue: "<merged|not-applicable>",
|
|
36
|
+
}),
|
|
37
|
+
capabilities: Flags.string({
|
|
38
|
+
summary: "逗号分隔的 CAP-* 引用",
|
|
39
|
+
helpValue: "<codes>",
|
|
40
|
+
}),
|
|
41
|
+
requirements: Flags.string({
|
|
42
|
+
summary: "逗号分隔的 REQ-* 引用",
|
|
43
|
+
helpValue: "<codes>",
|
|
44
|
+
}),
|
|
45
|
+
resources: Flags.string({
|
|
46
|
+
summary: "逗号分隔的 2.0 Data Resource code",
|
|
47
|
+
helpValue: "<codes>",
|
|
48
|
+
}),
|
|
49
|
+
actions: Flags.string({
|
|
50
|
+
summary: "逗号分隔的 2.0 App API action code",
|
|
51
|
+
helpValue: "<codes>",
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
async run() {
|
|
55
|
+
const { args, flags } = await this.parse(Spec);
|
|
56
|
+
const action = args.action;
|
|
57
|
+
if (action === "init")
|
|
58
|
+
return this.present(await this.services.appSpecInit(flags.cwd));
|
|
59
|
+
if (action === "context") {
|
|
60
|
+
return this.present(await this.services.appSpecContext(flags.cwd, args.target));
|
|
61
|
+
}
|
|
62
|
+
if (action === "check") {
|
|
63
|
+
return this.present(await this.services.appSpecCheck(flags.cwd));
|
|
64
|
+
}
|
|
65
|
+
const id = required(args.target, `APPSPEC_${action.toUpperCase().replaceAll("-", "_")}_ID_REQUIRED`);
|
|
66
|
+
if (action === "add-capability") {
|
|
67
|
+
return this.present(await this.services.appSpecAddCapability({
|
|
68
|
+
...(flags.cwd ? { root: flags.cwd } : {}),
|
|
69
|
+
id,
|
|
70
|
+
title: required(flags.title, "APPSPEC_TITLE_REQUIRED"),
|
|
71
|
+
resources: csv(flags.resources),
|
|
72
|
+
actions: csv(flags.actions),
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
if (action === "new") {
|
|
76
|
+
return this.present(await this.services.appSpecNew({
|
|
77
|
+
...(flags.cwd ? { root: flags.cwd } : {}),
|
|
78
|
+
id,
|
|
79
|
+
title: required(flags.title, "APPSPEC_TITLE_REQUIRED"),
|
|
80
|
+
...(flags.risk ? { risk: flags.risk } : {}),
|
|
81
|
+
...(flags.summary ? { summary: flags.summary } : {}),
|
|
82
|
+
capabilities: csv(flags.capabilities),
|
|
83
|
+
requirements: csv(flags.requirements),
|
|
84
|
+
resources: csv(flags.resources),
|
|
85
|
+
actions: csv(flags.actions),
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
if (action === "close") {
|
|
89
|
+
return this.present(await this.services.appSpecClose({
|
|
90
|
+
...(flags.cwd ? { root: flags.cwd } : {}),
|
|
91
|
+
id,
|
|
92
|
+
...(flags.summary ? { summary: flags.summary } : {}),
|
|
93
|
+
...(flags["current-spec"]
|
|
94
|
+
? {
|
|
95
|
+
currentSpec: flags["current-spec"],
|
|
96
|
+
}
|
|
97
|
+
: {}),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`APPSPEC_ACTION_UNSUPPORTED: ${action}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function csv(value) {
|
|
104
|
+
return [
|
|
105
|
+
...new Set(String(value || "")
|
|
106
|
+
.split(",")
|
|
107
|
+
.map(item => item.trim())
|
|
108
|
+
.filter(Boolean)),
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
function required(value, code) {
|
|
112
|
+
const normalized = String(value || "").trim();
|
|
113
|
+
if (!normalized)
|
|
114
|
+
throw new Error(code);
|
|
115
|
+
return normalized;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec.js","sourceRoot":"","sources":["../../src/commands/spec.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openxiangda-cli",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.108",
|
|
4
4
|
"description": "Thin application-level CLI for OpenXiangda 2.0.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@oclif/core": "4.13.3",
|
|
24
|
-
"openxiangda-contracts": "2.0.0-alpha.
|
|
25
|
-
"openxiangda-devkit-core": "2.0.0-alpha.
|
|
26
|
-
"openxiangda-mcp": "2.0.0-alpha.
|
|
27
|
-
"openxiangda-skill-kit": "2.0.0-alpha.
|
|
24
|
+
"openxiangda-contracts": "2.0.0-alpha.50",
|
|
25
|
+
"openxiangda-devkit-core": "2.0.0-alpha.67",
|
|
26
|
+
"openxiangda-mcp": "2.0.0-alpha.67",
|
|
27
|
+
"openxiangda-skill-kit": "2.0.0-alpha.85"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"tsx": "4.23.12",
|
package/template/AGENTS.md
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
# OpenXiangda 2.0 Application Agent Contract
|
|
2
2
|
|
|
3
3
|
- Use only the workspace-pinned CLI: `pnpm openxiangda <command>`. Never invoke a bare global `openxiangda` inside a 2.0 task.
|
|
4
|
+
- `appspec/` is the optional OpenXiangda 2.0 business-intent layer. Before changing observable behavior, read the bounded index with `pnpm openxiangda spec context --json`, then select only the relevant stable ID; use no ChangeSpec for L0 work, one short ChangeSpec for L1, and add permission/rollback/concurrency detail only for L2/L3. Before close, the user—not AI—confirms `currentSpec=merged` or `not-applicable`. AppSpec is advisory and never a release gate. Never read, import or migrate 1.x `openspec/` or SDD.
|
|
4
5
|
- Use Vite, React Router, Refine Core and Ant Design. Do not add Umi, ProComponents or another admin shell.
|
|
6
|
+
- Keep generated admin routes and custom actions inside the platform Shell. Bind generated `appRoutes` to local operation pages with `defineAdminContributions`, and use only its typed `toolbar`, `row` and `detail` resource slots. Do not create another router, menu, layout, identity provider, permission store or copied CRUD page; route/action access uses capability or `allOf`/`anyOf`, while Data/App API authorization remains server-owned.
|
|
5
7
|
- Declare each resource once in `openxiangda.config.ts` with only `code`, `name`, `fields` and optional list/layout/data-policy settings. Each field owns type, label, required state, Surface flags, reference/file metadata and access. Resource codes are lower kebab-case.
|
|
6
8
|
- Never write resource-level `schemaVersion`, `appCode`, `schema`, `surface`, `capabilities`, `fieldPolicies` or `platform/data` modules. The compiler derives the strict DataResource, CRUD capabilities, Surface and AI Schema. The application manifest still starts with its one top-level `schemaVersion: 3`.
|
|
7
9
|
- Ordinary list/get/create/update/delete, filters, export and batch operations use the platform Native Data API. Do not create Function CRUD or NestJS wrappers.
|
|
8
10
|
- Add NestJS only for a named business action that needs a cross-resource transaction, an invariant or an external side effect.
|
|
9
11
|
- Every interactive business action binds `@OpenXiangdaOperation(operation)` and injects `OpenXiangdaBusinessDataApiService` or `OpenXiangdaStandardOperations`. The platform checks the action capability once at App API ingress; the trusted backend then has full Data access only to its exact application/environment while audit retains the initiating user. Do not author `authorizationJSON` or reapply the user's resource, row and field permissions inside the action.
|
|
10
12
|
- A NestJS backend declares only `enabled`, `isolation: 'shared' | 'dedicated'` and `resourceProfile: 'light' | 'standard'`. Never put raw Kubernetes resources, replicas, ports or environment maps in application metadata; the platform owns capacity and scaling.
|
|
11
|
-
- Use the current logged-in user and the union of that user's application roles.
|
|
13
|
+
- Use the current logged-in user and the union of that user's application roles. An optional declared Perspective projects only read visibility (pages, rows and fields); create/update/delete, workflows and custom actions continue to authorize against the complete union. Standard Data API calls inherit `X-OpenXiangda-Perspective` automatically. Use `@CurrentPerspective()` only when custom Nest code reads outside the standard Data API, and apply an equivalent read projection explicitly. Business code must not persist a platform Token or authorization result and must not implement a second identity path.
|
|
12
14
|
- Fields inherit the resource read/create/update capabilities. Use field `access` only to tighten them; arrays are all-of and `false` is explicit deny. There is no `write` fallback.
|
|
13
15
|
- Field `type` is semantic, never a hand-authored database type. The supported catalog is exactly `text.short`, `text.long`, `text.rich`, `number.integer`, `number.decimal`, `boolean`, `date`, `time`, `datetime`, `date-range`, `datetime-range`, `option.single`, `option.multiple`, `cascade.single`, `cascade.multiple`, `user.single`, `user.multiple`, `department.single`, `department.multiple`, `resource-ref.single`, `resource-ref.multiple`, `file`, `image`, `signature`, `address`, `location`, `json`, `serial-number`, `uuid` and `subtable`. The compiler alone derives PostgreSQL columns, constraints and indexes.
|
|
14
16
|
- Declare every custom operation capability in `authz.capabilities` with `kind: 'backend'`, then reference that same code from the operation and its allowed roles. Generated resource CRUD capabilities do not go in this catalog.
|
|
17
|
+
- Build App Operation request/response schemas with `resourceRecordSchema`, `schemaRef`, `composeJsonSchema` and `composeAppOperationSchemas` from `openxiangda/config`. They project the same Resource declaration and canonical field value protocols; never import a physical toolchain package or repeat user, department, resource-reference or managed-file schemas by hand.
|
|
15
18
|
- Visitor duplicate protection uses `createVisitorReservation({ duplicateMatch: { fieldCode: submittedValue }, ... })`. `duplicateMatch` is a non-empty value map, never a field-name array, and no mutable pre-read is allowed.
|
|
16
19
|
- Desktop and mobile pages share values, validation and authorization, but use separate renderers. Options, members and departments store display snapshots; resource references store direct JSON display values; attachments, images and signatures use platform-managed file references.
|
|
17
20
|
- `option.*`, `user.*`, `department.*` and `resource-ref.*` values never collapse to scalar IDs. Single values store one labeled object and multiple values store object arrays. `resource-ref.*` JSON is not a foreign key, trusted target snapshot or automatically refreshed copy; current target state is read by `resourceCode` plus `value`. A resource source `labelField` must be `text.short` or `text.long`; `serial-number` is allowed in source search, description and snapshot fields, but not as the label. `location` accepts only exact WGS84 coordinates captured by DingTalk or browser geolocation; it has no manual input or `manual` source. Roles that consume directory-backed fields explicitly include `app:<app-code>:directory:read`.
|
|
@@ -19,4 +22,4 @@
|
|
|
19
22
|
- Do not add compatibility aliases, migration branches or silent fallbacks for an earlier 2.0 alpha contract. Replace an incorrect contract and regenerate the application.
|
|
20
23
|
- Standard Workflow and Notification Hub are optional 2.0 modules. Enable them only through the canonical `openxiangda.config.ts` declarations and generated clients; never copy 1.x workflow/message code, tables, APIs, templates or callbacks into this workspace. Keep ordinary CRUD and application-specific state machines independent of them.
|
|
21
24
|
- Run `pnpm openxiangda check --json` after contract changes and read `data.sealedArtifact`; check never seals, and an older `.openxiangda/build/app-package.json` is not the current check result. Use `pnpm openxiangda accept --plan <file>` only for optional real preproduction identity acceptance; it never blocks delivery. Deploy with `pnpm openxiangda deploy`, inspect with `pnpm openxiangda status` and `pnpm openxiangda logs`, and use the platform rollback command rather than mutating K3s directly.
|
|
22
|
-
- AI-native clients start the workspace protocol through the same pinned binary: `pnpm exec openxiangda --mcp-stdio --cwd <workspace>`. Read `openxiangda://workspace/contracts` or call `contract_describe
|
|
25
|
+
- AI-native clients start the workspace protocol through the same pinned binary: `pnpm exec openxiangda --mcp-stdio --cwd <workspace>`. Read `openxiangda://workspace/contracts` or call `contract_describe`; when AppSpec is enabled, also read `openxiangda://workspace/appspec` or call `appspec_context`. Require `aiCatalog` and `aiCatalogDigest` to match the normal compiler output. This is a stdio transport mode, not another application-owned server. Never write an application MCP server, Catalog file, preview store or second authorization path.
|
package/template/README.md
CHANGED
|
@@ -8,13 +8,17 @@ pnpm openxiangda dev
|
|
|
8
8
|
|
|
9
9
|
当前应用只直接依赖一个精确版本的 `openxiangda` 根包,并在 lockfile 中锁定其内部实现包。创建另一个应用时使用当前安装的 OpenXiangda 2.0 Skill 给出的精确根包版本,不使用 `latest`、`alpha` 或全局命令。
|
|
10
10
|
|
|
11
|
+
`appspec/app.md` 是可选的业务意图总纲,帮助人和 AI 理解当前需求,但不是发布门禁。没有行为变化
|
|
12
|
+
时不写变更;小改动最多维护一份短 ChangeSpec。按需使用
|
|
13
|
+
`pnpm openxiangda spec context --json`、`spec new` 和 `spec close`,不要引入 1.x `openspec/SDD`。
|
|
14
|
+
|
|
11
15
|
在 `openxiangda.config.ts` 的 `data.resources` 中只声明资源 `code`、`name`、`fields` 和可选列表/布局/数据策略。每个字段一次写完语义 `type`、`label`、`required`、列表/筛选标记、`options`/`source`/`file` 与 `access`;不要再写 `schema`、`surface`、PostgreSQL 类型、资源 capabilities、资源 fieldPolicies 或 `platform/data` 模块。编译器据此生成标准后台列表、筛选、表单、详情、审计、文件上传、独立移动端页面、Data API 查询计划和 AI Schema。
|
|
12
16
|
|
|
13
17
|
成员、部门分别声明 `user.single`/`user.multiple` 和 `department.single`/`department.multiple`;同应用动态资源声明 `resource-ref.single`/`resource-ref.multiple` 及编译期固定的 `source`。这些字段都保存完整 `{label,value,...}` 显示快照,资源引用还保存 `resourceCode` 和声明的 `snapshot` 字段;查询和权限以稳定 `value` 为比较键。资源来源的 `labelField` 只能引用 `text.short` 或 `text.long`;流水号可用于搜索、描述和快照,不能作为显示标签。需要目录选择器的业务角色必须显式拥有 `app:<app-code>:directory:read` 能力。用 `resourceCapabilityCodes` 生成资源 CRUD 权限,用 `currentUserDataPolicy` 声明唯一的当前用户行规则。
|
|
14
18
|
|
|
15
19
|
字段组件、CRUD Renderer、Shell、Runtime 与 Data/Directory/File/App Client 都由 `openxiangda/react`、`openxiangda/field-kit` 和 `openxiangda/core` 统一提供,应用模板不复制这些实现。Nest 只在需要复杂事务或外部集成时增加 App API。标准客户端显式携带当前环境、使用同源 Cookie;业务代码不读取或持久化 Token、平台内部身份标识与授权结果。
|
|
16
20
|
|
|
17
|
-
前端一次读取当前登录用户在当前环境的完整应用角色并集和 capability
|
|
21
|
+
前端一次读取当前登录用户在当前环境的完整应用角色并集和 capability。应用可在 `perspectives` 中声明工作视角;视角只投影页面、行和字段的读取范围,不改变真实身份,也不收窄新增、编辑、删除、流程与自定义动作权限。标准 Data API 自动识别视角;只有绕过标准 Data API 的自定义 Nest 查询才需要通过 `@CurrentPerspective()` 显式实现同等读取投影。字段默认继承资源 read/create/update;`access` 数组为 all-of,`false` 为明确拒绝,无权字段在提交前从 payload 删除。
|
|
18
22
|
|
|
19
23
|
```bash
|
|
20
24
|
pnpm openxiangda check
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom/client';
|
|
3
|
+
import { Button, Typography } from 'antd';
|
|
4
|
+
import {
|
|
5
|
+
defineAdminContributions,
|
|
6
|
+
OpenXiangdaApplication,
|
|
7
|
+
} from 'openxiangda/react';
|
|
8
|
+
import type { DataFieldSurface, DataResourceSurface } from 'openxiangda/core';
|
|
9
|
+
import 'openxiangda/react/styles.css';
|
|
10
|
+
|
|
11
|
+
const field = (
|
|
12
|
+
label: string,
|
|
13
|
+
type: DataFieldSurface['type'] = 'text.short',
|
|
14
|
+
widget: DataFieldSurface['widget'] = 'text',
|
|
15
|
+
extra: Partial<DataFieldSurface> = {}
|
|
16
|
+
): DataFieldSurface => ({
|
|
17
|
+
label,
|
|
18
|
+
type,
|
|
19
|
+
widget,
|
|
20
|
+
readCapabilities: [],
|
|
21
|
+
createCapabilities: [],
|
|
22
|
+
updateCapabilities: [],
|
|
23
|
+
...extra,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const fields: DataResourceSurface['fields'] = {
|
|
27
|
+
description: field('栏目说明', 'text.long', 'textarea', {
|
|
28
|
+
section: '栏目设置',
|
|
29
|
+
}),
|
|
30
|
+
status: field('状态', 'option.single', 'select', {
|
|
31
|
+
options: [
|
|
32
|
+
{ label: '启用', value: 'enabled' },
|
|
33
|
+
{ label: '停用', value: 'disabled' },
|
|
34
|
+
],
|
|
35
|
+
section: '栏目设置',
|
|
36
|
+
}),
|
|
37
|
+
name: field('栏目名称', 'text.short', 'text', { section: '栏目设置' }),
|
|
38
|
+
code: field('栏目编码', 'text.short', 'text', { section: '栏目设置' }),
|
|
39
|
+
contentType: field('内容类型', 'option.single', 'select', {
|
|
40
|
+
options: [
|
|
41
|
+
{ label: '文章', value: 'article' },
|
|
42
|
+
{ label: '图集', value: 'gallery' },
|
|
43
|
+
],
|
|
44
|
+
section: '栏目设置',
|
|
45
|
+
}),
|
|
46
|
+
manager: field('栏目管理员', 'user.single', 'directory-user', {
|
|
47
|
+
section: '管理信息',
|
|
48
|
+
}),
|
|
49
|
+
facility: field('配套设施', 'option.multiple', 'multi-select', {
|
|
50
|
+
options: [
|
|
51
|
+
{ label: '投影仪', value: 'projector' },
|
|
52
|
+
{ label: '白板', value: 'whiteboard' },
|
|
53
|
+
],
|
|
54
|
+
section: '管理信息',
|
|
55
|
+
}),
|
|
56
|
+
openingDay: field('开放星期', 'option.single', 'select', {
|
|
57
|
+
options: [
|
|
58
|
+
{ label: '工作日', value: 'weekday' },
|
|
59
|
+
{ label: '周末', value: 'weekend' },
|
|
60
|
+
],
|
|
61
|
+
section: '管理信息',
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const surface: DataResourceSurface = {
|
|
66
|
+
generated: true,
|
|
67
|
+
fields,
|
|
68
|
+
list: {
|
|
69
|
+
defaultPageSize: 20,
|
|
70
|
+
searchableFields: ['name', 'code'],
|
|
71
|
+
filterFields: ['contentType', 'status', 'manager', 'facility', 'openingDay'],
|
|
72
|
+
defaultSort: { field: 'code', order: 'asc' },
|
|
73
|
+
},
|
|
74
|
+
form: {
|
|
75
|
+
layout: 'sections',
|
|
76
|
+
fieldOrder: [
|
|
77
|
+
'code',
|
|
78
|
+
'name',
|
|
79
|
+
'contentType',
|
|
80
|
+
'description',
|
|
81
|
+
'status',
|
|
82
|
+
'manager',
|
|
83
|
+
'facility',
|
|
84
|
+
'openingDay',
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
detail: {
|
|
88
|
+
layout: 'sections',
|
|
89
|
+
fieldOrder: [
|
|
90
|
+
'code',
|
|
91
|
+
'name',
|
|
92
|
+
'contentType',
|
|
93
|
+
'description',
|
|
94
|
+
'status',
|
|
95
|
+
'manager',
|
|
96
|
+
'facility',
|
|
97
|
+
'openingDay',
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
mobile: { enabled: true },
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const names = [
|
|
104
|
+
'内容栏目',
|
|
105
|
+
'内容文章',
|
|
106
|
+
'内容专题',
|
|
107
|
+
'会员档案',
|
|
108
|
+
'门户轮播',
|
|
109
|
+
'工会小组',
|
|
110
|
+
'场地预约',
|
|
111
|
+
'场地不可用时段',
|
|
112
|
+
'场地档案',
|
|
113
|
+
'通知公告',
|
|
114
|
+
'活动报名',
|
|
115
|
+
'资料下载',
|
|
116
|
+
];
|
|
117
|
+
const resourceDefinitions = Object.fromEntries(
|
|
118
|
+
names.map((name, index) => {
|
|
119
|
+
const code = `resource-${String(index + 1).padStart(2, '0')}`;
|
|
120
|
+
return [
|
|
121
|
+
code,
|
|
122
|
+
{
|
|
123
|
+
code,
|
|
124
|
+
name,
|
|
125
|
+
capabilities: {
|
|
126
|
+
read: `app:openxiangda-application:data:${code}:read`,
|
|
127
|
+
create: `app:openxiangda-application:data:${code}:create`,
|
|
128
|
+
update: `app:openxiangda-application:data:${code}:update`,
|
|
129
|
+
delete: `app:openxiangda-application:data:${code}:delete`,
|
|
130
|
+
},
|
|
131
|
+
surface,
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
function ContentOperationsPage() {
|
|
138
|
+
return <Typography.Title level={2}>内容业务操作</Typography.Title>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const adminContributions = defineAdminContributions(
|
|
142
|
+
{
|
|
143
|
+
contentOperations: {
|
|
144
|
+
code: 'content-operations',
|
|
145
|
+
path: '/admin/operations/content',
|
|
146
|
+
label: '内容业务操作',
|
|
147
|
+
surface: 'admin',
|
|
148
|
+
navigation: 'menu',
|
|
149
|
+
capability: 'app:openxiangda-application:operations:read',
|
|
150
|
+
tabPersistence: 'session',
|
|
151
|
+
keepAlive: 'none',
|
|
152
|
+
},
|
|
153
|
+
} as const,
|
|
154
|
+
{
|
|
155
|
+
pages: { contentOperations: ContentOperationsPage },
|
|
156
|
+
resources: {
|
|
157
|
+
'resource-01': {
|
|
158
|
+
toolbar: [
|
|
159
|
+
{
|
|
160
|
+
code: 'publish-selected',
|
|
161
|
+
label: '发布选中栏目',
|
|
162
|
+
capability: 'app:openxiangda-application:operations:read',
|
|
163
|
+
render: ({ selectedRecords }) => (
|
|
164
|
+
<Button>发布选中栏目({selectedRecords.length})</Button>
|
|
165
|
+
),
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (location.pathname.endsWith('/resource-experience.e2e.html')) {
|
|
174
|
+
history.replaceState({}, '', '/resource-01');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
178
|
+
<React.StrictMode>
|
|
179
|
+
<OpenXiangdaApplication
|
|
180
|
+
appCode="openxiangda-application"
|
|
181
|
+
appName="资源体验验收"
|
|
182
|
+
contributions={adminContributions}
|
|
183
|
+
resourceDefinitions={resourceDefinitions}
|
|
184
|
+
/>
|
|
185
|
+
</React.StrictMode>
|
|
186
|
+
);
|
|
@@ -21,7 +21,7 @@ const definitions = resourceDefinitions as Record<string, DeclaredResource>;
|
|
|
21
21
|
const allCapabilities = [...capabilities] as string[];
|
|
22
22
|
const runtimeBase = `/view/${appCode}`;
|
|
23
23
|
|
|
24
|
-
function runtimeAuthorization(authorized: boolean) {
|
|
24
|
+
function runtimeAuthorization(authorized: boolean, superAdmin = false) {
|
|
25
25
|
return {
|
|
26
26
|
schemaVersion: 'openxiangda.runtime-authorization/v2',
|
|
27
27
|
state: 'active',
|
|
@@ -34,12 +34,24 @@ function runtimeAuthorization(authorized: boolean) {
|
|
|
34
34
|
authzVersion: 1,
|
|
35
35
|
scopeDataVersion: 'scope-1',
|
|
36
36
|
},
|
|
37
|
+
subjectProfile: {
|
|
38
|
+
schemaVersion: 'openxiangda.subject-profile/v2',
|
|
39
|
+
userId: 'acceptance-user',
|
|
40
|
+
displayName: '王老师',
|
|
41
|
+
avatarUrl: null,
|
|
42
|
+
jobNumber: 'T001',
|
|
43
|
+
affiliatedDepartment: { id: 'department-1', name: '理学院' },
|
|
44
|
+
},
|
|
45
|
+
roles: [
|
|
46
|
+
{ code: 'college_admin', name: '学院管理员', source: 'package' },
|
|
47
|
+
{ code: 'instrument_admin', name: '仪器管理员', source: 'package' },
|
|
48
|
+
],
|
|
37
49
|
principal: {
|
|
38
50
|
type: 'user_union',
|
|
39
51
|
userId: 'acceptance-user',
|
|
40
52
|
roleCodes: ['college_admin', 'instrument_admin'],
|
|
41
53
|
capabilityCodes: authorized ? allCapabilities : [],
|
|
42
|
-
isAppSuperAdmin:
|
|
54
|
+
isAppSuperAdmin: superAdmin,
|
|
43
55
|
identityScope: 'user-union:preproduction-id:acceptance-user:authz-1',
|
|
44
56
|
},
|
|
45
57
|
};
|
|
@@ -49,9 +61,14 @@ async function mockPlatform(
|
|
|
49
61
|
page: Page,
|
|
50
62
|
authorized = true,
|
|
51
63
|
targetRuntimeBase = runtimeBase,
|
|
64
|
+
superAdmin = false,
|
|
52
65
|
) {
|
|
53
66
|
let contextReads = 0;
|
|
54
|
-
|
|
67
|
+
let avatarCompletes = 0;
|
|
68
|
+
const observed: Array<{ path: string; perspectiveCode: string }> = [];
|
|
69
|
+
await page.route('https://upload.example.test/avatar-upload', route =>
|
|
70
|
+
route.fulfill({ status: 200, body: '' })
|
|
71
|
+
);
|
|
55
72
|
await page.route(`**${targetRuntimeBase}/**`, async route => {
|
|
56
73
|
if (route.request().resourceType() !== 'document') return route.continue();
|
|
57
74
|
const response = await route.fetch();
|
|
@@ -66,14 +83,45 @@ async function mockPlatform(
|
|
|
66
83
|
await page.route('**/service/**', async route => {
|
|
67
84
|
const url = new URL(route.request().url());
|
|
68
85
|
const request = route.request();
|
|
69
|
-
const
|
|
86
|
+
const perspectiveCode = request.headers()['x-openxiangda-perspective'] || '';
|
|
70
87
|
if (url.pathname.endsWith('/native/authz/current')) {
|
|
71
88
|
contextReads += 1;
|
|
72
89
|
return route.fulfill({
|
|
73
90
|
contentType: 'application/json',
|
|
74
91
|
body: JSON.stringify({
|
|
75
92
|
code: 200,
|
|
76
|
-
data: runtimeAuthorization(authorized),
|
|
93
|
+
data: runtimeAuthorization(authorized, superAdmin),
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (url.pathname.endsWith('/native/authz/profile/avatar/initiate')) {
|
|
98
|
+
return route.fulfill({
|
|
99
|
+
contentType: 'application/json',
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
code: 200,
|
|
102
|
+
data: {
|
|
103
|
+
uploadUrl: 'https://upload.example.test/avatar-upload',
|
|
104
|
+
uploadMethod: 'put',
|
|
105
|
+
headers: { 'content-type': 'image/png' },
|
|
106
|
+
objectName: 'images/tenant/app/user/avatar.png',
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (url.pathname.endsWith('/native/authz/profile/avatar/complete')) {
|
|
112
|
+
avatarCompletes += 1;
|
|
113
|
+
return route.fulfill({
|
|
114
|
+
contentType: 'application/json',
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
code: 200,
|
|
117
|
+
data: {
|
|
118
|
+
schemaVersion: 'openxiangda.subject-profile/v2',
|
|
119
|
+
userId: 'acceptance-user',
|
|
120
|
+
displayName: '王老师',
|
|
121
|
+
avatarUrl: 'https://cdn.example.test/avatar/new.png',
|
|
122
|
+
jobNumber: 'T001',
|
|
123
|
+
affiliatedDepartment: { id: 'department-1', name: '理学院' },
|
|
124
|
+
},
|
|
77
125
|
}),
|
|
78
126
|
});
|
|
79
127
|
}
|
|
@@ -81,7 +129,7 @@ async function mockPlatform(
|
|
|
81
129
|
/\/native\/data-resources\/([^/]+)\/fields\/([^/]+)\/source\/query$/
|
|
82
130
|
);
|
|
83
131
|
if (sourceQuery) {
|
|
84
|
-
observed.push({ path: url.pathname,
|
|
132
|
+
observed.push({ path: url.pathname, perspectiveCode });
|
|
85
133
|
return route.fulfill({
|
|
86
134
|
contentType: 'application/json',
|
|
87
135
|
body: JSON.stringify({
|
|
@@ -100,7 +148,7 @@ async function mockPlatform(
|
|
|
100
148
|
/\/native\/data\/([^/]+)\/export$/
|
|
101
149
|
);
|
|
102
150
|
if (exportRequest) {
|
|
103
|
-
observed.push({ path: url.pathname,
|
|
151
|
+
observed.push({ path: url.pathname, perspectiveCode });
|
|
104
152
|
return route.fulfill({
|
|
105
153
|
status: 200,
|
|
106
154
|
contentType: 'text/csv; charset=utf-8',
|
|
@@ -114,7 +162,7 @@ async function mockPlatform(
|
|
|
114
162
|
}
|
|
115
163
|
const query = url.pathname.match(/\/native\/data\/([^/]+)\/query$/);
|
|
116
164
|
if (query) {
|
|
117
|
-
observed.push({ path: url.pathname,
|
|
165
|
+
observed.push({ path: url.pathname, perspectiveCode });
|
|
118
166
|
return route.fulfill({
|
|
119
167
|
contentType: 'application/json',
|
|
120
168
|
body: JSON.stringify({
|
|
@@ -133,14 +181,14 @@ async function mockPlatform(
|
|
|
133
181
|
});
|
|
134
182
|
}
|
|
135
183
|
if (url.pathname.includes('/directory/')) {
|
|
136
|
-
observed.push({ path: url.pathname,
|
|
184
|
+
observed.push({ path: url.pathname, perspectiveCode });
|
|
137
185
|
return route.fulfill({
|
|
138
186
|
contentType: 'application/json',
|
|
139
187
|
body: JSON.stringify({ code: 200, data: { items: [], nextCursor: null } }),
|
|
140
188
|
});
|
|
141
189
|
}
|
|
142
190
|
if (url.pathname.includes('/openxiangda-app-api/v2/')) {
|
|
143
|
-
observed.push({ path: url.pathname,
|
|
191
|
+
observed.push({ path: url.pathname, perspectiveCode });
|
|
144
192
|
return route.fulfill({
|
|
145
193
|
contentType: 'application/json',
|
|
146
194
|
body: JSON.stringify({ code: 200, data: { status: 'ok' } }),
|
|
@@ -157,6 +205,9 @@ async function mockPlatform(
|
|
|
157
205
|
get contextReads() {
|
|
158
206
|
return contextReads;
|
|
159
207
|
},
|
|
208
|
+
get avatarCompletes() {
|
|
209
|
+
return avatarCompletes;
|
|
210
|
+
},
|
|
160
211
|
};
|
|
161
212
|
}
|
|
162
213
|
|
|
@@ -179,7 +230,13 @@ test('renders the compiled desktop resource contract and shared workbench', asyn
|
|
|
179
230
|
const more = page.getByRole('button', { name: /更多筛选/ });
|
|
180
231
|
await expect(more).toBeVisible();
|
|
181
232
|
await more.click();
|
|
182
|
-
|
|
233
|
+
const modal = page.getByRole('dialog', { name: '更多筛选' });
|
|
234
|
+
await expect(modal).toBeVisible();
|
|
235
|
+
await expect(modal.getByRole('button', { name: '应用筛选' })).toBeVisible();
|
|
236
|
+
await expect
|
|
237
|
+
.poll(async () => (await modal.boundingBox())?.width || 0)
|
|
238
|
+
.toBeGreaterThan(700);
|
|
239
|
+
await modal.getByRole('button', { name: '取消' }).click();
|
|
183
240
|
}
|
|
184
241
|
const downloadPromise = page.waitForEvent('download');
|
|
185
242
|
await page.getByRole('button', { name: /导出$/ }).click();
|
|
@@ -190,6 +247,10 @@ test('renders the compiled desktop resource contract and shared workbench', asyn
|
|
|
190
247
|
await expect(page.locator('.oxa-topbar')).toHaveCSS('height', '52px');
|
|
191
248
|
await expect(page.getByText('仪器' + '资源管理')).toHaveCount(0);
|
|
192
249
|
await page.locator('.oxa-current-user').click();
|
|
250
|
+
await expect(page.locator('.oxa-user-dropdown-profile')).toContainText('王老师');
|
|
251
|
+
await expect(page.locator('.oxa-user-dropdown-profile')).toContainText('理学院');
|
|
252
|
+
await expect(page.locator('.oxa-user-dropdown-roles')).toContainText('学院管理员');
|
|
253
|
+
await expect(page.locator('.oxa-user-dropdown-roles')).not.toContainText('college_admin');
|
|
193
254
|
await expect(page.getByText('跟随系统', { exact: true })).toBeVisible();
|
|
194
255
|
await page.getByText('深色', { exact: true }).click();
|
|
195
256
|
await expect(page.locator('html')).toHaveAttribute('data-oxa-theme', 'dark');
|
|
@@ -205,7 +266,7 @@ test('opens the stable admin entry under the preproduction gateway mount', async
|
|
|
205
266
|
await expect(page).toHaveURL(`${devRuntimeBase}/${codes[0]}`);
|
|
206
267
|
});
|
|
207
268
|
|
|
208
|
-
test('uses the current
|
|
269
|
+
test('uses the complete current-user role union when no Perspective is selected', async ({ page }) => {
|
|
209
270
|
const platform = await mockPlatform(page);
|
|
210
271
|
await page.goto(`${runtimeBase}/admin`);
|
|
211
272
|
if (codes.length) {
|
|
@@ -229,15 +290,121 @@ test('uses the current user role union without exposing a RoleSession header', a
|
|
|
229
290
|
)
|
|
230
291
|
);
|
|
231
292
|
expect(protectedRequests.length).toBeGreaterThanOrEqual(3);
|
|
232
|
-
expect(protectedRequests.every(item => item.
|
|
293
|
+
expect(protectedRequests.every(item => item.perspectiveCode === '')).toBe(true);
|
|
233
294
|
expect(platform.contextReads).toBe(1);
|
|
234
295
|
await page.locator('.oxa-current-user').click();
|
|
235
296
|
await expect(page.locator('.oxa-user-dropdown-roles')).toContainText(
|
|
236
|
-
'
|
|
297
|
+
'学院管理员'
|
|
298
|
+
);
|
|
299
|
+
await expect(page.locator('.oxa-user-dropdown-roles')).toContainText(
|
|
300
|
+
'仪器管理员'
|
|
301
|
+
);
|
|
302
|
+
await expect(page.locator('.oxa-user-dropdown-roles')).not.toContainText(
|
|
303
|
+
'college_admin'
|
|
237
304
|
);
|
|
238
305
|
await expect(page.getByText('切换应用角色', { exact: true })).toHaveCount(0);
|
|
239
306
|
});
|
|
240
307
|
|
|
308
|
+
test('updates the current user avatar through the platform-owned profile contract', async ({ page }) => {
|
|
309
|
+
const platform = await mockPlatform(page);
|
|
310
|
+
await page.goto(`${runtimeBase}/admin`);
|
|
311
|
+
await page.locator('.oxa-current-user').click();
|
|
312
|
+
await page.locator('.oxa-user-dropdown input[type="file"]').first().setInputFiles({
|
|
313
|
+
name: 'avatar.png',
|
|
314
|
+
mimeType: 'image/png',
|
|
315
|
+
buffer: Buffer.from(
|
|
316
|
+
'89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff00000040101005fe245400000000049454e44ae426082',
|
|
317
|
+
'hex'
|
|
318
|
+
),
|
|
319
|
+
});
|
|
320
|
+
await expect(page.getByText('头像已更新', { exact: true })).toBeVisible();
|
|
321
|
+
await expect.poll(() => platform.avatarCompletes).toBe(1);
|
|
322
|
+
await expect(page.locator('.oxa-current-user img')).toHaveAttribute(
|
|
323
|
+
'src',
|
|
324
|
+
'https://cdn.example.test/avatar/new.png'
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('preserves the left menu scroll position when switching resource pages', async ({ page }) => {
|
|
329
|
+
await mockPlatform(page, true, runtimeBase, true);
|
|
330
|
+
await page.goto('/resource-experience.e2e.html');
|
|
331
|
+
const scroller = page.locator('.oxa-menu-scroll');
|
|
332
|
+
await scroller.evaluate(node => {
|
|
333
|
+
const menu = node.querySelector('.oxa-menu') as HTMLElement | null;
|
|
334
|
+
if (menu) menu.style.minHeight = '1400px';
|
|
335
|
+
node.scrollTop = 240;
|
|
336
|
+
node.dispatchEvent(new Event('scroll'));
|
|
337
|
+
});
|
|
338
|
+
await expect.poll(() => scroller.evaluate(node => node.scrollTop)).toBe(240);
|
|
339
|
+
await page.locator('.oxa-sider').getByText('资料下载', { exact: true }).click();
|
|
340
|
+
await expect(page).toHaveURL('/resource-12');
|
|
341
|
+
await expect.poll(() => scroller.evaluate(node => node.scrollTop)).toBe(240);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test('mounts generated operation routes and typed actions in the unified Shell', async ({ page }) => {
|
|
345
|
+
await mockPlatform(page, true, runtimeBase, true);
|
|
346
|
+
await page.goto('/resource-experience.e2e.html');
|
|
347
|
+
await expect(
|
|
348
|
+
page.getByRole('button', { name: '发布选中栏目(0)' })
|
|
349
|
+
).toBeVisible();
|
|
350
|
+
await page.locator('.oxa-sider').getByText('内容业务操作', { exact: true }).click();
|
|
351
|
+
await expect(page).toHaveURL('/admin/operations/content');
|
|
352
|
+
await expect(
|
|
353
|
+
page.getByRole('heading', { name: '内容业务操作' })
|
|
354
|
+
).toBeVisible();
|
|
355
|
+
await expect(page.locator('.oxa-sider')).toBeVisible();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('fails closed on a direct operation route without its capability', async ({ page }) => {
|
|
359
|
+
await mockPlatform(page, false, runtimeBase, false);
|
|
360
|
+
await page.goto('/resource-experience.e2e.html');
|
|
361
|
+
await page.evaluate(() => {
|
|
362
|
+
history.pushState({}, '', '/admin/operations/content');
|
|
363
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
364
|
+
});
|
|
365
|
+
await expect(page.getByText('当前平台用户无此页面权限')).toBeVisible();
|
|
366
|
+
await expect(
|
|
367
|
+
page.locator('.oxa-sider').getByText('内容业务操作', { exact: true })
|
|
368
|
+
).toHaveCount(0);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('keeps platform implementation guidance out of generated forms', async ({ page }) => {
|
|
372
|
+
await mockPlatform(page, true, runtimeBase, true);
|
|
373
|
+
await page.goto('/resource-experience.e2e.html');
|
|
374
|
+
await page.getByRole('button', { name: '新增内容栏目' }).click();
|
|
375
|
+
await expect(page).toHaveURL('/resource-01/new');
|
|
376
|
+
const formLabels = page.locator('.ant-form-item-label label');
|
|
377
|
+
await expect(formLabels.first()).toBeVisible();
|
|
378
|
+
const labels = await formLabels.allTextContents();
|
|
379
|
+
expect(labels.slice(0, 2)).toEqual(['栏目编码', '栏目名称']);
|
|
380
|
+
await expect(
|
|
381
|
+
page.getByText('仅显示当前角色可编辑的字段;保存时将校验数据版本与权限范围。', {
|
|
382
|
+
exact: true,
|
|
383
|
+
})
|
|
384
|
+
).toHaveCount(0);
|
|
385
|
+
await expect(page.getByText('当前角色不可修改此字段', { exact: true })).toHaveCount(0);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('keeps primary filters compact and opens additional filters in a large modal', async ({ page }) => {
|
|
389
|
+
await mockPlatform(page, true, runtimeBase, true);
|
|
390
|
+
await page.goto('/resource-experience.e2e.html');
|
|
391
|
+
await expect(page.locator('.oxa-filter-primary-item')).toHaveCount(2);
|
|
392
|
+
await expect(
|
|
393
|
+
page.getByPlaceholder('搜索栏目名称或栏目编码')
|
|
394
|
+
).toBeVisible();
|
|
395
|
+
const more = page.getByRole('button', { name: '更多筛选' });
|
|
396
|
+
await more.click();
|
|
397
|
+
const modal = page.getByRole('dialog', { name: '更多筛选' });
|
|
398
|
+
await expect(modal).toBeVisible();
|
|
399
|
+
await expect(modal.locator('.oxa-more-filter-field')).toHaveCount(3);
|
|
400
|
+
await expect(modal).toContainText('栏目管理员');
|
|
401
|
+
await expect(modal).toContainText('配套设施');
|
|
402
|
+
await expect(modal).toContainText('开放星期');
|
|
403
|
+
await expect
|
|
404
|
+
.poll(async () => (await modal.boundingBox())?.width || 0)
|
|
405
|
+
.toBeGreaterThan(700);
|
|
406
|
+
});
|
|
407
|
+
|
|
241
408
|
test('renders platform mobile controls from declared field widgets', async ({ page }) => {
|
|
242
409
|
const candidate = codes
|
|
243
410
|
.map(code => ({ code, definition: definitions[code] }))
|
|
@@ -217,6 +217,21 @@ async function mockWorkflow(page: Page, options: { forbidden?: boolean } = {}) {
|
|
|
217
217
|
authzVersion: 1,
|
|
218
218
|
scopeDataVersion: 'scope-1',
|
|
219
219
|
},
|
|
220
|
+
subjectProfile: {
|
|
221
|
+
schemaVersion: 'openxiangda.subject-profile/v2',
|
|
222
|
+
userId: 'acceptance-user',
|
|
223
|
+
displayName: '王老师',
|
|
224
|
+
avatarUrl: null,
|
|
225
|
+
jobNumber: 'T001',
|
|
226
|
+
affiliatedDepartment: { id: 'department-1', name: '理学院' },
|
|
227
|
+
},
|
|
228
|
+
roles: [
|
|
229
|
+
{
|
|
230
|
+
code: 'department_manager',
|
|
231
|
+
name: '部门管理员',
|
|
232
|
+
source: 'package',
|
|
233
|
+
},
|
|
234
|
+
],
|
|
220
235
|
principal: {
|
|
221
236
|
type: 'user_union',
|
|
222
237
|
userId: 'acceptance-user',
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="openxiangda-runtime-base" content="/" />
|
|
7
|
+
<meta name="openxiangda-app-code" content="openxiangda-application" />
|
|
8
|
+
<meta name="openxiangda-environment" content="preproduction" />
|
|
9
|
+
<title>Resource experience acceptance</title>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="root"></div>
|
|
13
|
+
<script type="module" src="/e2e/resource-experience-fixture.tsx"></script>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import ReactDOM from 'react-dom/client';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
defineAdminContributions,
|
|
5
|
+
OpenXiangdaApplication,
|
|
6
|
+
} from 'openxiangda/react';
|
|
4
7
|
import {
|
|
5
8
|
appCode,
|
|
6
9
|
appName,
|
|
10
|
+
appPerspectives,
|
|
11
|
+
appRoutes,
|
|
7
12
|
resourceDefinitions,
|
|
8
13
|
workflowCodes,
|
|
9
14
|
} from '../../../packages/contracts/src/generated.js';
|
|
10
15
|
import 'openxiangda/react/styles.css';
|
|
11
16
|
|
|
17
|
+
const adminContributions = defineAdminContributions(appRoutes, {
|
|
18
|
+
pages: {},
|
|
19
|
+
});
|
|
20
|
+
|
|
12
21
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
13
22
|
<React.StrictMode>
|
|
14
23
|
<OpenXiangdaApplication
|
|
15
24
|
appCode={appCode}
|
|
16
25
|
appName={appName}
|
|
26
|
+
contributions={adminContributions}
|
|
27
|
+
perspectives={appPerspectives}
|
|
17
28
|
resourceDefinitions={resourceDefinitions}
|
|
18
29
|
workflows={workflowCodes}
|
|
19
30
|
/>
|
|
@@ -23,6 +23,8 @@ test('clean template contains only application declarations and one React entry'
|
|
|
23
23
|
assert.deepEqual(Object.keys(resourceDefinitions).sort(), [...resourceCodes].sort());
|
|
24
24
|
assert.match(config, /data:\s*\{\s*resources:/);
|
|
25
25
|
assert.match(source, /OpenXiangdaApplication/);
|
|
26
|
+
assert.match(source, /defineAdminContributions\(appRoutes/);
|
|
27
|
+
assert.match(source, /contributions=\{adminContributions\}/);
|
|
26
28
|
assert.match(source, /from 'openxiangda\/react'/);
|
|
27
29
|
assert.match(source, /openxiangda\/react\/styles\.css/);
|
|
28
30
|
const legacyMarkers = [
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema: openxiangda.appspec/app/v1
|
|
3
|
+
app: "openxiangda-application"
|
|
4
|
+
title: "OpenXiangda 应用"
|
|
5
|
+
status: active
|
|
6
|
+
---
|
|
7
|
+
# OpenXiangda 应用
|
|
8
|
+
|
|
9
|
+
AppSpec 是可选的业务意图辅助层,不是发布门禁。资源、字段、权限和动作仍以
|
|
10
|
+
`openxiangda.config.ts` 与实时编译合同为准。
|
|
11
|
+
|
|
12
|
+
## 业务目标
|
|
13
|
+
|
|
14
|
+
<!-- 用业务语言说明应用解决的问题和可观察结果。 -->
|
|
15
|
+
|
|
16
|
+
## 角色
|
|
17
|
+
|
|
18
|
+
<!-- 只记录业务角色及其目标;稳定 role code 在能力规格中引用。 -->
|
|
19
|
+
|
|
20
|
+
## 范围
|
|
21
|
+
|
|
22
|
+
### 包含
|
|
23
|
+
|
|
24
|
+
### 不包含
|
|
25
|
+
|
|
26
|
+
## 能力目录
|
|
27
|
+
|
|
28
|
+
<!-- 复杂业务再在 capabilities/ 下增加 CAP-*;小应用可以只维护本文件。 -->
|
|
29
|
+
|
|
30
|
+
## 术语
|
|
31
|
+
|
|
32
|
+
## 跨能力约束
|
|
33
|
+
|
|
34
|
+
## 未确认问题
|
|
35
|
+
|
|
36
|
+
- 无。
|
package/template/package.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export const compilerContractVersion = "native-2" as const;
|
|
3
3
|
export const appCode = "openxiangda-application" as const;
|
|
4
4
|
export const appName = "OpenXiangda 应用" as const;
|
|
5
|
+
export const appPerspectives = [] as const;
|
|
5
6
|
export const resourceCodes = [] as const;
|
|
6
7
|
export const resourceSurfaces = {} as const;
|
|
7
8
|
export const resourceDefinitions = {} as const;
|
|
@@ -31,6 +32,7 @@ export type ResourceCode = (typeof resourceCodes)[number];
|
|
|
31
32
|
export type Capability = (typeof capabilities)[number];
|
|
32
33
|
export type AppOperation = (typeof appOperations)[keyof typeof appOperations];
|
|
33
34
|
export type AppRoute = (typeof appRoutes)[keyof typeof appRoutes];
|
|
35
|
+
export type AppPerspective = (typeof appPerspectives)[number];
|
|
34
36
|
export type EventType = (typeof eventTypes)[number];
|
|
35
37
|
export type EventSubscriptionCode = (typeof eventSubscriptionCodes)[number];
|
|
36
38
|
export type EventTypesForSubscription<TCode extends EventSubscriptionCode> = never;
|
|
@@ -39,7 +39,7 @@ const agents = readFileSync(resolve(root, 'AGENTS.md'), 'utf8');
|
|
|
39
39
|
if (!agents.includes('pnpm openxiangda')) throw new Error('TEMPLATE_AGENT_PINNED_CLI_MISSING');
|
|
40
40
|
if (
|
|
41
41
|
!agents.includes("union of that user's application roles") ||
|
|
42
|
-
!agents.includes('
|
|
42
|
+
!agents.includes('Perspective projects only read visibility')
|
|
43
43
|
) {
|
|
44
44
|
throw new Error('TEMPLATE_AGENT_ROLE_UNION_BOUNDARY_MISSING');
|
|
45
45
|
}
|