openxiangda 1.0.269 → 1.0.271
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 +2 -0
- package/lib/application-environments.js +17 -0
- package/lib/cli.js +720 -78
- package/lib/design-gates.js +18 -0
- package/lib/release-plan.js +78 -63
- package/openxiangda-skills/SKILL.md +3 -1
- package/openxiangda-skills/references/notifications.md +28 -23
- package/openxiangda-skills/references/openxiangda-api.md +22 -30
- package/openxiangda-skills/references/pages/page-sdk.md +1 -1
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +37 -9
- package/openxiangda-skills/references/webhooks.md +213 -0
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +1 -1
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +2 -2
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +3 -0
- package/package.json +24 -23
- package/packages/sdk/dist/runtime/index.cjs +39 -39
- package/packages/sdk/dist/runtime/index.mjs +39 -39
- package/packages/sdk/dist/runtime/react.cjs +39 -39
- package/packages/sdk/dist/runtime/react.d.mts +3 -0
- package/packages/sdk/dist/runtime/react.d.ts +3 -0
- package/packages/sdk/dist/runtime/react.mjs +46 -46
- package/packages/sdk/src/build-source/scripts/publish-all.mjs +33 -2
- package/packages/sdk/src/build-source/scripts/register.mjs +161 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +1 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +1 -0
- package/templates/openxiangda-react-spa/AGENTS.md +4 -0
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +2 -0
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +1 -0
- package/templates/sy-lowcode-app-workspace/AGENTS.md +3 -1
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;
|
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',
|
|
@@ -168,10 +170,14 @@ function commandFromArgs(args) {
|
|
|
168
170
|
}
|
|
169
171
|
|
|
170
172
|
function createStep(id, args, options = {}) {
|
|
173
|
+
const env = options.env && Object.keys(options.env).length > 0
|
|
174
|
+
? { env: { ...options.env } }
|
|
175
|
+
: {};
|
|
171
176
|
return {
|
|
172
177
|
id,
|
|
173
178
|
args,
|
|
174
179
|
command: commandFromArgs(args),
|
|
180
|
+
...env,
|
|
175
181
|
writes: options.writes !== false,
|
|
176
182
|
stagedKind: options.stagedKind || null,
|
|
177
183
|
resumeMode: options.resumeMode || 'skip',
|
|
@@ -327,8 +333,7 @@ function buildWorkspaceReleaseSteps(
|
|
|
327
333
|
targets.resourceSelectors?.formPermissionGroups || []
|
|
328
334
|
);
|
|
329
335
|
const stagesFormRelease =
|
|
330
|
-
|
|
331
|
-
(targets.forms.length > 0 || formPermissionGroups.length > 0);
|
|
336
|
+
targets.forms.length > 0 || formPermissionGroups.length > 0;
|
|
332
337
|
const deferredDataViewCodes = stagesFormRelease
|
|
333
338
|
? uniqueSorted(targets.resourceSelectors?.dataViews || [])
|
|
334
339
|
: [];
|
|
@@ -345,58 +350,59 @@ function buildWorkspaceReleaseSteps(
|
|
|
345
350
|
excludeTypes: stagesFormRelease ? ['formPermissionGroups'] : [],
|
|
346
351
|
}
|
|
347
352
|
);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
changeId
|
|
361
|
-
),
|
|
362
|
-
{ resumeMode: 'replay-local' }
|
|
363
|
-
)
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
if (stagesFormRelease) {
|
|
367
|
-
const resourceTypes = [
|
|
368
|
-
...(targets.forms.length > 0 ? ['form-setting'] : []),
|
|
369
|
-
...(formPermissionGroups.length > 0
|
|
370
|
-
? ['form-permission-group']
|
|
371
|
-
: []),
|
|
372
|
-
];
|
|
373
|
-
const qualify = resourceTypes.length > 1;
|
|
374
|
-
const selectors = [
|
|
375
|
-
...targets.forms.map(code =>
|
|
376
|
-
qualify ? `form-setting:${code}` : code
|
|
353
|
+
const ensuredForms = uniqueSorted([
|
|
354
|
+
...targets.forms,
|
|
355
|
+
...targets.formDependencies,
|
|
356
|
+
]);
|
|
357
|
+
if (ensuredForms.length > 0) {
|
|
358
|
+
steps.push(
|
|
359
|
+
createStep(
|
|
360
|
+
'form-ensure',
|
|
361
|
+
appendProfileAndChange(
|
|
362
|
+
['form', 'ensure', '--only', ensuredForms.join(',')],
|
|
363
|
+
profileArg,
|
|
364
|
+
changeId
|
|
377
365
|
),
|
|
378
|
-
|
|
379
|
-
|
|
366
|
+
{ resumeMode: 'replay-local' }
|
|
367
|
+
)
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
if (stagesFormRelease) {
|
|
371
|
+
const resourceTypes = [
|
|
372
|
+
...(targets.forms.length > 0 ? ['form-setting'] : []),
|
|
373
|
+
...(formPermissionGroups.length > 0
|
|
374
|
+
? ['form-permission-group']
|
|
375
|
+
: []),
|
|
376
|
+
];
|
|
377
|
+
const qualify = resourceTypes.length > 1;
|
|
378
|
+
const selectors = [
|
|
379
|
+
...targets.forms.map(code =>
|
|
380
|
+
qualify ? `form-setting:${code}` : code
|
|
381
|
+
),
|
|
382
|
+
...formPermissionGroups.map(code =>
|
|
383
|
+
qualify ? `form-permission-group:${code}` : code
|
|
384
|
+
),
|
|
385
|
+
];
|
|
386
|
+
steps.push(
|
|
387
|
+
createStep(
|
|
388
|
+
'form-stage',
|
|
389
|
+
appendProfileAndChange(
|
|
390
|
+
[
|
|
391
|
+
'resource',
|
|
392
|
+
'publish',
|
|
393
|
+
resourceTypes.join(','),
|
|
394
|
+
'--only',
|
|
395
|
+
selectors.join(','),
|
|
396
|
+
],
|
|
397
|
+
profileArg,
|
|
398
|
+
changeId
|
|
380
399
|
),
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
'resource',
|
|
388
|
-
'publish',
|
|
389
|
-
resourceTypes.join(','),
|
|
390
|
-
'--only',
|
|
391
|
-
selectors.join(','),
|
|
392
|
-
],
|
|
393
|
-
profileArg,
|
|
394
|
-
changeId
|
|
395
|
-
),
|
|
396
|
-
{ stagedKind: 'FormRelease' }
|
|
397
|
-
)
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
+
{ stagedKind: 'FormRelease' }
|
|
401
|
+
)
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (runtimeMode === 'react-spa') {
|
|
400
406
|
steps.push(...directConfigurationSteps);
|
|
401
407
|
if (hasResourceTargets) {
|
|
402
408
|
steps.push(
|
|
@@ -464,10 +470,13 @@ function buildWorkspaceReleaseSteps(
|
|
|
464
470
|
return steps;
|
|
465
471
|
}
|
|
466
472
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
473
|
+
if (hasResourceTargets) {
|
|
474
|
+
steps.push(...directConfigurationSteps);
|
|
475
|
+
steps.push(
|
|
476
|
+
...buildTargetedResourceReleaseSteps(targets, profileArg, changeId)
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
const onlyTargets = targets.pages.map(code => `pages/${code}`);
|
|
471
480
|
if (onlyTargets.length > 0) {
|
|
472
481
|
steps.push(
|
|
473
482
|
createStep(
|
|
@@ -482,17 +491,17 @@ function buildWorkspaceReleaseSteps(
|
|
|
482
491
|
],
|
|
483
492
|
profileArg,
|
|
484
493
|
changeId
|
|
485
|
-
)
|
|
494
|
+
),
|
|
495
|
+
{
|
|
496
|
+
stagedKind: 'PageRelease',
|
|
497
|
+
env: { OPENXIANGDA_PAGE_STAGE_ONLY: '1' },
|
|
498
|
+
}
|
|
486
499
|
)
|
|
487
500
|
);
|
|
488
501
|
}
|
|
489
|
-
if (hasResourceTargets) {
|
|
490
|
-
steps.push(...directConfigurationSteps);
|
|
491
|
-
steps.push(
|
|
492
|
-
...buildTargetedResourceReleaseSteps(targets, profileArg, changeId)
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
502
|
if (
|
|
503
|
+
stagesFormRelease ||
|
|
504
|
+
targets.pages.length > 0 ||
|
|
496
505
|
targets.functions.length > 0 ||
|
|
497
506
|
targets.automations.length > 0 ||
|
|
498
507
|
targets.workflows.length > 0
|
|
@@ -506,6 +515,12 @@ function buildWorkspaceReleaseSteps(
|
|
|
506
515
|
'app-finalize',
|
|
507
516
|
'--staged-resources-json',
|
|
508
517
|
`.openxiangda/releases/${changeId || 'change'}/staged-resources.json`,
|
|
518
|
+
...(deferredDataViewCodes.length > 0
|
|
519
|
+
? [
|
|
520
|
+
'--finalize-data-views',
|
|
521
|
+
deferredDataViewCodes.join(','),
|
|
522
|
+
]
|
|
523
|
+
: []),
|
|
509
524
|
'--wait',
|
|
510
525
|
],
|
|
511
526
|
profileArg,
|
|
@@ -167,7 +167,7 @@ When the sealed Runtime source intentionally does not descend from the active Ru
|
|
|
167
167
|
|
|
168
168
|
`resource plan` and publish dry-runs are strictly GET/HEAD-only. `READ_ONLY_AUTH_REQUIRED` means the access token expired; run `openxiangda auth refresh --profile <name>` or log in again before retrying. Never add an automatic refresh POST inside a plan.
|
|
169
169
|
|
|
170
|
-
`release publish` is the default promotion entrypoint only for legacy unmanaged workspaces. It verifies without rewriting reviewed `change.json`/`release.json`, waits for the app lease, freezes the App capture after ownership is acquired, executes deterministic exact staged steps, resumes from `.openxiangda/releases/<change>/execution.json`, atomically finalizes, verifies mainline integration, and releases the lease. When the same audited historical-lineage condition applies, legacy `release publish` accepts `--adopt-online-baseline --adoption-reason "..."` and forwards the pair only to exact `resource publish --only/--code` stages
|
|
170
|
+
`release publish` is the default promotion entrypoint only for legacy unmanaged workspaces. It verifies without rewriting reviewed `change.json`/`release.json`, waits for the app lease, freezes the App capture after ownership is acquired, executes deterministic exact staged steps, resumes from `.openxiangda/releases/<change>/execution.json`, atomically finalizes, verifies mainline integration, and releases the lease. Legacy Page steps receive `OPENXIANGDA_PAGE_STAGE_ONLY=1`; the step environment and staged kind are hashed and journaled, and missing PageRelease evidence blocks Root finalize. Custom page publishers must honor that environment and call the current package CLI so `page publish` records the staged child. When the same audited historical-lineage condition applies, legacy `release publish` accepts `--adopt-online-baseline --adoption-reason "..."` and forwards the pair only to exact `resource publish --only/--code` stages. An intentional manifest replacement may add `--replace-manifest --reason "..."`; the inseparable pair reaches only the unique exact Backend stage. Missing reasons or compatible exact scopes fail before lease acquisition. Environment-managed applications use the two-phase `release ship`; candidate/deploy/test/fail/promote, `release begin`, and child commands remain recovery/diagnostic primitives.
|
|
171
171
|
|
|
172
172
|
Reviewed bundle commands may retain `<profile>` as a template. The explicit real `release publish --profile <name>` value is bound to actual child argv without rewriting tracked SDD. React SPA page codes are logical coverage targets and activate through one Runtime child; they do not require PageRelease. `release app-head` and `runtime releases` are compact by default; use `--full` only when the complete manifest is required.
|
|
173
173
|
|
|
@@ -189,6 +189,8 @@ Treat resource declarations as environment mapping and release-observability met
|
|
|
189
189
|
|
|
190
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.
|
|
191
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
|
+
|
|
192
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.
|
|
193
195
|
|
|
194
196
|
## Always
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Notification Resources
|
|
2
2
|
|
|
3
|
-
Use notification resources when
|
|
3
|
+
Use notification resources when an App Function, workflow, or automation needs reusable message templates.
|
|
4
4
|
|
|
5
|
-
AI generation rule: declare notification resources first, then
|
|
5
|
+
AI generation rule: declare notification resources first, then send only from trusted runtime code through `ctx.notification`. Code pages call a named App Function with a business identifier through `sdk.function.invoke`; they never choose effective recipients, rendered content, or channels. Do not hardcode `/api/notification-config/*` or store channel credentials in source.
|
|
6
6
|
|
|
7
7
|
## Resource Files
|
|
8
8
|
|
|
@@ -135,21 +135,35 @@ Standard card payload variables include `title`, `lastMessage`, `content`, `cont
|
|
|
135
135
|
|
|
136
136
|
## Runtime Calls
|
|
137
137
|
|
|
138
|
-
Code pages
|
|
138
|
+
Code pages invoke a named business action. Pass business identifiers, not an
|
|
139
|
+
effective recipient, rendered message, or channel list:
|
|
139
140
|
|
|
140
141
|
```ts
|
|
141
|
-
await sdk.
|
|
142
|
-
|
|
143
|
-
recipientId: userId,
|
|
144
|
-
payload: {
|
|
145
|
-
title: "预约提醒",
|
|
146
|
-
instrumentName,
|
|
147
|
-
startTime,
|
|
148
|
-
},
|
|
142
|
+
await sdk.function.invoke("send_reservation_reminder", {
|
|
143
|
+
input: { reservationId },
|
|
149
144
|
});
|
|
150
145
|
```
|
|
151
146
|
|
|
152
|
-
|
|
147
|
+
The App Function checks the operator and current reservation state, resolves the
|
|
148
|
+
recipient and template variables from authoritative data, then sends through the
|
|
149
|
+
trusted runtime bridge:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
export default async function sendReservationReminder(ctx) {
|
|
153
|
+
const reservation = await loadAuthorizedReservation(ctx.input.reservationId, ctx);
|
|
154
|
+
await ctx.notification.sendByType({
|
|
155
|
+
notificationType: "reservation_reminder",
|
|
156
|
+
recipientId: reservation.ownerUserId,
|
|
157
|
+
payload: {
|
|
158
|
+
title: "预约提醒",
|
|
159
|
+
instrumentName: reservation.instrumentName,
|
|
160
|
+
startTime: reservation.startTime,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
DingTalk-specific read and preview helpers remain available to pages:
|
|
153
167
|
|
|
154
168
|
```ts
|
|
155
169
|
const capabilities = await sdk.notification.capabilities();
|
|
@@ -161,13 +175,6 @@ const preview = await sdk.notification.previewDingTalk({
|
|
|
161
175
|
},
|
|
162
176
|
});
|
|
163
177
|
|
|
164
|
-
await sdk.notification.sendDingTalk({
|
|
165
|
-
notificationType: "reservation_reminder",
|
|
166
|
-
recipientId: userId,
|
|
167
|
-
payload: {
|
|
168
|
-
title: "预约提醒",
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
178
|
```
|
|
172
179
|
|
|
173
180
|
User-facing in-app message centers should read the platform inbox instead of
|
|
@@ -198,12 +205,10 @@ openxiangda notification preview reservation_reminder --body-json '{"payload":{"
|
|
|
198
205
|
openxiangda notification capabilities --json
|
|
199
206
|
openxiangda notification dingding-preview reservation_reminder --body-json '{"payload":{"title":"测试"}}'
|
|
200
207
|
openxiangda notification dingding-preview --template-code reservation_reminder --body-json '{"payload":{"title":"测试"}}'
|
|
201
|
-
openxiangda
|
|
202
|
-
openxiangda notification send reservation_reminder --body-json '{"recipientId":"USER_ID","payload":{"title":"测试"}}' --force
|
|
203
|
-
openxiangda notification batch-send reservation_reminder --body-json '{"recipients":[{"recipientId":"USER_ID","payload":{"title":"测试"}}]}' --force
|
|
208
|
+
openxiangda function invoke send_reservation_reminder --body-json '{"input":{"reservationId":"RESERVATION_ID"}}'
|
|
204
209
|
```
|
|
205
210
|
|
|
206
|
-
Automation or workflow JS_CODE:
|
|
211
|
+
App Function, Automation, or workflow JS_CODE:
|
|
207
212
|
|
|
208
213
|
```ts
|
|
209
214
|
export default async function notify(ctx) {
|
|
@@ -344,38 +344,16 @@ Requires Bearer token. Updates menu sorting and parent relationships in batch.
|
|
|
344
344
|
|
|
345
345
|
### POST `/apps/:appType/notifications/send-by-type`
|
|
346
346
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
{
|
|
352
|
-
"notificationType": "reservation_reminder",
|
|
353
|
-
"recipientId": "user-id",
|
|
354
|
-
"payload": {
|
|
355
|
-
"title": "预约提醒"
|
|
356
|
-
},
|
|
357
|
-
"channels": ["inapp"]
|
|
358
|
-
}
|
|
359
|
-
```
|
|
347
|
+
Removed. Always returns HTTP/envelope `410` with
|
|
348
|
+
`DIRECT_NOTIFICATION_SEND_REMOVED`. Pages must invoke a named App Function;
|
|
349
|
+
trusted runtime code sends through `ctx.notification` after business
|
|
350
|
+
authorization and recipient resolution.
|
|
360
351
|
|
|
361
352
|
### POST `/apps/:appType/notifications/batch-send-by-type`
|
|
362
353
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
{
|
|
367
|
-
"notificationType": "reservation_reminder",
|
|
368
|
-
"recipients": [
|
|
369
|
-
{
|
|
370
|
-
"recipientId": "user-id",
|
|
371
|
-
"payload": {
|
|
372
|
-
"title": "预约提醒"
|
|
373
|
-
},
|
|
374
|
-
"channels": ["inapp"]
|
|
375
|
-
}
|
|
376
|
-
]
|
|
377
|
-
}
|
|
378
|
-
```
|
|
354
|
+
Removed. Always returns HTTP/envelope `410` with
|
|
355
|
+
`DIRECT_NOTIFICATION_SEND_REMOVED`. Batch recipient selection belongs in a
|
|
356
|
+
bounded, authorized App Function, Workflow, Automation, or trusted JS_CODE.
|
|
379
357
|
|
|
380
358
|
### GET `/apps/:appType/notifications/templates`
|
|
381
359
|
|
|
@@ -426,7 +404,9 @@ or a direct DingTalk config:
|
|
|
426
404
|
|
|
427
405
|
### POST `/apps/:appType/notifications/dingtalk/send`
|
|
428
406
|
|
|
429
|
-
|
|
407
|
+
Removed. Always returns HTTP/envelope `410` with
|
|
408
|
+
`DIRECT_NOTIFICATION_SEND_REMOVED`. The CLI command is also removed; use
|
|
409
|
+
`openxiangda function invoke <functionCode>` for an authorized business action.
|
|
430
410
|
|
|
431
411
|
### GET `/apps/:appType/notifications/type-configs`
|
|
432
412
|
|
|
@@ -969,3 +949,15 @@ Requires Bearer token. Creates a short-lived signed OSS upload URL for browser d
|
|
|
969
949
|
### POST `/apps/:appType/storage-configs/:code/objects/delete`
|
|
970
950
|
|
|
971
951
|
Requires Bearer token. Deletes an OSS object under the configured `pathPrefix`.
|
|
952
|
+
## Inbound Webhook
|
|
953
|
+
|
|
954
|
+
- Management: `GET|POST /openxiangda-api/v1/apps/:appType/webhooks`
|
|
955
|
+
- Detail/update/disable: `GET|POST|PUT|DELETE /openxiangda-api/v1/apps/:appType/webhooks/:code`
|
|
956
|
+
- Delivery list/detail: `GET /openxiangda-api/v1/apps/:appType/webhooks/:code/deliveries[/:deliveryId]`
|
|
957
|
+
- Public callback: `POST /openxiangda-webhooks/v1/:endpointId`
|
|
958
|
+
|
|
959
|
+
The public `endpointId` resolves tenant/application ownership and is never
|
|
960
|
+
replaced with `appType`. Management calls use normal profile authentication;
|
|
961
|
+
the public callback is unauthenticated at the platform edge and the target
|
|
962
|
+
Function must verify the provider signature from the exact raw body. See
|
|
963
|
+
`webhooks.md` for the declaration and runtime envelope.
|
|
@@ -15,7 +15,7 @@ Guidelines:
|
|
|
15
15
|
- Use `sdk.dataSource.run()` with a page data source descriptor when the page config should own the data view code, default fields, or default filters.
|
|
16
16
|
- Use `sdk.function.invoke(code, { input })` for reusable backend business logic declared under `src/resources/functions/` and `src/functions/`. Do not implement multi-form orchestration, connector fan-out, notification orchestration, or permission-sensitive backend rules directly in a page component.
|
|
17
17
|
- Use `sdk.connector.invoke`, `sdk.connector.call("connector.api")`, or `sdk.connector.download` for external services. The SDK calls the platform runtime connector endpoint; it must not call third-party domains directly.
|
|
18
|
-
- Use `sdk.
|
|
18
|
+
- Pages must not send notifications directly. Use `sdk.function.invoke(code, { input })` with business identifiers; the named App Function performs authorization, resolves recipients and variables from authoritative data, and sends with trusted `ctx.notification`. Custom notification types must still be declared in `src/resources/notifications/` and published with `openxiangda resource publish`.
|
|
19
19
|
- Use `sdk.export.create` and `sdk.export.get` for asynchronous XLSX exports. Simple pages may send a declarative workbook definition. Complex or reusable exports should send only a published App Function `definitionCode`, the current query snapshot, export scope, and stable selected row IDs. The server executes `structured_export_provider_v1` with fresh user permissions; never upload executable rendering code from the browser. See `docs/structured-export-v1.md`.
|
|
20
20
|
- Use `AttachmentPreviewList`, `ImagePreviewGrid`, or `useFilePreview` from `openxiangda/runtime/react` for in-page attachment previews in a custom React SPA page. They use the current PageSdk context and enforce the platform capability and ticket contracts. Use `sdk.createFileAccessTicket(bucketName, objectName, fileName, "preview", { appType })` only when the page needs a shareable or new-window preview link. `appType` defaults to the current page context. Open `response.result.previewPageUrl`; do not use `previewUrl` or `/service/file/preview-by-ticket/:ticket` as the page entry.
|
|
21
21
|
- Use `sdk.organization.departments.*` and `sdk.organization.accounts.*` only for intentional organization pages. Read-only list/detail pages need `app:organization:read` or `app:organization:manage`; writes and password operations need `app:organization:manage`. Do not call legacy `/user` or `/department` endpoints from pages.
|
|
@@ -552,19 +552,17 @@ const stats = await sdk.dataView.stats("ticket_stats_by_customer", {
|
|
|
552
552
|
}
|
|
553
553
|
```
|
|
554
554
|
|
|
555
|
-
|
|
555
|
+
页面调用具名 App Function,只传业务标识:
|
|
556
556
|
|
|
557
557
|
```ts
|
|
558
|
-
await sdk.
|
|
559
|
-
|
|
560
|
-
recipientId: userId,
|
|
561
|
-
payload: { title: "预约提醒", instrumentName, startTime },
|
|
558
|
+
await sdk.function.invoke("send_reservation_reminder", {
|
|
559
|
+
input: { reservationId },
|
|
562
560
|
});
|
|
563
561
|
```
|
|
564
562
|
|
|
565
|
-
JS_CODE
|
|
563
|
+
App Function / Workflow / Automation / JS_CODE 在完成业务鉴权并解析接收人后调用:`ctx.notification.sendByType({ ... })`。允许的 channels:`inapp` / `email` / `dingding` / `wechat` / `thirdparty_todo`。完整规则见 [`notifications.md`](notifications.md)。
|
|
566
564
|
|
|
567
|
-
|
|
565
|
+
页面可做钉钉卡片预览:`sdk.notification.previewDingTalk({ notificationType, payload })`;CLI 为 `openxiangda notification dingding-preview`。实际发送必须位于具名 App Function 等可信运行时中。
|
|
568
566
|
|
|
569
567
|
## 4. App Function — `src/resources/functions/<functionCode>.json` + `src/functions/<functionCode>/index.ts`
|
|
570
568
|
|
|
@@ -683,7 +681,37 @@ const result = await sdk.function.invoke("reservation_reminder_summary", {
|
|
|
683
681
|
|
|
684
682
|
适用边界:可复用后端业务逻辑、跨页面/自动化/流程共享的查询编排、连接器调用、通知编排、受控平台 API 调用。App Function 支持 `ctx.form.queryOne/queryMany/getById/createOne/updateOne/updateById`、`ctx.dataView`、`ctx.connector`、`ctx.notification`、`ctx.platform.roles`、`ctx.platform.api` 等受控 helper,当前 MVP 不暴露原始 SQL/Redis。应用角色查询和成员维护优先使用 `ctx.platform.roles.list/findByCode/addUsers/removeUser`;底层 `ctx.platform.api` 返回 HTTP 包装与平台 envelope,需要自行解包。已发布可信代码可以访问当前租户、当前应用内的资源;function manifest 的 `resources` 是可选映射、审计和影响分析信息,不再是逐函数权限白名单。页面用户仍不能直接提交内部表单,跨应用和跨租户访问仍被拒绝。运行时接口默认需要应用自动化管理权限;普通用户页面要调用时,用 `definitionJson.runtimeInvoke.audience` 声明 `authenticated`、`page_permission_group`、`app_roles`、`platform_roles` 或 `scope_policy`,使用 `roleCodes` 匹配应用角色、使用 `platformRoleCodes` 匹配同步身份 `SCHOOL_GUARDIAN`、`SCHOOL_STUDENT`、`SCHOOL_TEACHER`,不要把 `"*"`、`"all-app-roles"` 写进角色编码。若表单只能由函数/流程写入,在 `src/resources/settings/forms/<formCode>.json` 设置 `runtimeWrite.mode="function_only"` 关闭原始写入接口。
|
|
685
683
|
|
|
686
|
-
## 5.
|
|
684
|
+
## 5. Inbound Webhook — `src/resources/webhooks/<code>.json`
|
|
685
|
+
|
|
686
|
+
```json
|
|
687
|
+
{
|
|
688
|
+
"code": "yuquan_access",
|
|
689
|
+
"name": "玉泉门禁开门事件",
|
|
690
|
+
"targetFunctionCode": "qfyy_access_event",
|
|
691
|
+
"idempotencyQueryParam": "nonce",
|
|
692
|
+
"maxBodyBytes": 262144,
|
|
693
|
+
"status": "active"
|
|
694
|
+
}
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
Webhook 只声明公开入口到固定 App Function 的映射,不包含 Secret 或验签规则。
|
|
698
|
+
供应商 Secret 在目标 Function 顶层 `secretRefs` 声明,源码通过
|
|
699
|
+
`await ctx.secrets.get(name)` 读取,并且必须在任何表单查询、写入、连接器或通知
|
|
700
|
+
调用之前使用 `input.rawBody` 验签。平台保存原始 UTF-8 Body、Base64 Body、原始
|
|
701
|
+
Query 字符串、重复参数数组、解析 JSON 和安全请求头;投递为 at-least-once,应用
|
|
702
|
+
还必须用 `input.idempotencyKey` 对业务写入做幂等保护。
|
|
703
|
+
|
|
704
|
+
```bash
|
|
705
|
+
openxiangda resource validate webhook --profile <name>
|
|
706
|
+
openxiangda resource plan webhook --only yuquan_access --profile <name> --json
|
|
707
|
+
openxiangda resource publish webhook --only yuquan_access --change <id> --profile <name>
|
|
708
|
+
openxiangda webhook deliveries yuquan_access --profile <name> --json
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
完整 Function 输入、HMAC-SHA1 常量时间比较、返回状态和玉泉门禁示例见
|
|
712
|
+
[`webhooks.md`](webhooks.md)。
|
|
713
|
+
|
|
714
|
+
## 6. Workflow — `src/resources/workflows/<code>/workflow.json`(manifest)+ `src/workflows/<code>/workflow.ts`(代码优先)
|
|
687
715
|
|
|
688
716
|
```jsonc
|
|
689
717
|
// src/resources/workflows/customer_approval/workflow.json
|
|
@@ -713,7 +741,7 @@ export default defineWorkflow({
|
|
|
713
741
|
|
|
714
742
|
CLI 编译为 `definition.v3.json` + `preview.json`,平台运行时仍走标准工作流引擎。完整规则见 [`workflow-v3.md`](workflow-v3.md)。
|
|
715
743
|
|
|
716
|
-
##
|
|
744
|
+
## 7. Automation — `src/resources/automations/<code>/{definition.code.json,preview.json}` + `src/automations/<code>/index.ts`
|
|
717
745
|
|
|
718
746
|
```jsonc
|
|
719
747
|
// src/resources/automations/notify_on_submit/definition.code.json
|