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.
Files changed (42) hide show
  1. package/README.md +20 -1
  2. package/lib/cli.js +417 -24
  3. package/lib/config.js +60 -56
  4. package/lib/design-gates.js +18 -0
  5. package/lib/migration-advice.js +19 -0
  6. package/lib/release-plan.js +2 -0
  7. package/lib/workspace-init.js +11 -4
  8. package/openxiangda-skills/SKILL.md +8 -0
  9. package/openxiangda-skills/references/architecture-patterns.md +218 -218
  10. package/openxiangda-skills/references/component-guide.md +122 -122
  11. package/openxiangda-skills/references/openxiangda-api.md +12 -0
  12. package/openxiangda-skills/references/platform-data-model.md +285 -285
  13. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +32 -2
  14. package/openxiangda-skills/references/troubleshooting.md +224 -224
  15. package/openxiangda-skills/references/webhooks.md +213 -0
  16. package/openxiangda-skills/references/workspace-state.md +1 -1
  17. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +1 -1
  18. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +3 -0
  19. package/package.json +26 -23
  20. package/packages/sdk/dist/{ProcessPreview-Cyk6uv-w.d.mts → ProcessPreview-DSUIJi5V.d.mts} +2 -3
  21. package/packages/sdk/dist/{ProcessPreview-Cyk6uv-w.d.ts → ProcessPreview-DSUIJi5V.d.ts} +2 -3
  22. package/packages/sdk/dist/components/index.d.mts +41 -42
  23. package/packages/sdk/dist/components/index.d.ts +41 -42
  24. package/packages/sdk/dist/{dataManagementApi-CE8Zyj3a.d.ts → dataManagementApi-Ro9itwm8.d.ts} +2 -2
  25. package/packages/sdk/dist/{dataManagementApi-4fSaCA5t.d.mts → dataManagementApi-p_HOhQnA.d.mts} +2 -2
  26. package/packages/sdk/dist/runtime/index.d.mts +3 -4
  27. package/packages/sdk/dist/runtime/index.d.ts +3 -4
  28. package/packages/sdk/dist/runtime/react.d.mts +5 -5
  29. package/packages/sdk/dist/runtime/react.d.ts +5 -5
  30. package/packages/sdk/src/build-source/scripts/utils/mime-types.mjs +70 -70
  31. package/packages/sdk/src/build-source/scripts/utils/oss-client.mjs +128 -128
  32. package/packages/sdk/src/build-source/scripts/utils/progress.mjs +57 -57
  33. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +1 -0
  34. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +6 -0
  35. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +1 -0
  36. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +6 -0
  37. package/templates/openxiangda-react-spa/AGENTS.md +8 -0
  38. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +2 -0
  39. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +7 -1
  40. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +1 -0
  41. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +7 -1
  42. 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
- function mergeConfigs(...configs) {
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 mergeProfile(previous = {}, next = {}) {
67
- const merged = {
68
- ...previous,
69
- ...next,
70
- };
71
- const baseUrlChanged =
72
- previous.baseUrl && next.baseUrl && previous.baseUrl !== next.baseUrl;
73
- for (const key of ['token', 'user', 'tenant']) {
74
- if (
75
- !baseUrlChanged &&
76
- !Object.prototype.hasOwnProperty.call(next, key) &&
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 merged;
63
+ return start;
83
64
  }
84
65
 
85
- function isConfigEqual(left, right) {
86
- return JSON.stringify(normalizeConfig(left)) === JSON.stringify(normalizeConfig(right));
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 loadConfig(cwd = process.cwd()) {
90
- ensureUserConfigDir();
91
- const globalConfig = readJson(CONFIG_FILE, null);
92
- const legacyConfig = readJson(path.join(cwd, LEGACY_PROJECT_CONFIG_FILE), null);
93
-
94
- if (legacyConfig?.profiles && Object.keys(legacyConfig.profiles).length > 0) {
95
- const migrated = mergeConfigs(legacyConfig, globalConfig);
96
- if (!globalConfig || !isConfigEqual(globalConfig, migrated)) {
97
- saveConfig(migrated);
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
- return normalizeConfig(globalConfig);
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
- ensureUserConfigDir();
107
- const normalized = normalizeConfig(config);
108
- const tempFile = `${CONFIG_FILE}.${process.pid}.tmp`;
109
- fs.writeFileSync(tempFile, `${JSON.stringify(normalized, null, 2)}\n`, {
110
- mode: 0o600,
111
- });
112
- fs.renameSync(tempFile, CONFIG_FILE);
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.chmodSync(CONFIG_FILE, 0o600);
115
- } catch {
116
- // chmod is best-effort on non-POSIX filesystems.
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,
@@ -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 };
@@ -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',
@@ -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 config = loadConfig();
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 忽略规则,不得提交或打包。