openxiangda 1.0.163 → 1.0.165

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 (41) hide show
  1. package/README.md +6 -2
  2. package/lib/change-baseline.js +88 -2
  3. package/lib/cli.js +2013 -146
  4. package/lib/publish-lease.js +13 -1
  5. package/lib/release-mainline.js +545 -0
  6. package/lib/runtime-lineage.js +19 -12
  7. package/lib/sdd.js +137 -0
  8. package/lib/utils.js +76 -2
  9. package/lib/workspace-bootstrap.js +36 -7
  10. package/lib/workspace-init.js +12 -3
  11. package/lib/worktree-owner.js +13 -0
  12. package/openxiangda-skills/SKILL.md +10 -4
  13. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +24 -2
  14. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +10 -2
  15. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +2 -0
  16. package/package.json +4 -1
  17. package/packages/sdk/dist/runtime/index.cjs +164 -48
  18. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  19. package/packages/sdk/dist/runtime/index.d.mts +1 -1
  20. package/packages/sdk/dist/runtime/index.d.ts +1 -1
  21. package/packages/sdk/dist/runtime/index.mjs +164 -48
  22. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  23. package/packages/sdk/dist/runtime/react.cjs +164 -48
  24. package/packages/sdk/dist/runtime/react.cjs.map +1 -1
  25. package/packages/sdk/dist/runtime/react.d.mts +79 -1
  26. package/packages/sdk/dist/runtime/react.d.ts +79 -1
  27. package/packages/sdk/dist/runtime/react.mjs +164 -48
  28. package/packages/sdk/dist/runtime/react.mjs.map +1 -1
  29. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +2 -1
  30. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +2 -2
  31. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +2 -1
  32. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +3 -3
  33. package/templates/openxiangda-react-spa/AGENTS.md +9 -5
  34. package/templates/openxiangda-react-spa/scripts/deploy.mjs +19 -39
  35. package/templates/openxiangda-react-spa/scripts/guard-publish.mjs +4 -5
  36. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +3 -1
  37. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +1 -1
  38. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +2 -1
  39. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +1 -1
  40. package/templates/sy-lowcode-app-workspace/AGENTS.md +5 -3
  41. package/templates/sy-lowcode-app-workspace/scripts/guard-publish.mjs +4 -5
package/README.md CHANGED
@@ -56,7 +56,9 @@ User tokens are stored in `~/.openxiangda/profiles.json` with `0600` permissions
56
56
 
57
57
  React SPA workspaces publish their frontend with `openxiangda runtime deploy`. Every finalized Runtime release is built from a clean, committed Git `HEAD`; the CLI freezes `sourceRevision`, the current active release, and its source revision before any build/upload. Deploy fails with `RUNTIME_SOURCE_BASE_DIVERGED` when its `HEAD` does not descend from the online Runtime source, including with `--no-activate`, so an old isolated worktree cannot stage and later activate a silent rollback. `.openxiangda/`, `openspec/`, `dist/`, and other pure generated/governance/state paths do not make the source dirty, but `--no-build` cannot bypass the lineage gate. An intentional rollback requires `--allow-runtime-rollback --reason "<at least 8 characters>"`, which is persisted for audit and never bypasses dirty/non-Git checks.
58
58
 
