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 +162 -13
- package/lib/developer-center.js +6 -3
- package/package.json +2 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-form.mdc +24 -0
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-page.mdc +25 -0
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-workflow-automation.mdc +26 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-form.md +37 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-page.md +29 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-workflow-automation.md +34 -0
- package/templates/openxiangda-react-spa/scripts/guard-publish.mjs +1 -1
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
|
|
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
|
|
2274
|
+
const remoteStatus = await requestWithAuth(
|
|
2169
2275
|
config,
|
|
2170
2276
|
selected.binding.profile,
|
|
2171
2277
|
environmentSetApiPath(state.logicalApp.code, '/status')
|
|
2172
2278
|
);
|
|
2173
|
-
const
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
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
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
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,
|
package/lib/developer-center.js
CHANGED
|
@@ -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=>({"&":"&","<":"<",">":">","\\"":""","'":"'"}[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.
|
|
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
|
-
'
|
|
12
|
+
'发布前必须先把批准的提交合并并 push 到权威主线;激活后直接执行 release integration-status 与 release end。',
|
|
13
13
|
'',
|
|
14
14
|
'legacy lowcode-workspace publish-all 只用于旧 classic workspace 的兼容流程。',
|
|
15
15
|
'',
|