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.
Files changed (29) hide show
  1. package/README.md +2 -0
  2. package/lib/application-environments.js +17 -0
  3. package/lib/cli.js +720 -78
  4. package/lib/design-gates.js +18 -0
  5. package/lib/release-plan.js +78 -63
  6. package/openxiangda-skills/SKILL.md +3 -1
  7. package/openxiangda-skills/references/notifications.md +28 -23
  8. package/openxiangda-skills/references/openxiangda-api.md +22 -30
  9. package/openxiangda-skills/references/pages/page-sdk.md +1 -1
  10. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +37 -9
  11. package/openxiangda-skills/references/webhooks.md +213 -0
  12. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +1 -1
  13. package/openxiangda-skills/skills/openxiangda-page/SKILL.md +2 -2
  14. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +3 -0
  15. package/package.json +24 -23
  16. package/packages/sdk/dist/runtime/index.cjs +39 -39
  17. package/packages/sdk/dist/runtime/index.mjs +39 -39
  18. package/packages/sdk/dist/runtime/react.cjs +39 -39
  19. package/packages/sdk/dist/runtime/react.d.mts +3 -0
  20. package/packages/sdk/dist/runtime/react.d.ts +3 -0
  21. package/packages/sdk/dist/runtime/react.mjs +46 -46
  22. package/packages/sdk/src/build-source/scripts/publish-all.mjs +33 -2
  23. package/packages/sdk/src/build-source/scripts/register.mjs +161 -1
  24. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +1 -0
  25. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +1 -0
  26. package/templates/openxiangda-react-spa/AGENTS.md +4 -0
  27. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +2 -0
  28. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +1 -0
  29. package/templates/sy-lowcode-app-workspace/AGENTS.md +3 -1
@@ -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;
@@ -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
- runtimeMode === 'react-spa' &&
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
- if (runtimeMode === 'react-spa') {
349
- const ensuredForms = uniqueSorted([
350
- ...targets.forms,
351
- ...targets.formDependencies,
352
- ]);
353
- if (ensuredForms.length > 0) {
354
- steps.push(
355
- createStep(
356
- 'form-ensure',
357
- appendProfileAndChange(
358
- ['form', 'ensure', '--only', ensuredForms.join(',')],
359
- profileArg,
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
- ...formPermissionGroups.map(code =>
379
- qualify ? `form-permission-group:${code}` : code
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
- steps.push(
383
- createStep(
384
- 'form-stage',
385
- appendProfileAndChange(
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
- const onlyTargets = [
468
- ...targets.forms.map(code => `forms/${code}`),
469
- ...targets.pages.map(code => `pages/${code}`),
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; missing reasons or plans without an exact resource stage 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.
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 a page, workflow, or automation needs reusable message templates.
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 call `sdk.notification` in code pages or `ctx.notification` in JS_CODE. Do not hardcode `/api/notification-config/*` or store channel credentials in source.
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.notification.sendByType({
142
- notificationType: "reservation_reminder",
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
- DingTalk-specific helpers are available when the caller wants to inspect or force the DingTalk channel:
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 notification dingding-send reservation_reminder --body-json '{"recipientId":"USER_ID","payload":{"title":"测试"}}' --force
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
- Requires Bearer token. Sends a notification in the current app scope.
348
- Each returned message can include `deliveryMeta` with provider message id, outTrackId, card template id, fallback usage, and provider error summary.
349
-
350
- ```json
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
- Requires Bearer token. Sends one notification type to multiple recipients.
364
-
365
- ```json
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
- Requires Bearer token. Sends the resolved `notificationType` through the DingTalk channel only. The CLI command is `openxiangda notification dingding-send <notificationType> --body-json '{"recipientId":"USER_ID","payload":{"title":"测试"}}' --force`.
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.notification.sendByType` and `batchSendByType` for reusable business messages. Custom notification types must be declared in `src/resources/notifications/` and published with `openxiangda resource publish`.
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.notification.sendByType({
559
- notificationType: "reservation_reminder",
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 调用:`ctx.notification.sendByType({ ... })`。允许的 channels:`inapp` / `email` / `dingding` / `wechat` / `thirdparty_todo`。完整规则见 [`notifications.md`](notifications.md)。
563
+ App Function / Workflow / Automation / JS_CODE 在完成业务鉴权并解析接收人后调用:`ctx.notification.sendByType({ ... })`。允许的 channels:`inapp` / `email` / `dingding` / `wechat` / `thirdparty_todo`。完整规则见 [`notifications.md`](notifications.md)。
566
564
 
567
- 钉钉卡片预览/发送:`sdk.notification.previewDingTalk({ notificationType, payload })`、`sdk.notification.sendDingTalk({ notificationType, recipientId, payload })`;CLI 为 `openxiangda notification dingding-preview` 和 `openxiangda notification dingding-send --force`。
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. Workflow — `src/resources/workflows/<code>/workflow.json`(manifest)+ `src/workflows/<code>/workflow.ts`(代码优先)
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
- ## 6. Automation — `src/resources/automations/<code>/{definition.code.json,preview.json}` + `src/automations/<code>/index.ts`
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