59
- Concurrent development may still upload immutable previews with `--no-activate`, but the preview must also freeze a real clean Git source revision before it can become a release. Final promotion uses `release begin/end` so one `(tenant, app)` has one writer while other worktrees keep developing and testing. `release begin --change <change>` binds the per-task `clientSessionId` (Codex uses `CODEX_THREAD_ID`) and freezes both the reviewed Git source base and the selected remote resource-field heads. Before the first live write, the CLI preflights the complete selected set. A stale resource worktree fails with `SOURCE_BASE_DIVERGED`; a selected remote field that moved after the baseline fails with `RESOURCE_FIELD_CONFLICT`. Resolve the divergence, rebuild/re-plan, and start a new release instead of retrying a stale payload. The default upload mode is `auto`: it tries staged multipart first, then falls back to built-in OSS direct upload if the staged file API returns HTTP 403. Use `--upload-mode oss-direct` to force built-in OSS direct upload, or `--upload-mode legacy-json` only as a compatibility fallback for older platforms. Upload progress, timeout, and `traceId` are written to stderr; `--json` stdout remains the final JSON result.
59
+ Concurrent development may still upload immutable previews with `--no-activate`, but the preview must also freeze a real clean Git source revision before it can become a release. Final promotion uses `release begin/end` so one `(tenant, app)` has one writer while other worktrees keep developing and testing. `release begin --change <change>` binds the per-task `clientSessionId` (Codex uses `CODEX_THREAD_ID`), keeps the earlier change base separate, and freezes the actual clean publish `HEAD` plus the selected remote resource-field heads. That publish commit must already contain the current authoritative remote default branch, and it cannot change during the release. Before the first live write, the CLI preflights the complete selected set. A stale resource worktree fails with `SOURCE_BASE_DIVERGED`/`RELEASE_SOURCE_BEHIND_MAIN`; a selected remote field that moved after the baseline fails with `RESOURCE_FIELD_CONFLICT`. Resolve the divergence, rebuild/re-plan, and start a new release instead of retrying a stale payload. The default upload mode is `auto`: it tries staged multipart first, then falls back to built-in OSS direct upload if the staged file API returns HTTP 403. Use `--upload-mode oss-direct` to force built-in OSS direct upload, or `--upload-mode legacy-json` only as a compatibility fallback for older platforms. Upload progress, timeout, and `traceId` are written to stderr; `--json` stdout remains the final JSON result.
60
+
61
+ After a live promotion, merge or fast-forward the exact frozen publish commit into the authoritative default branch and push it before closing the task. `openxiangda release integration-status --profile <name>` checks the live remote tip; normal `release end` keeps the reconciliation evidence and returns `RELEASE_MAIN_MERGE_PENDING` until that exact SHA is present. Squash/rebase does not preserve the published commit and therefore does not pass. The authoritative target is always the live default `main`/`master` of `origin` (or the repository's sole remote); CLI flags, environment variables, local-only branches, and lease expiry cannot bypass it. OpenXiangda's own `npm publish` and the platform image release script apply the stronger pre-release form of the same rule: root and pinned submodule commits must already be in their remote default branches.
60
62
 
61
63
  Source-triggered Function and Automation publishing is source-only by default. On a current platform, every eligible existing Function and Automation in one command is uploaded, prepared, verified, and activated through one immutable Backend Release transaction; a stale target therefore causes zero resource writes instead of failing halfway through 88 Functions and 11 Automations. The release preserves online `resourceBindings`, input/output contracts, descriptive metadata, trigger/view configuration, and enabled/published state, and planned noops do not advance resource versions or timestamps. Only an older platform whose Backend Release `head` route explicitly returns HTTP 404 uses the compatibility per-resource source PATCH, with a warning on stderr. To intentionally make a complete local manifest replace the online definition, select exact codes and opt in explicitly with `--replace-manifest --reason "<why this replacement is safe>"`; `--sdd-bypass` does not widen that scope.
62
64
 
@@ -121,7 +123,7 @@ openxiangda resource publish function --code customer_get --change <change> --pr
121
123
 
122
124
  Exact `--only` / `--code` selectors are applied before unrelated manifests, source dependencies, and JS_CODE targets are read or built. Shared/transitive dependencies of the selected targets remain in scope; omitting a selector intentionally preserves full-workspace validation and planning. Resource commands use the packaged canonical scoped builder for standard workspaces, so an older checked-in `scripts/build-js-code.mjs` does not need to be upgraded before the installed CLI gains this optimization; refresh the workspace template only when developers also need the same behavior from a manual `pnpm build-js-code` command.
123
125
 
124
- For source-only Function/Automation changes, the final command does not reconstruct whole definitions with client-side GET+PUT. `release begin` captures the Git/change baseline, one preflight covers every selected code, and Backend Release performs one `prepare -> verify -> activate` sequence for all eligible updates. Inspect history with `openxiangda release backend-head|backend-list|backend-detail`, compare it with `backend-diff`, or create an audited immutable rollback with `backend-rollback <releaseId> --change <change> --reason "..."`. Always run `release end` when promotion finishes or is abandoned.
126
+ For source-only Function/Automation changes, the final command does not reconstruct whole definitions with client-side GET+PUT. `release begin` captures the Git/change baseline, one preflight covers every selected code, and Backend Release performs one `prepare -> verify -> activate` sequence for all eligible updates. Inspect history with `openxiangda release backend-head|backend-list|backend-detail`, compare it with `backend-diff`, or create an audited immutable rollback with `backend-rollback <releaseId> --change <change> --reason "..."`. Always merge/push the frozen SHA and run `release end` when promotion finishes. A lost or expired lease retains the pending-mainline evidence instead of silently clearing it.
125
127
 
126
128
  Page repair publishing is staged by default. It first freezes `pages/snapshot`, sends the active Page Release parent plus every page revision, and uses revision `0` only for a genuinely new page. Review with `openxiangda page head|releases|detail|diff`; activate an immutable complete release explicitly with `page activate <releaseId> --change <change>`. Historical activation requires `page rollback <releaseId> --rollback --change <change> --reason "..."`. Parent or revision conflicts are never refreshed or retried automatically.
127
129
 
@@ -421,6 +423,8 @@ const PublicAccessError = ({ error }: { error: { message?: string } }) => (
421
423
 
422
424
  后端业务逻辑优先声明为 App Function:源码放在 `src/functions/<functionCode>/index.ts`,资源 manifest 放在 `src/resources/functions/<functionCode>.json`。函数运行在 trusted_node 中,通过 `ctx.form.queryOne/queryMany/getById/createOne/updateOne/updateById`、`ctx.dataView`、`ctx.connector`、`ctx.notification`、`ctx.organization`、`ctx.platform.roles`、`ctx.platform.api` 等受控 API 访问平台能力;自动化、流程和运行时接口都可以调用同一个 function。角色查询和角色成员维护优先使用 `ctx.platform.roles.list/findByCode/addUsers/removeUser`,底层 `ctx.platform.api` 返回 HTTP 包装和平台 envelope,需要业务代码自行解包。运行时页面调用会在 `ctx.operator`、`ctx.currentUser`、`ctx.permissions` 中注入可信的 `roleCodes`、`currentRoleCode`、`hasFullAccess`、`isAppAdmin`、`isPlatformAdmin`,敏感动作必须读取这些服务端上下文做授权,不要信任页面 input 里传入的角色字段。`ctx.form.createOne/updateOne/updateById` 是后端受控写入,授权边界是函数 manifest 的 `resources.forms` 绑定和函数调用授权,不是页面用户对目标表单的直接提交入口;报名、签到、福利选择等内部多表写入应走 App Function,不要为了写内部表单给普通用户开放原始 submit 权限。JS_CODE V2 仍兼容,但新逻辑建议写成 function,再由 `function_call` 节点、`sdk.function.invoke(code, { input })` 或 `/:appType/v1/functions/:code/invoke.json` 调用。直接运行时接口默认要求调用者具备应用自动化管理权限;普通页面调用必须在函数 `definitionJson.runtimeInvoke.audience` 声明 `authenticated`、`page_permission_group`、`app_roles` 或 `scope_policy`,`roleCodes` 只用于当前应用角色精确匹配,不支持 `"*"` / `"all-app-roles"`。自动化/流程内部调用走服务端受控上下文。内部业务表单需要关闭原始写入接口时,在表单 settings 中设置 `runtimeWrite.mode="function_only"`。
423
425
 
426
+ App Function 访问第三方凭据时使用 `app_function_secrets_v1`:manifest 顶层只声明 `secretRefs: [{ "name": "dingtalk_org_app_key", "required": true }]`,同时使用 `definitionJson.version="function_v2"`、`runtimeContractVersion="trusted_node_v2"`;源码通过 `await ctx.secrets.get(name)` 解析,并通过 `ctx.utils.http` 访问受控公网 HTTPS(该桥接不会携带平台 Runtime token)。值只能经 `openxiangda secret create|rotate --value-stdin --change <id> --profile <name>` 或隐藏 TTY 输入,禁止进入 Git、manifest、源码、构建产物、plan、日志或异常。带 `secretRefs` 的 Function 必须走 `backend_release_v2`;需要整应用原子发布时先执行 `resource publish function --only <code> --stage-only`,再把返回的真实 `stagedResource` 交给 `release app-finalize --staged-resources-json ...` 完成 `atomic_staged_children_v2`。默认直接激活的 Backend Release 只返回 `activeResource`,不会伪装成 staged;旧平台 capability 不完整时 CLI 会失败关闭,绝不忽略绑定。
427
+
424
428
  平台部门和账号管理走 app-scoped organization 能力。调用者必须在目标应用拥有 `app:organization:manage`;平台管理员天然可用,普通应用角色需要显式授权。Runtime service principal 不会直接放行,`ctx.organization` 会按真实操作人 / audit actor 校验权限。新接口只提供创建、更新、查询和密码专用操作,不提供删除;`account-update` 不能携带 `password`,重置他人密码必须走 `account-reset-password`,当前用户改密用 SDK / `ctx.organization.accounts.changeMyPassword({ oldPassword, newPassword })`。
425
429
 
426
430
  CLI 写操作必须加 `--force`:
@@ -111,6 +111,75 @@ function clearChangeBaseline(target, baselineIdOrOptions, maybeOptions = {}) {
111
111
  return true;
112
112
  }
113
113
 
114
+ function clearChangeBaselineForReconciliation(target, expected = {}, options = {}) {
115
+ const baseline = target?.bound?.promotion?.changeBaseline;
116
+ if (!baseline || typeof baseline !== 'object' || Array.isArray(baseline)) return false;
117
+ assertStoredChangeBaseline(target, baseline, {
118
+ ...options,
119
+ access: 'reconciliation-read',
120
+ });
121
+
122
+ const expectedBaselineId = normalizeOptionalString(expected.baselineId || expected.id);
123
+ const expectedChangeId = normalizeOptionalString(expected.changeId);
124
+ const expectedClientSessionId = normalizeOptionalString(expected.clientSessionId);
125
+ const expectedSourceCommit = normalizeOptionalString(
126
+ expected.releaseSourceRevision?.baseCommit || expected.sourceCommit
127
+ );
128
+ const hasExpectedLeaseId = Object.prototype.hasOwnProperty.call(
129
+ expected,
130
+ 'leaseId'
131
+ );
132
+ const expectedLeaseId = normalizeOptionalString(expected.leaseId);
133
+ if (
134
+ !expectedBaselineId ||
135
+ !expectedChangeId ||
136
+ !expectedClientSessionId ||
137
+ !expectedSourceCommit ||
138
+ !hasExpectedLeaseId
139
+ ) {
140
+ throw changeBaselineError(
141
+ 'CHANGE_BASELINE_RECONCILIATION_INVALID',
142
+ 'reconciliation 清理必须提供精确 baselineId/changeId/clientSessionId/sourceCommit/leaseId(无 lease 时显式传 null)'
143
+ );
144
+ }
145
+
146
+ if (
147
+ normalizeOptionalString(baseline.baselineId || baseline.id) !== expectedBaselineId ||
148
+ normalizeOptionalString(baseline.changeId) !== expectedChangeId ||
149
+ normalizeOptionalString(baseline.clientSessionId) !== expectedClientSessionId ||
150
+ normalizeOptionalString(baseline.releaseSourceRevision?.baseCommit) !== expectedSourceCommit
151
+ ) {
152
+ return false;
153
+ }
154
+
155
+ const lease = target?.bound?.promotion?.publishLease;
156
+ if (expectedLeaseId) {
157
+ if (
158
+ !lease ||
159
+ normalizeOptionalString(lease.leaseId) !== expectedLeaseId ||
160
+ normalizeOptionalString(lease.changeId) !== expectedChangeId ||
161
+ normalizeOptionalString(lease.clientSessionId) !== expectedClientSessionId ||
162
+ normalizeOptionalString(lease.workspace?.commit) !== expectedSourceCommit
163
+ ) {
164
+ return false;
165
+ }
166
+ } else if (lease) {
167
+ return false;
168
+ }
169
+
170
+ // Validate both pieces of release evidence before mutating either one, then
171
+ // persist once so a failed baseline CAS can never leave a lease-only partial
172
+ // cleanup behind.
173
+ target.bound.promotion = { ...(target.bound.promotion || {}) };
174
+ delete target.bound.promotion.publishLease;
175
+ delete target.bound.promotion.changeBaseline;
176
+ if (Object.keys(target.bound.promotion).length === 0) {
177
+ delete target.bound.promotion;
178
+ }
179
+ persistTargetState(target, options);
180
+ return true;
181
+ }
182
+
114
183
  function isChangeBaselineOwnedByCurrentSession(baseline, env = process.env) {
115
184
  const ownerThreadId = getChangeBaselineOwnerThreadId(baseline);
116
185
  if (!ownerThreadId) return true;
@@ -129,11 +198,24 @@ function readGitSourceBase(cwd = process.cwd(), baseRef = 'HEAD') {
129
198
  ]).trim();
130
199
  assertGitObjectId(treeHash, 'treeHash');
131
200
 
132
- const remote = tryRunGit(repository.root, [
201
+ let remote = tryRunGit(repository.root, [
133
202
  'config',
134
203
  '--get',
135
204
  'remote.origin.url',
136
205
  ]).trim();
206
+ if (!remote) {
207
+ const remotes = tryRunGit(repository.root, ['remote'])
208
+ .split(/\r?\n/)
209
+ .map(item => item.trim())
210
+ .filter(Boolean);
211
+ if (remotes.length === 1) {
212
+ remote = tryRunGit(repository.root, [
213
+ 'config',
214
+ '--get',
215
+ `remote.${remotes[0]}.url`,
216
+ ]).trim();
217
+ }
218
+ }
137
219
  const commonGitDirValue = tryRunGit(repository.root, [
138
220
  'rev-parse',
139
221
  '--path-format=absolute',
@@ -530,7 +612,10 @@ function assertStoredChangeBaseline(target, baseline, options = {}) {
530
612
  `change baseline 属于 profile ${baselineProfile || '<missing>'},不能用于 ${identity.profile}`
531
613
  );
532
614
  }
533
- if (!isChangeBaselineOwnedByCurrentSession(baseline, options.env || process.env)) {
615
+ if (
616
+ options.access !== 'reconciliation-read' &&
617
+ !isChangeBaselineOwnedByCurrentSession(baseline, options.env || process.env)
618
+ ) {
534
619
  throw ownershipError(
535
620
  getChangeBaselineOwnerThreadId(baseline),
536
621
  currentCodexThreadId(options.env || process.env)
@@ -1068,6 +1153,7 @@ module.exports = {
1068
1153
  buildGitBaseSourceArtifacts,
1069
1154
  canonicalJsonSha256,
1070
1155
  clearChangeBaseline,
1156
+ clearChangeBaselineForReconciliation,
1071
1157
  getStoredChangeBaseline,
1072
1158
  isChangeBaselineOwnedByCurrentSession,
1073
1159
  readGitSourceBase,