openxiangda-skill-kit 2.0.0-alpha.12

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.
Files changed (37) hide show
  1. package/README.md +10 -0
  2. package/dist/bin.d.ts +3 -0
  3. package/dist/bin.d.ts.map +1 -0
  4. package/dist/bin.js +35 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/index.d.ts +29 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +205 -0
  9. package/dist/index.js.map +1 -0
  10. package/docs/architecture/repository-and-release.md +52 -0
  11. package/docs/backend.md +89 -0
  12. package/docs/concepts.md +34 -0
  13. package/docs/data-authz.md +100 -0
  14. package/docs/delivery.md +71 -0
  15. package/docs/frontend.md +46 -0
  16. package/docs/getting-started.md +120 -0
  17. package/docs/index.md +23 -0
  18. package/docs/llms.txt +12 -0
  19. package/docs/reference/cli.md +51 -0
  20. package/docs/reference/mcp.md +26 -0
  21. package/docs/workflow-events.md +63 -0
  22. package/package.json +39 -0
  23. package/skills/manifest.json +40 -0
  24. package/skills/openxiangda-v2/SKILL.md +38 -0
  25. package/skills/openxiangda-v2/agents/openai.yaml +4 -0
  26. package/skills/openxiangda-v2-architecture/SKILL.md +29 -0
  27. package/skills/openxiangda-v2-architecture/agents/openai.yaml +4 -0
  28. package/skills/openxiangda-v2-backend/SKILL.md +42 -0
  29. package/skills/openxiangda-v2-backend/agents/openai.yaml +4 -0
  30. package/skills/openxiangda-v2-data-authz/SKILL.md +44 -0
  31. package/skills/openxiangda-v2-data-authz/agents/openai.yaml +4 -0
  32. package/skills/openxiangda-v2-delivery/SKILL.md +63 -0
  33. package/skills/openxiangda-v2-delivery/agents/openai.yaml +4 -0
  34. package/skills/openxiangda-v2-frontend/SKILL.md +40 -0
  35. package/skills/openxiangda-v2-frontend/agents/openai.yaml +4 -0
  36. package/skills/openxiangda-v2-workflow-events/SKILL.md +39 -0
  37. package/skills/openxiangda-v2-workflow-events/agents/openai.yaml +4 -0
