openxiangda 1.0.189 → 1.0.191

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/lib/cli.js CHANGED
@@ -2142,14 +2142,120 @@ function buildEnvironmentStatusDiff(status) {
2142
2142
  };
2143
2143
  }
2144
2144
 
2145
- function readStudioGit(args) {
2145
+ function readStudioGit(args, cwd = process.cwd()) {
2146
2146
  const result = spawnSync('git', args, {
2147
- cwd: process.cwd(),
2147
+ cwd,
2148
2148
  encoding: 'utf8',
2149
+ timeout: 5000,
2149
2150
  });
2150
2151
  return result.status === 0 ? String(result.stdout || '').trim() : '';
2151
2152
  }
2152
2153
 
2154
+ function studioGitSnapshot() {
2155
+ const porcelain = readStudioGit([
2156
+ 'status',
2157
+ '--porcelain',
2158
+ '--untracked-files=all',
2159
+ ]);
2160
+ const branch = readStudioGit(['branch', '--show-current']);
2161
+ const commit = readStudioGit(['rev-parse', 'HEAD']);
2162
+ const upstream = readStudioGit([
2163
+ 'rev-parse',
2164
+ '--abbrev-ref',
2165
+ '--symbolic-full-name',
2166
+ '@{upstream}',
2167
+ ]);
2168
+ const upstreamCommit = upstream
2169
+ ? readStudioGit(['rev-parse', upstream])
2170
+ : '';
2171
+ const upstreamParts = upstream.split('/');
2172
+ const remoteName = upstreamParts.length > 1 ? upstreamParts[0] : '';
2173
+ const upstreamBranch = upstreamParts.slice(1).join('/');
2174
+ const remoteHeadCommit =
2175
+ remoteName && upstreamBranch
2176
+ ? readStudioGit([
2177
+ 'ls-remote',
2178
+ '--heads',
2179
+ remoteName,
2180
+ `refs/heads/${upstreamBranch}`,
2181
+ ]).split(/\s+/)[0] || ''
2182
+ : '';
2183
+ const aheadBehind = upstream
2184
+ ? readStudioGit([
2185
+ 'rev-list',
2186
+ '--left-right',
2187
+ '--count',
2188
+ `${upstream}...HEAD`,
2189
+ ])
2190
+ .split(/\s+/)
2191
+ .map(value => Number(value))
2192
+ : [];
2193
+ const worktreePaths = readStudioGit(['worktree', 'list', '--porcelain'])
2194
+ .split(/\r?\n/)
2195
+ .filter(line => line.startsWith('worktree '))
2196
+ .map(line => line.slice('worktree '.length))
2197
+ .filter(Boolean);
2198
+ const dirtyWorktrees = worktreePaths
2199
+ .filter(worktreePath => path.resolve(worktreePath) !== process.cwd())
2200
+ .filter(worktreePath =>
2201
+ Boolean(
2202
+ readStudioGit(
2203
+ ['status', '--porcelain', '--untracked-files=all'],
2204
+ worktreePath
2205
+ )
2206
+ )
2207
+ );
2208
+ const ahead = Number.isFinite(aheadBehind[1]) ? aheadBehind[1] : null;
2209
+ const behind = Number.isFinite(aheadBehind[0]) ? aheadBehind[0] : null;
2210
+ const authoritativeBranch = ['main', 'master'].includes(branch);
2211
+ const upstreamMainline = /\/(?:main|master)$/.test(upstream);
2212
+ const clean = !porcelain;
2213
+ const mainlineAligned =
2214
+ clean &&
2215
+ authoritativeBranch &&
2216
+ upstreamMainline &&
2217
+ commit === upstreamCommit &&
2218
+ commit === remoteHeadCommit &&
2219
+ ahead === 0 &&
2220
+ behind === 0 &&
2221
+ dirtyWorktrees.length === 0;
2222
+ return {
2223
+ branch,
2224
+ commit,
2225
+ upstream: upstream || null,
2226
+ upstreamCommit: upstreamCommit || null,
2227
+ remoteHeadCommit: remoteHeadCommit || null,
2228
+ ahead,
2229
+ behind,
2230
+ clean,
2231
+ mainlineAligned,
2232
+ dirtyWorktrees,
2233
+ worktreeCount: worktreePaths.length,
2234
+ changes: porcelain ? porcelain.split(/\r?\n/).slice(0, 50) : [],
2235
+ };
2236
+ }
2237
+
2238
+ function isProductionCommissioning(environment) {
2239
+ const policy = environment?.sideEffectPolicy || {};
2240
+ return (
2241
+ policy.payments === 'deny' &&
2242
+ policy.notifications === 'tester_allowlist' &&
2243
+ policy.externalWrites === 'deny' &&
2244
+ policy.publicIndexing === 'deny' &&
2245
+ policy.organizationWrites === 'deny' &&
2246
+ policy.scheduledAutomations === 'disabled' &&
2247
+ policy.environmentBanner === true
2248
+ );
2249
+ }
2250
+
2251
+ function isDeploymentEvidenceValid(deployment, now = Date.now()) {
2252
+ if (!deployment?.evidenceHash) return false;
2253
+ const evidence = deployment.evidenceSummary;
2254
+ if (!evidence || evidence.outcome !== 'passed') return false;
2255
+ const validUntil = Date.parse(evidence.validUntil || '');
2256
+ return Number.isFinite(validUntil) && validUntil > now;
2257
+ }
2258
+
2153
2259
  async function studio(args) {
2154
2260
  const { flags } = parseArgs(args);
2155
2261
  const config = loadConfig();
@@ -2165,26 +2271,69 @@ async function studio(args) {
2165
2271
  state,
2166
2272
  state.currentTarget || 'preproduction'
2167
2273
  );
2168
- const remote = await requestWithAuth(
2274
+ const remoteStatus = await requestWithAuth(
2169
2275
  config,
2170
2276
  selected.binding.profile,
2171
2277
  environmentSetApiPath(state.logicalApp.code, '/status')
2172
2278
  );
2173
- const porcelain = readStudioGit([
2174
- 'status',
2175
- '--porcelain',
2176
- '--untracked-files=all',
2177
- ]);
2279
+ const environments = await Promise.all(
2280
+ (remoteStatus?.environments || []).map(async environment => {
2281
+ const deploymentId = environment?.latestDeployment?.id;
2282
+ if (!deploymentId) return environment;
2283
+ try {
2284
+ const latestDeployment = await requestWithAuth(
2285
+ config,
2286
+ selected.binding.profile,
2287
+ applicationDeploymentApiPath(
2288
+ state.logicalApp.code,
2289
+ deploymentId
2290
+ )
2291
+ );
2292
+ return { ...environment, latestDeployment };
2293
+ } catch {
2294
+ return environment;
2295
+ }
2296
+ })
2297
+ );
2298
+ const remote = { ...remoteStatus, environments };
2299
+ const git = studioGitSnapshot();
2300
+ const preproduction = environments.find(
2301
+ environment => environment.kind === 'preproduction'
2302
+ );
2303
+ const production = environments.find(
2304
+ environment => environment.kind === 'production'
2305
+ );
2306
+ const drift = buildEnvironmentStatusDiff(remote).drift;
2307
+ const evidenceValid = isDeploymentEvidenceValid(
2308
+ preproduction?.latestDeployment
2309
+ );
2310
+ const productionCommissioning =
2311
+ isProductionCommissioning(production);
2312
+ const candidateReady = git.mainlineAligned;
2313
+ const testRegistrationReady = Boolean(
2314
+ preproduction?.latestDeployment?.id &&
2315
+ preproduction.latestDeployment.status !== 'failed'
2316
+ );
2317
+ const promotionReady =
2318
+ candidateReady &&
2319
+ productionCommissioning &&
2320
+ evidenceValid &&
2321
+ preproduction?.latestDeployment?.status === 'succeeded' &&
2322
+ Boolean(preproduction.latestDeployment.candidateId);
2178
2323
  return {
2179
2324
  workspace: process.cwd(),
2180
2325
  logicalApp: state.logicalApp,
2181
2326
  currentTarget: state.currentTarget,
2182
2327
  targets: state.targets,
2183
- git: {
2184
- branch: readStudioGit(['branch', '--show-current']),
2185
- commit: readStudioGit(['rev-parse', 'HEAD']),
2186
- clean: !porcelain,
2187
- changes: porcelain ? porcelain.split(/\r?\n/).slice(0, 50) : [],
2328
+ git,
2329
+ drift,
2330
+ deliveryGates: {
2331
+ candidateReady,
2332
+ evidenceValid,
2333
+ productionCommissioning,
2334
+ promotionReady,
2335
+ rollbackReady: Boolean(production?.heads?.appReleaseId),
2336
+ testRegistrationReady,
2188
2337
  },
2189
2338
  openxiangdaVersion: CURRENT_VERSION,
2190
2339
  remote,
@@ -60,7 +60,7 @@ function studioHtml(sessionToken) {
60
60
  .pre{--accent:var(--pre)}.prod{--accent:var(--prod)}.card-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:22px}
61
61
  h2{font-size:19px;margin:0}.badge{border:1px solid color-mix(in srgb,var(--accent) 45%,var(--line));color:var(--accent);padding:5px 9px;border-radius:999px;font-size:11px;letter-spacing:.08em}
62
62
  dl{display:grid;grid-template-columns:130px 1fr;gap:11px;margin:0;font-size:13px}dt{color:var(--muted)}dd{margin:0;font-family:ui-monospace,SFMono-Regular,monospace;overflow-wrap:anywhere}
63
- .wide{grid-column:1/-1}.toolbar{display:flex;flex-wrap:wrap;gap:10px;margin-top:20px}button{appearance:none;border:1px solid #3a3d36;background:#22241f;color:#f4f4ef;padding:10px 14px;border-radius:10px;font-weight:600;cursor:pointer}
63
+ .wide{grid-column:1/-1}.toolbar{display:flex;flex-wrap:wrap;gap:10px;margin-top:20px}.gate-ok{color:var(--prod)}.gate-bad{color:var(--bad)}button{appearance:none;border:1px solid #3a3d36;background:#22241f;color:#f4f4ef;padding:10px 14px;border-radius:10px;font-weight:600;cursor:pointer}
64
64
  button:hover{border-color:#777c70}button.primary{background:#e9efe9;color:#111;border-color:#e9efe9}button.danger{color:#ffaaa3;border-color:#70423e}
65
65
  button:disabled{opacity:.4;cursor:not-allowed}.status{display:flex;gap:8px;align-items:center;color:var(--muted);font-size:13px}.dot{width:8px;height:8px;border-radius:50%;background:var(--prod);box-shadow:0 0 14px var(--prod)}
66
66
  pre{white-space:pre-wrap;word-break:break-word;background:#10110f;border:1px solid #252722;border-radius:12px;padding:16px;color:#c7cbc2;max-height:300px;overflow:auto;font:12px/1.55 ui-monospace,SFMono-Regular,monospace}
@@ -76,6 +76,7 @@ function studioHtml(sessionToken) {
76
76
  <section class="grid">
77
77
  <article class="card env pre"><div class="card-head"><h2>预发环境</h2><span class="badge">PREPRODUCTION</span></div><dl id="pre"></dl></article>
78
78
  <article class="card env prod"><div class="card-head"><h2>正式环境</h2><span class="badge">PRODUCTION</span></div><dl id="prod"></dl></article>
79
+ <article class="card wide"><div class="card-head"><h2>交付门禁与环境差异</h2><span class="badge" id="gate-badge">CHECKING</span></div><dl id="gates"></dl></article>
79
80
  <article class="card wide"><div class="card-head"><h2>交付状态</h2><div class="status"><span class="dot"></span><span id="health">连接中</span></div></div><div class="toolbar">
80
81
  <button data-action="candidate">生成候选</button><button data-action="deploy">部署预发</button><button data-action="test">登记测试证据</button><button class="primary" data-action="promote">晋级正式</button><button class="danger" data-action="rollback">准备回退</button><button data-action="refresh">刷新</button>
81
82
  </div><pre id="output">Developer Center 只监听 127.0.0.1;所有动作复用 OpenXiangda CLI 门禁。</pre></article>
@@ -87,9 +88,11 @@ const token=${token}; history.replaceState(null,"",location.pathname);
87
88
  const out=document.querySelector("#output"), dialog=document.querySelector("#dialog");
88
89
  let status=null, pending=null;
89
90
  const esc=v=>String(v??"-").replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;","\\"":"&quot;","'":"&#39;"}[c]));
90
- const rows=e=>[["appType",e?.appType],["公开地址",e?.publicOrigin],["AppRelease",e?.heads?.appReleaseId],["候选版本",e?.latestDeployment?.candidateId],["部署状态",e?.latestDeployment?.status],["副作用策略",JSON.stringify(e?.sideEffectPolicy||{})]].map(([k,v])=>\`<dt>\${esc(k)}</dt><dd>\${esc(v)}</dd>\`).join("");
91
+ const rows=e=>[["appType",e?.appType],["环境修订",e?.revision],["公开地址",e?.publicOrigin],["AppRelease",e?.heads?.appReleaseId],["RuntimeRelease",e?.heads?.runtimeReleaseId],["BackendRelease",e?.heads?.backendReleaseId],["PageRelease",e?.heads?.pageReleaseId],["WorkflowRelease",e?.heads?.workflowReleaseId],["候选版本",e?.latestDeployment?.candidateId],["Deployment",e?.latestDeployment?.id],["部署状态",e?.latestDeployment?.status],["测试证据",e?.latestDeployment?.evidenceHash],["证据有效期",e?.latestDeployment?.evidenceSummary?.validUntil],["副作用策略",JSON.stringify(e?.sideEffectPolicy||{})]].map(([k,v])=>\`<dt>\${esc(k)}</dt><dd>\${esc(v)}</dd>\`).join("");
92
+ const bool=v=>\`<span class="\${v?"gate-ok":"gate-bad"}">\${v?"通过":"阻断"}</span>\`;
93
+ const gateRows=s=>[["权威主线",s?.git?.upstream||"-"],["远端主线提交",s?.git?.remoteHeadCommit],["ahead / behind",\`\${s?.git?.ahead??"-"} / \${s?.git?.behind??"-"}\`],["工作区干净",bool(Boolean(s?.git?.clean))],["其他脏 worktree",s?.git?.dirtyWorktrees?.length?s.git.dirtyWorktrees.join(", "):bool(true)],["Candidate 门禁",bool(Boolean(s?.deliveryGates?.candidateReady))],["预发测试证据",bool(Boolean(s?.deliveryGates?.evidenceValid))],["正式 commissioning",bool(Boolean(s?.deliveryGates?.productionCommissioning))],["同一 AppRelease",bool(Boolean(s?.drift?.sameAppRelease))],["资源差异",s?.drift?.sameAppRelease?"无":"存在(预发/正式 Release Head 不同)"],["OpenXiangda",s?.openxiangdaVersion]].map(([k,v])=>\`<dt>\${esc(k)}</dt><dd>\${typeof v==="string"&&v.startsWith("<span")?v:esc(v)}</dd>\`).join("");
91
94
  async function api(path,options={}){const r=await fetch(path,{...options,headers:{"content-type":"application/json","x-openxiangda-studio-token":token,...options.headers}});const j=await r.json();if(!r.ok)throw new Error(j.message||"request failed");return j}
92
- async function refresh(){try{status=await api("/api/status");document.querySelector("#repo").textContent=\`\${status.git?.branch||"-"} @ \${(status.git?.commit||"-").slice(0,12)} · \${status.git?.clean?"clean":"dirty"}\`;document.querySelector("#pre").innerHTML=rows(status.remote?.environments?.find(e=>e.kind==="preproduction"));document.querySelector("#prod").innerHTML=rows(status.remote?.environments?.find(e=>e.kind==="production"));document.querySelector("#health").textContent="状态已同步";}catch(e){document.querySelector("#health").textContent="读取失败";out.textContent=e.message}}
95
+ async function refresh(){try{status=await api("/api/status");document.querySelector("#repo").textContent=\`\${status.git?.branch||"-"} @ \${(status.git?.commit||"-").slice(0,12)} · \${status.git?.mainlineAligned?"mainline ready":status.git?.clean?"clean / not aligned":"dirty"}\`;document.querySelector("#pre").innerHTML=rows(status.remote?.environments?.find(e=>e.kind==="preproduction"));document.querySelector("#prod").innerHTML=rows(status.remote?.environments?.find(e=>e.kind==="production"));document.querySelector("#gates").innerHTML=gateRows(status);const ready=Boolean(status.deliveryGates?.candidateReady);const badge=document.querySelector("#gate-badge");badge.textContent=ready?"READY":"BLOCKED";badge.className=\`badge \${ready?"gate-ok":"gate-bad"}\`;document.querySelector('[data-action="candidate"]').disabled=!ready;document.querySelector('[data-action="deploy"]').disabled=!ready;document.querySelector('[data-action="test"]').disabled=!status.deliveryGates?.testRegistrationReady;document.querySelector('[data-action="promote"]').disabled=!status.deliveryGates?.promotionReady;document.querySelector('[data-action="rollback"]').disabled=!status.deliveryGates?.rollbackReady;document.querySelector("#health").textContent="状态已同步";}catch(e){document.querySelector("#health").textContent="读取失败";out.textContent=e.message}}
93
96
  document.querySelectorAll("[data-action]").forEach(b=>b.onclick=()=>{pending=b.dataset.action;if(pending==="refresh")return refresh();document.querySelector("#dialog-title").textContent=b.textContent;document.querySelector("#value").value="";document.querySelector("#details").value=pending==="test"?JSON.stringify({outcome:"passed",requiredGates:["static","schema","appFunctions","roles","browser","lifecycle","cleanup"],results:{static:{status:"passed"},schema:{status:"passed"},appFunctions:{status:"passed"},roles:{status:"passed"},browser:{status:"passed"},lifecycle:{status:"passed"},cleanup:{status:"passed"}},cleanup:{passed:true,residueCount:0}},null,2):"";dialog.showModal()});
94
97
  document.querySelector("#confirm").onclick=async e=>{e.preventDefault();const action=pending;dialog.close();out.textContent="执行中…";try{const result=await api("/api/action",{method:"POST",body:JSON.stringify({action,value:document.querySelector("#value").value,details:document.querySelector("#details").value,confirmProduction:["promote","rollback"].includes(action)})});out.textContent=JSON.stringify(result,null,2);await refresh()}catch(error){out.textContent=error.message}};
95
98
  refresh();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.189",
3
+ "version": "1.0.191",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -111,6 +111,7 @@
111
111
  "test:page-release-cli": "node scripts/page-release-cli-smoke.mjs",
112
112
  "test:app-release-cli": "node scripts/app-release-cli-smoke.mjs",
113
113
  "test:application-environments": "node scripts/application-environments-smoke.mjs",
114
+ "test:developer-center": "node scripts/developer-center-smoke.mjs",
114
115
  "test:form-release-cas": "node scripts/form-release-cas-smoke.mjs",
115
116
  "test:source-dependencies": "node scripts/source-dependencies-smoke.mjs",
116
117
  "test:form-field-contract": "node scripts/form-field-contract-smoke.mjs",
@@ -0,0 +1,24 @@
1
+ ---
2
+ description: src/forms/** — Use openxiangda-form skill
3
+ globs: src/forms/**/*
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # OpenXiangda React SPA Form Files
8
+
9
+ Editing `src/forms/<formCode>/`. Use the **`openxiangda-form`** skill.
10
+
11
+ - Keep field/schema rules in `schema.ts` and presentation in `page.tsx`.
12
+ - Prefer OpenXiangda platform fields, then `antd` / `antd-mobile`; do not use native inputs, hand-written uploaders, pickers, or user/department selectors.
13
+ - Option fields require non-empty `options`; linked data uses `SelectField` with `optionSource.type: "linkedForm"`.
14
+ - Top-level `schema.rules` is only for `FormEffect[]`; validation belongs to field `rules`.
15
+ - Do not recreate platform system fields. Use `behavior: "HIDDEN"` + `valueSync` for derived or permission keys.
16
+
17
+ Release with exact scope:
18
+
19
+ ```bash
20
+ openxiangda resource plan form-setting --only <formCode> --profile <name>
21
+ openxiangda resource publish form-setting --only <formCode> --change <change> --profile <name>
22
+ ```
23
+
24
+ Managed production activation only occurs through candidate → preproduction deploy/test → promotion of the same candidate.
@@ -0,0 +1,25 @@
1
+ ---
2
+ description: src/pages/** — Use openxiangda-page skill
3
+ globs: src/pages/**/*
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # OpenXiangda React SPA Page Files
8
+
9
+ Editing `src/pages/`. Use the **`openxiangda-page`** skill.
10
+
11
+ - Keep routes in `src/app/router.tsx` and navigation in `src/app/navigation.ts`.
12
+ - Split complex pages into thin views, `domain/`, `shared/{services,hooks}/`, `components/`, and styles.
13
+ - Use server pagination and structured `filterGroup`; never fetch an oversized page and filter in the browser.
14
+ - Prefer OpenXiangda platform components for platform data fields and `antd` / `antd-mobile` for ordinary UI.
15
+ - Do not use native form controls, hand-written pickers/uploaders, or frontend-only authorization.
16
+
17
+ Build and stage:
18
+
19
+ ```bash
20
+ pnpm typecheck
21
+ pnpm build
22
+ openxiangda runtime deploy --no-activate --change <change> --profile <name>
23
+ ```
24
+
25
+ Managed production activation only occurs through candidate → preproduction deploy/test → promotion of the same candidate.
@@ -0,0 +1,26 @@
1
+ ---
2
+ description: src/{workflows,automations,js-code-nodes,functions}/** — Use openxiangda-workflow-automation skill
3
+ globs: src/{workflows,automations,js-code-nodes,functions}/**/*
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # OpenXiangda React SPA Workflow / Automation / Function Files
8
+
9
+ Use the **`openxiangda-workflow-automation`** skill for workflows, automations, JS_CODE, and App Functions.
10
+
11
+ - Ordinary lifecycle state changes use a status field, domain state machine, and automation; create workflows only for real approval semantics.
12
+ - Prefer App Functions for reusable backend logic. JS_CODE is for cross-form, batch, process, platform API, external HTTP, and complex orchestration.
13
+ - Keep UI interactions, normal validation, and display logic in React.
14
+ - Log important backend steps with `ctx.logger`; obtain third-party credentials only through declared `secretRefs`.
15
+ - Never copy workflow, automation, or function IDs across profiles.
16
+
17
+ Build and release with exact scope:
18
+
19
+ ```bash
20
+ pnpm typecheck:js-code
21
+ pnpm build-js-code --script <code>
22
+ openxiangda resource plan <workflow|automation|function> --only <code> --profile <name>
23
+ openxiangda resource publish <workflow|automation|function> --only <code> --change <change> --profile <name>
24
+ ```
25
+
26
+ Managed production activation only occurs through candidate → preproduction deploy/test → promotion of the same candidate.
@@ -0,0 +1,37 @@
1
+ ---
2
+ description: src/forms/** glob — 精准引导到 openxiangda-form skill
3
+ glob: src/forms/**/*
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # OpenXiangda React SPA Form Files
8
+
9
+ You are editing files under `src/forms/<formCode>/`. Use the **`openxiangda-form`** skill.
10
+
11
+ ## Required structure
12
+
13
+ ```text
14
+ src/forms/<formCode>/
15
+ ├── schema.ts # defineFormSchema() default export
16
+ └── page.tsx # presentation only
17
+ ```
18
+
19
+ ## Common pitfalls
20
+
21
+ - 平台字段组件优先于 `antd` / `antd-mobile` 包装和自定义业务组件。
22
+ - `schema.ts` 定义字段、选项、规则与行为;`page.tsx` 只负责展示。
23
+ - 禁止直接写原生 `<input>` / `<select>` / `<textarea>` / file input,禁止手写人员、部门、上传和 picker 组件。
24
+ - 选项字段必须提供非空 `options`;跨表数据使用 `SelectField` + `optionSource.type: "linkedForm"`。
25
+ - 顶层 `schema.rules` 只用于 `FormEffect[]`;字段校验放字段自己的 `rules`。
26
+ - 平台系统字段由平台维护,不要重复创建。
27
+ - 派生键和隐式权限键使用 `behavior: "HIDDEN"` + `valueSync`。
28
+
29
+ ## Release
30
+
31
+ ```bash
32
+ openxiangda resource validate form-setting --only <formCode> --profile <name>
33
+ openxiangda resource plan form-setting --only <formCode> --profile <name>
34
+ openxiangda resource publish form-setting --only <formCode> --change <change> --profile <name>
35
+ ```
36
+
37
+ 正式环境托管发布仍必须走 candidate → preproduction deploy/test → 同 candidate promotion,禁止直接激活生产资源。
@@ -0,0 +1,29 @@
1
+ ---
2
+ description: src/pages/** glob — 精准引导到 openxiangda-page skill
3
+ glob: src/pages/**/*
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # OpenXiangda React SPA Page Files
8
+
9
+ You are editing files under `src/pages/`. Use the **`openxiangda-page`** skill.
10
+
11
+ ## Strong defaults
12
+
13
+ - 路由集中维护在 `src/app/router.tsx`,应用导航维护在 `src/app/navigation.ts`。
14
+ - 复杂页面拆分为薄视图、`domain/`、`shared/{services,hooks}/`、`components/` 和样式文件。
15
+ - 默认使用原生 Tailwind utilities,不使用未配置的 shadcn token。
16
+ - 列表必须服务端分页并使用结构化 `filterGroup`;禁止抓取超大页后在浏览器过滤。
17
+ - 平台数据字段优先使用 OpenXiangda 平台组件;普通 UI 使用 `antd` / `antd-mobile`。
18
+ - 禁止原生表单控件、手写 picker/uploader/人员部门选择器。
19
+ - 后端权限、公开访问 grants 与 App Function 检查才是授权依据;前端只做展示保护。
20
+
21
+ ## Release
22
+
23
+ ```bash
24
+ pnpm typecheck
25
+ pnpm build
26
+ openxiangda runtime deploy --no-activate --change <change> --profile <name>
27
+ ```
28
+
29
+ Runtime 只暂存到候选发布;正式环境托管发布必须走 candidate → preproduction deploy/test → 同 candidate promotion。
@@ -0,0 +1,34 @@
1
+ ---
2
+ description: src/{workflows,automations,js-code-nodes,functions}/** glob — 精准引导到 openxiangda-workflow-automation skill
3
+ glob: src/{workflows,automations,js-code-nodes,functions}/**/*
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # OpenXiangda React SPA Workflow / Automation / Function Files
8
+
9
+ Use the **`openxiangda-workflow-automation`** skill when editing:
10
+
11
+ - `src/workflows/<code>/workflow.ts`
12
+ - `src/automations/<code>/index.ts`
13
+ - `src/js-code-nodes/<code>/index.ts`
14
+ - `src/functions/<code>/index.ts`
15
+
16
+ ## Boundaries
17
+
18
+ - 普通 `pending → processing → resolved → closed` 使用状态字段、领域状态机与 automation;只有真实审批语义才创建 workflow。
19
+ - 复用后端业务逻辑优先 App Function;JS_CODE 用于跨表、批量、流程、平台 API、外部 HTTP 与复杂编排。
20
+ - UI 交互、普通表单校验和展示逻辑留在 React 代码,不放进 JS_CODE。
21
+ - 每个关键步骤使用 `ctx.logger.debug/info/warn/error`;第三方凭据只通过 `secretRefs` 与 `ctx.secrets.get()` 获取。
22
+ - workflowId / automationId / functionId 不得跨 profile 复制。
23
+
24
+ ## Build and release
25
+
26
+ ```bash
27
+ pnpm typecheck:js-code
28
+ pnpm build-js-code --script <code>
29
+ openxiangda resource validate <workflow|automation|function> --only <code> --profile <name>
30
+ openxiangda resource plan <workflow|automation|function> --only <code> --profile <name>
31
+ openxiangda resource publish <workflow|automation|function> --only <code> --change <change> --profile <name>
32
+ ```
33
+
34
+ 资源发布只生成候选子版本;正式环境托管发布必须走 candidate → preproduction deploy/test → 同 candidate promotion。
@@ -9,7 +9,7 @@ const lines = [
9
9
  ' openxiangda resource publish <type> --only <codes> --change <change> --profile <name>',
10
10
  ' openxiangda runtime deploy --no-activate --change <change> --profile <name>',
11
11
  '',
12
- '正式激活后还必须 merge/push 冻结 SHA,再执行 release integration-status 与 release end。',
12
+ '发布前必须先把批准的提交合并并 push 到权威主线;激活后直接执行 release integration-status 与 release end。',
13
13
  '',
14
14
  'legacy lowcode-workspace publish-all 只用于旧 classic workspace 的兼容流程。',
15
15
  '',