openxiangda 1.0.268 → 1.0.270
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/README.md +20 -1
- package/lib/cli.js +417 -24
- package/lib/config.js +60 -56
- package/lib/design-gates.js +18 -0
- package/lib/migration-advice.js +19 -0
- package/lib/release-plan.js +2 -0
- package/lib/workspace-init.js +11 -4
- package/openxiangda-skills/SKILL.md +8 -0
- package/openxiangda-skills/references/architecture-patterns.md +218 -218
- package/openxiangda-skills/references/component-guide.md +122 -122
- package/openxiangda-skills/references/openxiangda-api.md +12 -0
- package/openxiangda-skills/references/platform-data-model.md +285 -285
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +32 -2
- package/openxiangda-skills/references/troubleshooting.md +224 -224
- package/openxiangda-skills/references/webhooks.md +213 -0
- package/openxiangda-skills/references/workspace-state.md +1 -1
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +1 -1
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +3 -0
- package/package.json +26 -23
- package/packages/sdk/dist/{ProcessPreview-Cyk6uv-w.d.mts → ProcessPreview-DSUIJi5V.d.mts} +2 -3
- package/packages/sdk/dist/{ProcessPreview-Cyk6uv-w.d.ts → ProcessPreview-DSUIJi5V.d.ts} +2 -3
- package/packages/sdk/dist/components/index.d.mts +41 -42
- package/packages/sdk/dist/components/index.d.ts +41 -42
- package/packages/sdk/dist/{dataManagementApi-CE8Zyj3a.d.ts → dataManagementApi-Ro9itwm8.d.ts} +2 -2
- package/packages/sdk/dist/{dataManagementApi-4fSaCA5t.d.mts → dataManagementApi-p_HOhQnA.d.mts} +2 -2
- package/packages/sdk/dist/runtime/index.d.mts +3 -4
- package/packages/sdk/dist/runtime/index.d.ts +3 -4
- package/packages/sdk/dist/runtime/react.d.mts +5 -5
- package/packages/sdk/dist/runtime/react.d.ts +5 -5
- package/packages/sdk/src/build-source/scripts/utils/mime-types.mjs +70 -70
- package/packages/sdk/src/build-source/scripts/utils/oss-client.mjs +128 -128
- package/packages/sdk/src/build-source/scripts/utils/progress.mjs +57 -57
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +1 -0
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +6 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +1 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +6 -0
- package/templates/openxiangda-react-spa/AGENTS.md +8 -0
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +2 -0
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +7 -1
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +1 -0
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +7 -1
- package/templates/sy-lowcode-app-workspace/AGENTS.md +9 -1
package/lib/config.js
CHANGED
|
@@ -9,7 +9,6 @@ const CONFIG_FILE = path.join(CONFIG_DIR, 'profiles.json');
|
|
|
9
9
|
const GLOBAL_ENV_FILE = path.join(CONFIG_DIR, '.env');
|
|
10
10
|
const PROJECT_DIR = '.openxiangda';
|
|
11
11
|
const PROJECT_STATE_FILE = path.join(PROJECT_DIR, 'state.json');
|
|
12
|
-
const LEGACY_PROJECT_CONFIG_FILE = path.join(PROJECT_DIR, 'profiles.json');
|
|
13
12
|
const PROJECT_STATE_LOCK_TIMEOUT_MS = 15_000;
|
|
14
13
|
const PROJECT_STATE_STALE_LOCK_MS = 30_000;
|
|
15
14
|
const projectStateBaselines = new WeakMap();
|
|
@@ -47,74 +46,78 @@ function normalizeConfig(config) {
|
|
|
47
46
|
};
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
const merged = emptyConfig();
|
|
52
|
-
for (const config of configs) {
|
|
53
|
-
if (!config) continue;
|
|
54
|
-
const normalized = normalizeConfig(config);
|
|
55
|
-
merged.version = normalized.version || merged.version;
|
|
56
|
-
if (normalized.currentProfile) {
|
|
57
|
-
merged.currentProfile = normalized.currentProfile;
|
|
58
|
-
}
|
|
59
|
-
for (const [name, profile] of Object.entries(normalized.profiles)) {
|
|
60
|
-
merged.profiles[name] = mergeProfile(merged.profiles[name], profile);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return normalizeConfig(merged);
|
|
64
|
-
}
|
|
49
|
+
const configPaths = new WeakMap();
|
|
65
50
|
|
|
66
|
-
function
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
previous[key] != null
|
|
78
|
-
) {
|
|
79
|
-
merged[key] = previous[key];
|
|
80
|
-
}
|
|
51
|
+
function workspaceRoot(cwd = process.cwd()) {
|
|
52
|
+
const start = path.resolve(cwd);
|
|
53
|
+
if (!fs.existsSync(start)) return start;
|
|
54
|
+
let directory = start;
|
|
55
|
+
while (directory !== os.homedir()) {
|
|
56
|
+
if (['app-workspace.config.ts', 'app-workspace.config.js', 'openxiangda.config.ts', '.openxiangda/profiles.json']
|
|
57
|
+
.some(file => fs.existsSync(path.join(directory, file)))) return directory;
|
|
58
|
+
if (fs.existsSync(path.join(directory, '.git'))) return start;
|
|
59
|
+
const parent = path.dirname(directory);
|
|
60
|
+
if (parent === directory) break;
|
|
61
|
+
directory = parent;
|
|
81
62
|
}
|
|
82
|
-
return
|
|
63
|
+
return start;
|
|
83
64
|
}
|
|
84
65
|
|
|
85
|
-
function
|
|
86
|
-
return
|
|
66
|
+
function getConfigFile(config, cwd = process.cwd()) {
|
|
67
|
+
return configPaths.get(config) || path.join(workspaceRoot(cwd), PROJECT_DIR, 'profiles.json');
|
|
87
68
|
}
|
|
88
69
|
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
70
|
+
function assertConfigFile(file) {
|
|
71
|
+
if (file === CONFIG_FILE) throw new Error('请进入项目工作区后登录;不再使用用户主目录的全局登录态');
|
|
72
|
+
for (const target of [path.dirname(file), file]) {
|
|
73
|
+
try {
|
|
74
|
+
const info = fs.lstatSync(target);
|
|
75
|
+
if (info.isSymbolicLink() || (target === file && (!info.isFile() || info.size > 1024 * 1024))) {
|
|
76
|
+
throw new Error(`工作区登录态必须是普通文件: ${file}`);
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error.code !== 'ENOENT') throw error;
|
|
98
80
|
}
|
|
99
|
-
return migrated;
|
|
100
81
|
}
|
|
82
|
+
}
|
|
101
83
|
|
|
102
|
-
|
|
84
|
+
function loadConfig(cwd = process.cwd(), options = {}) {
|
|
85
|
+
const file = options.exact
|
|
86
|
+
? path.join(path.resolve(cwd), PROJECT_DIR, 'profiles.json')
|
|
87
|
+
: getConfigFile(null, cwd);
|
|
88
|
+
assertConfigFile(file);
|
|
89
|
+
let config;
|
|
90
|
+
try {
|
|
91
|
+
config = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
92
|
+
if (!config || typeof config !== 'object' || Array.isArray(config) ||
|
|
93
|
+
!config.profiles || typeof config.profiles !== 'object' || Array.isArray(config.profiles)) {
|
|
94
|
+
throw new Error('invalid configuration');
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error.code !== 'ENOENT') throw new Error(`工作区登录态无法解析,请在当前工作区重新登录: ${file}`);
|
|
98
|
+
}
|
|
99
|
+
const result = normalizeConfig(config);
|
|
100
|
+
configPaths.set(result, file);
|
|
101
|
+
return result;
|
|
103
102
|
}
|
|
104
103
|
|
|
105
104
|
function saveConfig(config) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
fs.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
105
|
+
const file = getConfigFile(config);
|
|
106
|
+
assertConfigFile(file);
|
|
107
|
+
const directory = path.dirname(file);
|
|
108
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
109
|
+
fs.chmodSync(directory, 0o700);
|
|
110
|
+
const ignore = path.join(directory, '.gitignore');
|
|
111
|
+
let current = '';
|
|
113
112
|
try {
|
|
114
|
-
fs.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
if (!fs.lstatSync(ignore).isFile() || fs.lstatSync(ignore).isSymbolicLink()) throw new Error('登录态忽略文件必须是普通文件');
|
|
114
|
+
current = fs.readFileSync(ignore, 'utf8');
|
|
115
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
116
|
+
const missing = ['/profiles.json', '/profiles.json.*'].filter(entry => !current.split(/\r?\n/).includes(entry));
|
|
117
|
+
if (missing.length) fs.writeFileSync(ignore, `${current}${current && !current.endsWith('\n') ? '\n' : ''}${missing.join('\n')}\n`, { mode: 0o600 });
|
|
118
|
+
atomicWriteJson(file, normalizeConfig(config));
|
|
119
|
+
fs.chmodSync(file, 0o600);
|
|
120
|
+
configPaths.set(config, file);
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
function normalizeBaseUrl(value) {
|
|
@@ -441,6 +444,7 @@ module.exports = {
|
|
|
441
444
|
GLOBAL_ENV_FILE,
|
|
442
445
|
PROJECT_STATE_FILE,
|
|
443
446
|
getProfile,
|
|
447
|
+
getConfigFile,
|
|
444
448
|
loadGlobalEnv,
|
|
445
449
|
loadConfig,
|
|
446
450
|
loadProjectState,
|
package/lib/design-gates.js
CHANGED
|
@@ -540,6 +540,23 @@ const RESOURCE_EXPLAINS = {
|
|
|
540
540
|
'openxiangda function invoke summarize_customer --body-json \'{"input":{}}\'',
|
|
541
541
|
],
|
|
542
542
|
},
|
|
543
|
+
webhook: {
|
|
544
|
+
dir: 'src/resources/webhooks/*.json',
|
|
545
|
+
minimalManifest: {
|
|
546
|
+
code: 'yuquan_access',
|
|
547
|
+
name: '玉泉门禁开门事件',
|
|
548
|
+
targetFunctionCode: 'qfyy_access_event',
|
|
549
|
+
idempotencyQueryParam: 'nonce',
|
|
550
|
+
maxBodyBytes: 262144,
|
|
551
|
+
status: 'active',
|
|
552
|
+
},
|
|
553
|
+
commands: [
|
|
554
|
+
'openxiangda resource validate webhook --profile <name>',
|
|
555
|
+
'openxiangda resource plan webhook --only yuquan_access --profile <name> --json',
|
|
556
|
+
'openxiangda resource publish webhook --only yuquan_access --change <id> --profile <name>',
|
|
557
|
+
'openxiangda webhook deliveries yuquan_access --profile <name> --json',
|
|
558
|
+
],
|
|
559
|
+
},
|
|
543
560
|
connector: {
|
|
544
561
|
dir: 'src/resources/connectors/*.json',
|
|
545
562
|
minimalManifest: {
|
|
@@ -633,6 +650,7 @@ function getResourceExplain(type) {
|
|
|
633
650
|
auth: 'auth-config',
|
|
634
651
|
authconfigs: 'auth-config',
|
|
635
652
|
functions: 'function',
|
|
653
|
+
webhooks: 'webhook',
|
|
636
654
|
connectors: 'connector',
|
|
637
655
|
};
|
|
638
656
|
const key = aliases[rawKey] || rawKey;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Static guidance: no registry requests, installation or workspace mutations.
|
|
2
|
+
function migrationAdvice() {
|
|
3
|
+
return {
|
|
4
|
+
generation: 'v1',
|
|
5
|
+
message: '建议评估升级到 OpenXiangda 2.0;新应用优先使用 V2,现有应用先核实能力覆盖、迁移成本与验收方案。',
|
|
6
|
+
launcherInstallCommand: 'npm install -g openxiangda@latest --registry=https://registry.npmjs.org',
|
|
7
|
+
nodeRequirement: '>=24',
|
|
8
|
+
assessCommand: 'openxiangda migrate assess --to v2',
|
|
9
|
+
guide: 'https://github.com/1377385356/openxiangda/blob/master/docs/getting-started.md#upgrade',
|
|
10
|
+
workspacePolicy: '更新统一入口后旧项目仍使用 V1;不会自动转换应用、数据或流程。',
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function printMigrationAdvice() {
|
|
15
|
+
const advice = migrationAdvice();
|
|
16
|
+
process.stderr.write(`\n${advice.message}\nNode.js 24+:${advice.launcherInstallCommand}\n使用新版全局入口在旧项目评估:${advice.assessCommand}\n${advice.workspacePolicy}\nCLI / Skill / MCP 安装升级说明:${advice.guide}\n`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { migrationAdvice, printMigrationAdvice };
|
package/lib/release-plan.js
CHANGED
|
@@ -102,6 +102,7 @@ const DIRECT_RESOURCE_TYPE_BY_KEY = Object.freeze({
|
|
|
102
102
|
menus: 'menu',
|
|
103
103
|
dataViews: 'data-view',
|
|
104
104
|
storageConfigs: 'storage',
|
|
105
|
+
webhooks: 'webhook',
|
|
105
106
|
authConfigs: 'auth-config',
|
|
106
107
|
routes: 'route',
|
|
107
108
|
publicAccessPolicies: 'public-access',
|
|
@@ -116,6 +117,7 @@ const DIRECT_RESOURCE_RELEASE_ORDER = Object.freeze([
|
|
|
116
117
|
'roles',
|
|
117
118
|
'connectors',
|
|
118
119
|
'storageConfigs',
|
|
120
|
+
'webhooks',
|
|
119
121
|
'authConfigs',
|
|
120
122
|
'routes',
|
|
121
123
|
'publicAccessPolicies',
|
package/lib/workspace-init.js
CHANGED
|
@@ -41,6 +41,7 @@ function initWorkspace(options = {}) {
|
|
|
41
41
|
throw new Error('workspace init 绑定应用时必须同时提供 --profile 和 --app-type');
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
const resolvedProfile = profileName ? getProfile(loadConfig(targetDir, { exact: true }), profileName) : null;
|
|
44
45
|
ensureCanInitialize(targetDir, force, templateDir);
|
|
45
46
|
copyTemplate(templateDir, targetDir, {
|
|
46
47
|
__WORKSPACE_PACKAGE_NAME__: packageName,
|
|
@@ -49,8 +50,7 @@ function initWorkspace(options = {}) {
|
|
|
49
50
|
|
|
50
51
|
let bound = null;
|
|
51
52
|
if (profileName && appType) {
|
|
52
|
-
const
|
|
53
|
-
const resolved = getProfile(config, profileName);
|
|
53
|
+
const resolved = resolvedProfile;
|
|
54
54
|
const state = {
|
|
55
55
|
version: 1,
|
|
56
56
|
profiles: {
|
|
@@ -125,11 +125,18 @@ function ensureCanInitialize(targetDir, force, templateDir) {
|
|
|
125
125
|
const entries = fs
|
|
126
126
|
.readdirSync(targetDir)
|
|
127
127
|
.filter(name => !['.DS_Store'].includes(name));
|
|
128
|
-
if (entries.length > 0 && !force) {
|
|
128
|
+
if (entries.length > 0 && !force && !isAuthOnlyWorkspace(targetDir, entries)) {
|
|
129
129
|
throw new Error(`目标目录非空: ${targetDir}。如需写入请传 --force`);
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
function isAuthOnlyWorkspace(targetDir, entries) {
|
|
134
|
+
if (entries.length !== 1 || entries[0] !== '.openxiangda') return false;
|
|
135
|
+
const directory = path.join(targetDir, '.openxiangda');
|
|
136
|
+
if (!fs.lstatSync(directory).isDirectory() || fs.lstatSync(directory).isSymbolicLink()) return false;
|
|
137
|
+
return fs.readdirSync(directory).every(name => ['profiles.json', '.gitignore'].includes(name) && fs.lstatSync(path.join(directory, name)).isFile() && !fs.lstatSync(path.join(directory, name)).isSymbolicLink());
|
|
138
|
+
}
|
|
139
|
+
|
|
133
140
|
function normalizeRuntime(value) {
|
|
134
141
|
return value === 'react-spa' || value === 'spa' ? 'react-spa' : 'legacy';
|
|
135
142
|
}
|
|
@@ -154,7 +161,7 @@ function copyTemplate(sourceDir, targetDir, replacements, baseDir = sourceDir) {
|
|
|
154
161
|
function shouldSkipTemplateEntry(baseDir, sourcePath, entryName) {
|
|
155
162
|
if (TEMPLATE_IGNORE_NAMES.has(entryName)) return true;
|
|
156
163
|
const relativePath = path.relative(baseDir, sourcePath).split(path.sep).join('/');
|
|
157
|
-
return TEMPLATE_IGNORE_PATHS.has(relativePath);
|
|
164
|
+
return TEMPLATE_IGNORE_PATHS.has(relativePath) || /^\.openxiangda\/(profiles\.json|session\.json)(\.|$)/.test(relativePath);
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
function applyReplacements(content, replacements) {
|
|
@@ -19,6 +19,10 @@ If the workspace contains `app-workspace.config.ts`, forms/pages/resource manife
|
|
|
19
19
|
|
|
20
20
|
For an existing V1 project, proactively check whether V2 covers its required capabilities, whether it is still in testing, and whether migration cost is manageable. When those conditions hold, recommend V2 first and explain the benefit, rebuild scope and verification cost. Use the unified entry's `openxiangda migrate assess --to v2` for source pointers; confirm the project design, data/workflow mapping, acceptance and rollback before implementing migration. Keep unknown conditions explicit and continue current maintenance with the matching engine until migration is authorized.
|
|
21
21
|
|
|
22
|
+
## Installation and upgrade
|
|
23
|
+
|
|
24
|
+
For CLI, Skill and MCP setup or upgrades, read the [unified installation guide](https://github.com/1377385356/openxiangda/blob/master/docs/getting-started.md#upgrade). Install the global launcher with Node.js 24+ using `npm install -g openxiangda@latest --registry=https://registry.npmjs.org`. Use that global command for `migrate assess`; a project-local V1 CLI does not implement it. Workspace updates remain on `legacy-v1`. Refresh matching skills after dependency updates and restart a configured V2 MCP client to load its new project version. Never automatically replace a V1 project dependency with V2.
|
|
25
|
+
|
|
22
26
|
## 1.x Delivery V2 is the normal 1.x release path
|
|
23
27
|
|
|
24
28
|
When `app-workspace.config.ts` declares `deliveryVersion: 2`, all later V1 SDD,
|
|
@@ -185,6 +189,8 @@ Treat resource declarations as environment mapping and release-observability met
|
|
|
185
189
|
|
|
186
190
|
An App Function may declare metadata-only top-level `secretRefs: [{ name, required }]` only with `function_v2` + `trusted_node_v2`; source resolves values with `await ctx.secrets.get(name)` and uses `ctx.utils.http` for controlled public HTTPS. Create/rotate values through hidden TTY or `openxiangda secret ... --value-stdin --change <change> --profile <name>`. Never put values in arguments, files, manifests, state, plans, logs, errors, or chat. Secret bindings require `backend_release_v2` and whole-app `atomic_staged_children_v2`; a missing capability is fail-closed and never uses the legacy source PATCH. For whole-app activation use exact-scope `resource publish <type> --only <code> --stage-only`, then pass the returned verified `stagedResource` to `release app-finalize`; an active Backend Release is never labeled staged.
|
|
187
191
|
|
|
192
|
+
Inbound third-party callbacks use `src/resources/webhooks/<code>.json` and the public path returned after publish. A Webhook binds one fixed same-app Function; it never stores Secret values or provider signature rules. The Function reads the exact `input.rawBody`, verifies before any data/helper/network call, and protects business writes with `input.idempotencyKey`. Use `openxiangda webhook deliveries|delivery` for receipts and read `references/webhooks.md` through the workflow-automation skill for the full contract.
|
|
193
|
+
|
|
188
194
|
The lease is app-level promotion ownership, while worktree ownership prevents two Codex tasks from editing through the same source directory. Different worktrees may keep developing and validating; only the clean synchronized main checkout publishes. Use full resource/runtime publish only when the approved bundle intentionally covers the whole dependency closure.
|
|
189
195
|
|
|
190
196
|
## Always
|
|
@@ -229,3 +235,5 @@ The lease is app-level promotion ownership, while worktree ownership prevents tw
|
|
|
229
235
|
- `../openxiangda-open-api/SKILL.md`
|
|
230
236
|
|
|
231
237
|
Load a reference only when the selected subskill explicitly requires it. Workspace `AGENTS.md` remains authoritative for project-specific constraints.
|
|
238
|
+
|
|
239
|
+
平台登录态只使用当前工作区的 `.openxiangda/profiles.json`,不再读取或合并全局 profiles。升级后请进入每个项目运行 `openxiangda login <platform-url>`。登录文件自动加入 Git 忽略规则,不得提交或打包。
|