package/README.md ADDED
@@ -0,0 +1,10 @@
1
+ # openxiangda-skill-kit
2
+
3
+ Validates OpenXiangda 2.0 skill metadata, links, command references, version boundaries, and deterministic distribution manifests.
4
+
5
+ ```bash
6
+ openxiangda-skill-kit install --dest ~/.codex/skills
7
+ openxiangda-skill-kit validate ./skills
8
+ ```
9
+
10
+ The published package includes the shared 2.0 documentation tree. Installation copies it into a namespaced hidden reference directory beside the installed Skills and rewrites local links, so every installed `SKILL.md` remains valid outside the source repository.
package/dist/bin.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
package/dist/bin.js ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { installSkills, resolveDefaultSkillsRoot, validateSkills, writeSkillManifest, } from './index.js';
5
+ const args = process.argv.slice(2);
6
+ const command = args[0] === 'install' || args[0] === 'validate' ? args.shift() : 'validate';
7
+ const writeManifest = args.includes('--write-manifest');
8
+ const force = args.includes('--force');
9
+ const optionValue = (name) => {
10
+ const index = args.indexOf(name);
11
+ return index >= 0 ? args[index + 1] : undefined;
12
+ };
13
+ const defaultSkillsRoot = resolveDefaultSkillsRoot();
14
+ if (command === 'install') {
15
+ const skillsRoot = resolve(optionValue('--source') || defaultSkillsRoot);
16
+ const destination = resolve(optionValue('--dest') || process.env.CODEX_HOME || resolve(homedir(), '.codex'), optionValue('--dest') ? '' : 'skills');
17
+ const result = await installSkills({ skillsRoot, destination, force });
18
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
19
+ }
20
+ else {
21
+ const rootArgument = args.find(argument => !argument.startsWith('-'));
22
+ const skillsRoot = resolve(process.cwd(), rootArgument || defaultSkillsRoot);
23
+ const issues = await validateSkills(skillsRoot);
24
+ if (issues.length) {
25
+ for (const issue of issues)
26
+ process.stderr.write(`${issue.skill}: ${issue.message} (${issue.file})\n`);
27
+ process.exitCode = 1;
28
+ }
29
+ else {
30
+ if (writeManifest)
31
+ await writeSkillManifest(skillsRoot);
32
+ process.stdout.write(`Validated ${skillsRoot}${writeManifest ? ' and wrote manifest' : ''}.\n`);
33
+ }
34
+ }
35
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EACL,aAAa,EACb,wBAAwB,EACxB,cAAc,EACd,kBAAkB,GACnB,MAAM,YAAY,CAAC;AAEpB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC;AAC5F,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC;AACxD,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;AACvC,MAAM,WAAW,GAAG,CAAC,IAAY,EAAE,EAAE;IACnC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAClD,CAAC,CAAC;AACF,MAAM,iBAAiB,GAAG,wBAAwB,EAAE,CAAC;AAErD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;IAC1B,MAAM,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,UAAU,CAAC,IAAI,iBAAiB,CAAC,CAAC;IACzE,MAAM,WAAW,GAAG,OAAO,CACzB,WAAW,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,OAAO,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,EAC/E,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CACtC,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,EAAE,UAAU,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC;IACvE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AAC/D,CAAC;KAAM,CAAC;IACN,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IACtE,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,YAAY,IAAI,iBAAiB,CAAC,CAAC;IAC7E,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,UAAU,CAAC,CAAC;IAChD,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,KAAK,MAAM,KAAK,IAAI,MAAM;YAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,OAAO,KAAK,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC;QACvG,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACvB,CAAC;SAAM,CAAC;QACN,IAAI,aAAa;YAAE,MAAM,kBAAkB,CAAC,UAAU,CAAC,CAAC;QACxD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,aAAa,UAAU,GAAG,aAAa,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAClG,CAAC;AACH,CAAC"}
@@ -0,0 +1,29 @@
1
+ export interface SkillValidationIssue {
2
+ skill: string;
3
+ file: string;
4
+ message: string;
5
+ }
6
+ export interface SkillManifestEntry {
7
+ name: string;
8
+ description: string;
9
+ sha256: string;
10
+ }
11
+ export interface SkillManifest {
12
+ schemaVersion: 1;
13
+ skills: SkillManifestEntry[];
14
+ }
15
+ export interface InstallSkillsInput {
16
+ skillsRoot: string;
17
+ destination: string;
18
+ force?: boolean;
19
+ }
20
+ export declare function resolveDefaultSkillsRoot(): string;
21
+ export declare function validateSkills(skillsRoot: string): Promise<SkillValidationIssue[]>;
22
+ export declare function createSkillManifest(skillsRoot: string): Promise<SkillManifest>;
23
+ export declare function writeSkillManifest(skillsRoot: string, output?: string): Promise<SkillManifest>;
24
+ export declare function installSkills(input: InstallSkillsInput): Promise<{
25
+ destination: string;
26
+ installed: string[];
27
+ references: string | null;
28
+ }>;
29
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,CAAC,CAAC;IACjB,MAAM,EAAE,kBAAkB,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,wBAAgB,wBAAwB,WAKvC;AAmBD,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAoDxF;AAED,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAkBpF;AAED,wBAAsB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,SAAuC,0BAIzG;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,kBAAkB;;;;GA2C5D"}
package/dist/index.js ADDED
@@ -0,0 +1,205 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { DEVKIT_COMMANDS } from 'openxiangda-devkit-core';
6
+ export function resolveDefaultSkillsRoot() {
7
+ const packaged = resolve(import.meta.dirname, '../skills');
8
+ const development = resolve(import.meta.dirname, '../../../skills');
9
+ const packagedDocs = resolve(packaged, '../docs');
10
+ return existsSync(packaged) && existsSync(packagedDocs) ? packaged : development;
11
+ }
12
+ const FORBIDDEN_V2_TERMS = [
13
+ /\bApp Function\b/i,
14
+ /\bJS_CODE\b/i,
15
+ /\bresource publish\b/i,
16
+ /\bworkflow v3\b/i,
17
+ ];
18
+ const knownCommandIds = new Set(DEVKIT_COMMANDS.map(command => command.id));
19
+ export async function validateSkills(skillsRoot) {
20
+ const skills = await loadSkills(skillsRoot);
21
+ const knownSkills = new Set(skills.map(skill => skill.name));
22
+ const issues = [];
23
+ for (const skill of skills) {
24
+ const add = (message, file = skill.file) => issues.push({ skill: skill.name || skill.directory, file, message });
25
+ if (!skill.name)
26
+ add('SKILL.md frontmatter must declare name.');
27
+ if (skill.name !== skill.directory)
28
+ add('Skill directory and frontmatter name must match.');
29
+ if (!skill.description || !/\bUse (when|for)\b/i.test(skill.description)) {
30
+ add('Description must explain when the skill should be used.');
31
+ }
32
+ if (/\bTODO\b/.test(skill.source))
33
+ add('Skill contains unfinished TODO text.');
34
+ for (const forbidden of FORBIDDEN_V2_TERMS) {
35
+ if (forbidden.test(skill.source))
36
+ add(`2.0 skill contains forbidden 1.x term: ${forbidden.source}`);
37
+ }
38
+ for (const command of extractCommands(skill.source)) {
39
+ const commandId = toCommandId(command);
40
+ if (!commandId || !knownCommandIds.has(commandId)) {
41
+ add(`Unknown 2.0 CLI command reference: openxiangda ${command}`);
42
+ }
43
+ }
44
+ for (const linkedSkill of extractSkillMentions(skill.source)) {
45
+ if (!knownSkills.has(linkedSkill))
46
+ add(`Unknown skill reference: $${linkedSkill}`);
47
+ }
48
+ for (const reference of extractLocalMarkdownLinks(skill.source)) {
49
+ const target = resolve(dirname(skill.file), reference);
50
+ if (!(await isFile(target)))
51
+ add(`Broken local reference: ${reference}`);
52
+ }
53
+ const agentFile = resolve(skillsRoot, skill.directory, 'agents/openai.yaml');
54
+ if (!(await isFile(agentFile))) {
55
+ add('Missing agents/openai.yaml.', agentFile);
56
+ }
57
+ else {
58
+ const agentSource = await readFile(agentFile, 'utf8');
59
+ if (!agentSource.includes(`$${skill.name}`)) {
60
+ add('Agent default_prompt must explicitly mention the skill name.', agentFile);
61
+ }
62
+ if (/\bTODO\b/.test(agentSource))
63
+ add('Agent metadata contains unfinished TODO text.', agentFile);
64
+ }
65
+ }
66
+ return issues.sort((left, right) => `${left.skill}:${left.file}:${left.message}`.localeCompare(`${right.skill}:${right.file}:${right.message}`));
67
+ }
68
+ export async function createSkillManifest(skillsRoot) {
69
+ const skills = await loadSkills(skillsRoot);
70
+ const entries = await Promise.all(skills.map(async (skill) => {
71
+ const agentFile = resolve(skillsRoot, skill.directory, 'agents/openai.yaml');
72
+ const agentSource = await readFile(agentFile, 'utf8');
73
+ return {
74
+ name: skill.name,
75
+ description: skill.description,
76
+ sha256: createHash('sha256')
77
+ .update(skill.source)
78
+ .update('\0')
79
+ .update(agentSource)
80
+ .digest('hex'),
81
+ };
82
+ }));
83
+ return { schemaVersion: 1, skills: entries.sort((left, right) => left.name.localeCompare(right.name)) };
84
+ }
85
+ export async function writeSkillManifest(skillsRoot, output = resolve(skillsRoot, 'manifest.json')) {
86
+ const manifest = await createSkillManifest(skillsRoot);
87
+ await writeFile(output, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
88
+ return manifest;
89
+ }
90
+ export async function installSkills(input) {
91
+ const skills = await loadSkills(input.skillsRoot);
92
+ const docsSource = resolve(input.skillsRoot, '../docs');
93
+ const referencesTarget = resolve(input.destination, '.openxiangda-v2-references');
94
+ const hasReferences = await isDirectory(docsSource);
95
+ const targets = skills.map(skill => resolve(input.destination, skill.directory));
96
+ if (hasReferences)
97
+ targets.push(referencesTarget);
98
+ if (!input.force) {
99
+ for (const target of targets) {
100
+ if (await pathExists(target))
101
+ throw new Error(`SKILL_ALREADY_EXISTS: ${target}`);
102
+ }
103
+ }
104
+ await mkdir(input.destination, { recursive: true });
105
+ if (hasReferences) {
106
+ await rm(referencesTarget, { recursive: true, force: true });
107
+ await cp(docsSource, referencesTarget, { recursive: true });
108
+ await rm(resolve(referencesTarget, '.vitepress'), { recursive: true, force: true });
109
+ }
110
+ const installed = [];
111
+ for (const skill of skills) {
112
+ const source = resolve(input.skillsRoot, skill.directory);
113
+ const target = resolve(input.destination, skill.directory);
114
+ await rm(target, { recursive: true, force: true });
115
+ await cp(source, target, { recursive: true });
116
+ if (hasReferences) {
117
+ const skillFile = resolve(target, 'SKILL.md');
118
+ const sourceText = await readFile(skillFile, 'utf8');
119
+ await writeFile(skillFile, sourceText.replaceAll('../../docs/', '../.openxiangda-v2-references/'), 'utf8');
120
+ }
121
+ installed.push(skill.name);
122
+ }
123
+ return {
124
+ destination: input.destination,
125
+ installed,
126
+ references: hasReferences ? referencesTarget : null,
127
+ };
128
+ }
129
+ async function loadSkills(skillsRoot) {
130
+ const entries = await readdir(skillsRoot, { withFileTypes: true });
131
+ const directories = entries
132
+ .filter(entry => entry.isDirectory() && entry.name.startsWith('openxiangda-'))
133
+ .map(entry => entry.name)
134
+ .sort();
135
+ return Promise.all(directories.map(async (directory) => {
136
+ const file = resolve(skillsRoot, directory, 'SKILL.md');
137
+ const source = await readFile(file, 'utf8');
138
+ const frontmatter = parseFrontmatter(source);
139
+ return {
140
+ directory,
141
+ file,
142
+ source,
143
+ name: frontmatter.name || '',
144
+ description: frontmatter.description || '',
145
+ };
146
+ }));
147
+ }
148
+ function parseFrontmatter(source) {
149
+ const match = source.match(/^---\n([\s\S]*?)\n---\n/);
150
+ if (!match?.[1])
151
+ return {};
152
+ return Object.fromEntries(match[1]
153
+ .split('\n')
154
+ .map(line => line.match(/^([a-z_]+):\s*(.+)$/))
155
+ .filter((line) => Boolean(line))
156
+ .map(line => [line[1], line[2].replace(/^['"]|['"]$/g, '')]));
157
+ }
158
+ function extractCommands(source) {
159
+ return [...source.matchAll(/`openxiangda\s+([^`]+)`/g)].map(match => match[1].trim());
160
+ }
161
+ function extractSkillMentions(source) {
162
+ return [...source.matchAll(/\$([a-z0-9-]+)/g)].map(match => match[1]);
163
+ }
164
+ function extractLocalMarkdownLinks(source) {
165
+ return [...source.matchAll(/\]\(([^)]+)\)/g)]
166
+ .map(match => match[1])
167
+ .filter(reference => !/^(?:https?:|#)/.test(reference));
168
+ }
169
+ function toCommandId(command) {
170
+ const tokens = command.split(/\s+/).filter(token => token && !token.startsWith('-'));
171
+ if (!tokens[0])
172
+ return undefined;
173
+ for (let length = Math.min(tokens.length, 4); length >= 1; length -= 1) {
174
+ const candidate = tokens.slice(0, length).join(':');
175
+ if (knownCommandIds.has(candidate))
176
+ return candidate;
177
+ }
178
+ return undefined;
179
+ }
180
+ async function isFile(file) {
181
+ try {
182
+ return (await stat(file)).isFile();
183
+ }
184
+ catch {
185
+ return false;
186
+ }
187
+ }
188
+ async function isDirectory(path) {
189
+ try {
190
+ return (await stat(path)).isDirectory();
191
+ }
192
+ catch {
193
+ return false;
194
+ }
195
+ }
196
+ async function pathExists(path) {
197
+ try {
198
+ await stat(path);
199
+ return true;
200
+ }
201
+ catch {
202
+ return false;
203
+ }
204
+ }
205
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ # OpenXiangda 2.0 仓库与发布架构
2
+
3
+ ## 已确认决策
4
+
5
+ - 2.0 是独立产品线,不提供任何 1.x 开发、迁移或兼容入口。
6
+ - 1.x 旧应用由独立仓库和旧运行时维护;平台可以暂时同时托管两代应用。
7
+ - 新应用只安装工作区本地 `openxiangda-cli`,不经过全局 1.x CLI。
8
+ - pnpm 管理 workspace,Turborepo 计算受影响任务,Changesets 管理独立包版本。
9
+ - 官方应用模板保存并发布一组经过完整验证的精确依赖版本。
10
+
11
+ ## 包发布单元
12
+
13
+ | 单元 | 职责 | 版本策略 |
14
+ | --- | --- | --- |
15
+ | contracts/compiler/devkit-core/nest | 平台协议和后端 SDK | 独立 SemVer,依赖范围由 Changesets 更新 |
16
+ | workflow | Workflow Kernel v2 的纯定义与规划 | 独立 SemVer |
17
+ | cli/mcp/skill-kit/create-openxiangda | AI 与开发工具入口 | 独立 SemVer |
18
+ | admin/testing | 前端框架和测试工具 | 独立 SemVer |
19
+ | application template | 经验证的应用 BOM | 精确固定上述包版本 |
20
+
21
+ ## 测试层级
22
+
23
+ 1. 提交与 PR 使用 `pnpm verify:affected`,只运行受影响包及其依赖任务。
24
+ 2. 发布候选使用 `pnpm verify:release`,运行全量 2.0 包、官方模板、Skills 和文档构建。
25
+ 3. 发布后在全新独立项目执行安装、创建应用、前后端构建和 CLI/Skill 冒烟测试。
26
+ 4. 平台集成测试部署到测试环境;只有平台协议或部署能力变更才需要阻塞核心发布。
27
+ 5. prod-1 只晋级已经验证的相同 npm 版本、AppPackage 和 OCI digest,不重新构建。
28
+
29
+ 任何层级都不得调用 1.x 测试套件。
30
+
31
+ ## 确定性发布
32
+
33
+ ```text
34
+ reviewed Changesets
35
+ -> authoritative master commit
36
+ -> frozen dependency install
37
+ -> affected/full 2.0 verification
38
+ -> deterministic version commit
39
+ -> CI npm publish
40
+ -> independent-project acceptance
41
+ -> immutable promotion
42
+ ```
43
+
44
+ 发包阶段不允许 AI 决定包、版本、测试范围或发布顺序。AI 可以编写代码和
45
+ Changeset,但最终范围由 Git diff、workspace 依赖图和机器校验共同确定。
46
+
47
+ ## OAuth2 外部应用身份
48
+
49
+ 2.0 外部应用采用 OAuth2 Client Credentials。平台签发短期 access token,
50
+ token 至少绑定 tenant、application、environment、client 和 scope。client secret
51
+ 只在创建或轮换时返回一次,平台保存不可逆摘要;支持双密钥轮换窗口、即时吊销、
52
+ 速率限制和完整审计。应用 NestJS 后端只信任平台校验并注入的 service Principal。
@@ -0,0 +1,89 @@
1
+ # 后端
2
+
3
+ 官方模板使用 NestJS。应用后端是普通 Node 服务,但身份、数据、事件、工作流、网关与部署由平台提供统一适配。
4
+
5
+ ## 标准模块
6
+
7
+ ```text
8
+ src/platform token、request context、Data API、Workflow、Events 客户端
9
+ src/domain 业务服务与不变量
10
+ src/modules 控制器、DTO、应用用例
11
+ src/consumers 幂等事件消费者
12
+ src/health liveness/readiness
13
+ ```
14
+
15
+ 事件消费者默认使用平台托管的持久回执,不需要应用数据库。平台按租户、应用、环境、订阅和事件 ID 唯一保存领取租约与完成状态;Nest SDK 自动签名回执命令。幂等的完成/释放命令默认做 3 次有界退避重试,且业务 handler 成功后不会因为完成回执暂时故障而主动释放租约。进程内回执只用于显式的本地单元测试;涉及外部系统的副作用仍必须把 CloudEvent `id` 作为对方的幂等键。
16
+
17
+ ## 身份和接口
18
+
19
+ 平台网关将用户 token 传给应用后端。`openxiangda-nest` 校验 token,并构造包含 tenant、application、user、activeRole、attributes、correlationId 的上下文。面向其他系统的接口同样经平台域名和网关发布,使用 OAuth2 client credentials 获取的短期应用身份与 scope。
20
+
21
+ 用户身份必须绑定 RoleSession;应用身份绑定 tenant、app、environment、client、credentialVersion 和 scopes。`OpenXiangdaAuthzGuard` 默认要求服务身份至少具有 `app:invoke`,声明 `@RequireCapability()` 时还要求 token 中包含完全匹配的 capability。应用身份可以调用授权的 App API/Data API,但 Workflow 客户端拒绝没有用户 RoleSession 的上下文。
22
+
23
+ Worker、Scheduler 或事件消费者没有浏览器请求上下文时,注入 `OpenXiangdaApplicationDataApiService`。每个已部署后端的应用环境都有一个平台托管的 workload OAuth2 client;平台自动创建或复用客户端,并通过 Kubernetes Secret 注入 `OPENXIANGDA_OAUTH_CLIENT_ID`、`OPENXIANGDA_OAUTH_CLIENT_SECRET` 和 `OPENXIANGDA_OAUTH_SCOPES`,这些保留变量不写入 `backend.secrets`。本地开发时可以在未提交的 `.env` 中显式提供测试客户端凭据。
24
+
25
+ 运行凭据的状态只包含 clientId、当前版本、hint、宽限期和待激活轮换元数据,不返回 Secret。`openxiangda oauth runtime rotate --environment <key> --idempotency-key <key>` 使用当前版本 CAS 暂存新凭据,并自动对当前激活 AppVersion 创建一次同版本滚动部署;部署准备阶段才把待激活凭据提升为当前凭据。旧 Pod 使用的上一版本在指定宽限期内仍可换 token,因此轮换不依赖同时重启全部副本。失败或重复请求不会生成第二份凭据,运维重试必须复用幂等键。
26
+
27
+ SDK 会合并并发换 token、在到期前刷新,并在平台返回 401 时清除旧 token 后重试一次;应用代码不应缓存、打印或持久化 access token。
28
+
29
+ ```ts
30
+ OpenXiangdaModule.forRoot({
31
+ appCode: process.env.OPENXIANGDA_APP_CODE!,
32
+ platformBaseUrl: process.env.OPENXIANGDA_PLATFORM_BASE_URL!,
33
+ environmentId: process.env.OPENXIANGDA_ENVIRONMENT_ID!,
34
+ version: process.env.OPENXIANGDA_APP_VERSION!,
35
+ oauthClient: {
36
+ clientId: process.env.OPENXIANGDA_OAUTH_CLIENT_ID!,
37
+ clientSecret: process.env.OPENXIANGDA_OAUTH_CLIENT_SECRET!,
38
+ scopes: ['app:instrument-center:data:instruments:read'],
39
+ },
40
+ });
41
+ ```
42
+
43
+ ## 数据访问
44
+
45
+ 后端不持有平台数据库连接。普通读取和写入使用 Data API;需要原子性的相关写入使用受限事务批处理。批处理只允许声明资源、受限操作数、修订检查和平台审计,不能执行任意 SQL。
46
+
47
+ 详情使用单记录接口,统计报表使用受限聚合接口,审计使用业务记录审计接口。三者都沿用调用上下文中的 RoleSession 和字段策略。附件先调用 initiate 获得短期签名上传计划,上传对象后调用 complete 校验,再把返回的 `DataFileRef` 随普通 create/update 或受限事务保存;不要把对象存储密钥、对象名或签名 URL作为业务字段。
48
+
49
+ ## 运行与 Secret
50
+
51
+ 容器必须提供 liveness/readiness,支持优雅退出和结构化日志。事件签名密钥、工作流 provider 密钥及外部系统凭证由平台生成 Kubernetes Secret,并通过 `valueFrom.secretKeyRef` 注入;不会写入 AppPackage 或返回给浏览器。
52
+
53
+ 外部凭证只在配置中声明逻辑名和环境变量名,值由目标环境独立维护:
54
+
55
+ ```ts
56
+ backend: {
57
+ root: 'apps/server',
58
+ runtime: 'node',
59
+ framework: 'nestjs',
60
+ runMode: 'shared',
61
+ secrets: [
62
+ {
63
+ name: 'dingtalk-client-secret',
64
+ env: 'DINGTALK_CLIENT_SECRET',
65
+ required: true,
66
+ },
67
+ ],
68
+ }
69
+ ```
70
+
71
+ 创建与轮换时,CLI 只从本机环境变量或文件读取值,不接受命令行明文参数,也不在输出中回显:
72
+
73
+ ```bash
74
+ OPENXIANGDA_SECRET_VALUE='...' \
75
+ openxiangda secret create dingtalk-client-secret \
76
+ --environment development \
77
+ --from-env OPENXIANGDA_SECRET_VALUE
78
+
79
+ openxiangda secret list --environment development
80
+
81
+ OPENXIANGDA_SECRET_VALUE='...' \
82
+ openxiangda secret rotate dingtalk-client-secret \
83
+ --environment development \
84
+ --revision 1 \
85
+ --from-env OPENXIANGDA_SECRET_VALUE \
86
+ --idempotency-key secret-rotation-20260812
87
+ ```
88
+
89
+ Secret 按 tenant、application、environment 隔离,版本不可变,元数据修改使用 revision CAS,所有管理动作进入不含明文的审计记录。必需 Secret 缺失、禁用或过期时,部署在创建运行资源前失败;可选 Secret 则不注入。
@@ -0,0 +1,34 @@
1
+ # 核心架构
2
+
3
+ ```mermaid
4
+ flowchart LR
5
+ Repo["应用 Git 仓库"] --> CI["应用 CI"]
6
+ CI --> Package["不可变 AppPackage"]
7
+ Package --> Control["平台控制面"]
8
+ Control --> Deploy["DeploymentRun"]
9
+ Deploy --> Web["前端静态包"]
10
+ Deploy --> Backend["每应用独立 NestJS 容器"]
11
+ Deploy --> Config["Data/AuthZ/Workflow/Event 配置版本"]
12
+ Backend --> Data["统一 Data API"]
13
+ Backend --> Kernel["Workflow Kernel v2"]
14
+ Backend --> Events["事件投递服务"]
15
+ ```
16
+
17
+ ## 工程边界
18
+
19
+ - Git 仓库是应用源码与声明的事实来源。
20
+ - AppPackage 是交付边界,包含前端摘要、后端镜像摘要、配置包摘要和契约版本。
21
+ - 平台是运行状态的事实来源,持久保存应用版本、部署运行、检查点与环境激活状态。
22
+ - AI、CLI、MCP 都是控制面客户端,不负责持有发布状态。
23
+
24
+ ## 运行档位
25
+
26
+ 2.0 只有平台可控的后端运行方式:每应用独立容器,共享 Kubernetes 集群、节点池、网关和可观测基础设施。后续可以用资源配额形成共享档与独享档,但不建设多应用共用 Node 进程。
27
+
28
+ ## 数据边界
29
+
30
+ 首期不为应用创建独立数据库。业务后端通过统一 Data API 访问平台数据;Data API 提供资源化查询、字段策略、行级授权、并发修订和受限事务批处理。这样保留统一治理,又不限制应用后端表达业务逻辑。
31
+
32
+ ## 版本列车
33
+
34
+ `openxiangda-*` 包、CLI、MCP、Skills 和文档按固定版本列车发布。应用包记录所需契约范围;平台在部署前拒绝不兼容版本。
@@ -0,0 +1,100 @@
1
+ # Data API 与权限
2
+
3
+ 2.0 权限由 RBAC 与上下文数据策略共同组成:角色回答“可以做什么”,策略回答“在当前角色下可以对哪些数据和字段做”。
4
+
5
+ ## 请求模型
6
+
7
+ ```text
8
+ subject = user + activeRole + departments + application attributes
9
+ action = generated capability
10
+ resource = resource type + target record attributes
11
+ environment = tenant + application + request context
12
+ decision = allow/deny + row filter + field policy + audit obligations
13
+ ```
14
+
15
+ 多角色用户每次只选择一个 activeRole。平台不默认合并全部角色权限,因此学院管理员与仪器管理员可以在稳定、可解释的视图之间切换。
16
+
17
+ Admin 页面需要判定多个菜单、按钮或字段时调用 batch explain,一次请求最多 200 个判定。平台在同一 RoleSession 下只解析一次角色权限集合,再逐项返回解释结果;前端不得把批量结果用于另一个角色会话,也不得在接口失败时默认放行。
18
+
19
+ 单记录详情、聚合、审计和附件下载都沿用同一个 RoleSession、行策略与字段策略。聚合在平台数据库侧执行,只接受已声明字段、受限维度/指标/过滤/排序和结果上限,不允许应用提交 SQL,也不能借聚合读取不可见字段。业务审计复用与数据写入同事务提交的持久事件,返回前再次执行行授权并裁剪不可读字段。
20
+
21
+ ## 随应用包发布的权限声明
22
+
23
+ `openxiangda.config.ts` 是角色、capability、范围维度和数据策略的唯一代码源。编译器把它们写入 Config Bundle;平台在切换 AppVersion 时与 Data Resource、事件和工作流配置放在同一个数据库事务中激活。旧版本遗漏过的角色或策略只会在它们曾由应用包管理时被停用,不会回收后台手工创建的运维角色。
24
+
25
+ ```ts
26
+ authz: {
27
+ roles: [
28
+ {
29
+ code: 'applicant',
30
+ name: '申请人',
31
+ capabilities: ['app:instrument-center:data:reservations:read'],
32
+ },
33
+ {
34
+ code: 'college_admin',
35
+ name: '学院管理员',
36
+ capabilities: ['app:instrument-center:data:reservations:read'],
37
+ },
38
+ {
39
+ code: 'instrument_admin',
40
+ name: '仪器管理员',
41
+ capabilities: ['app:instrument-center:data:reservations:read'],
42
+ },
43
+ ],
44
+ scopeDimensions: [{ code: 'college', name: '学院' }],
45
+ dataPolicies: [{
46
+ code: 'reservation_access',
47
+ name: '预约访问范围',
48
+ matchMode: 'OR',
49
+ rules: [
50
+ {
51
+ subject: 'current_user',
52
+ field: 'applicant_id',
53
+ roleCodes: ['applicant'],
54
+ },
55
+ {
56
+ dimensionCode: 'college',
57
+ field: 'college_id',
58
+ roleCodes: ['college_admin'],
59
+ },
60
+ {
61
+ relationCode: 'instrument_manager',
62
+ resourceCode: 'instrument',
63
+ field: 'instrument_id',
64
+ roleCodes: ['instrument_admin'],
65
+ },
66
+ ],
67
+ }],
68
+ }
69
+ ```
70
+
71
+ `roleCodes` 把一条数据规则限定到当前 `RoleSession` 选中的角色。同一用户同时是申请人、学院管理员时,切换到学院管理员后,`applicant` 的 `current_user` 规则不再参与 OR 计算。多角色应用的角色特有规则必须声明 `roleCodes`;未声明时表示该规则对所有活动角色通用。编译器会拒绝空数组、重复或未声明的角色代码。
72
+
73
+ 角色分配、学院范围授权和“用户—仪器”关系授权属于环境运行数据,不进入不可变应用包。它们通过平台管理 API 维护,部署新版本不会删除。
74
+
75
+ ## Directory v2
76
+
77
+ 应用需要选择部门或人员时只使用 Directory v2。查询和 ID 解析都要求当前角色具备 `app:<appCode>:directory:read`,返回稳定 ID、名称、描述和组织路径,不向应用前端暴露联系方式。已保存字段始终存 ID;列表显示、搜索和表单通过同一接口解析可读名称。平台查询范围受当前用户可见通讯录约束,应用不能借助选择器枚举租户通讯录。
78
+
79
+ ## 应用数据属性
80
+
81
+ 仪器管理员、学院管理员等关系来自应用数据时,平台把运行时授权物化为范围授权或 Relationship Grant。例如:
82
+
83
+ - `instrument.manager_user_ids` 与当前用户匹配,允许管理该仪器;
84
+ - `college.id` 属于当前角色管理的学院集合,允许查看该学院预约;
85
+ - 字段策略可以只允许指定角色修改审批意见或费用字段。
86
+
87
+ ## 管理员
88
+
89
+ 应用管理员保留 bypass。它作为明确的最高权限声明实现,而不是散落在查询代码中的特判;所有绕过业务数据策略的写操作都记录审计原因、操作者和关联 ID。
90
+
91
+ ## 防护
92
+
93
+ - 行过滤和字段策略必须在服务端/Data API 执行。
94
+ - 更新使用 revision,避免静默覆盖。
95
+ - 受限事务批处理沿用同一授权上下文,不允许在批次中提权。
96
+ - 托管附件只保存平台生成的文件引用。上传使用短期签名 URL,平台在完成阶段校验对象大小和类型,文件绑定记录后仍按行权限和字段读权限下载;浏览器不持有对象存储凭证。未完成/未绑定上传默认 24 小时后回收,字段替换、清空或记录删除后的对象默认保留 30 天再回收;后台使用批次租约、指数退避和最大重试次数,业务请求不等待对象删除。
97
+ - 聚合查询必须有明确维度/指标上限并在 RLS 数据角色下执行;禁止客户端拉取全量数据后统计。
98
+ - 数据变更事件是业务审计的事实来源,记录操作者、角色会话或 OAuth Client、修订、前后值与关联记录;`eventId/requestId/traceId/environment/outbox` 状态贯穿业务审计和事件投递,审计读取仍需当前身份有权查看该记录。
99
+ - 字段策略同时约束读取、搜索、导出、导入和写入;前端隐藏只是体验,Data API 必须再次执行同一策略。
100
+ - 策略测试覆盖允许、拒绝、角色切换、跨学院访问和管理员 bypass。