openyida 2026.10.23-beta.6 → 2026.10.25

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 (47) hide show
  1. package/README.md +5 -22
  2. package/bin/yida.js +15 -18
  3. package/lib/app/form-message.js +352 -0
  4. package/lib/app/services/canvas-page-schema-builder.js +43 -1
  5. package/lib/auth/token-auth.js +1 -8
  6. package/lib/auth/token-store.js +0 -1
  7. package/lib/core/agent-capabilities.js +5 -38
  8. package/lib/core/command-manifest.js +73 -10
  9. package/lib/core/locales/en.js +30 -19
  10. package/lib/core/locales/zh.js +29 -18
  11. package/lib/core/task-center.js +32 -4
  12. package/lib/navigation/page-navigation.js +62 -0
  13. package/lib/samples/openyida-scaffold/canvas-navigation.jsx +6 -2
  14. package/lib/solution-template/solution-template.js +666 -0
  15. package/lib/task-center/contract.js +26 -1
  16. package/lib/task-center/loader.js +5 -0
  17. package/lib/task-center/navigation.js +41 -0
  18. package/lib/task-center/scaffold.js +9 -0
  19. package/package.json +5 -13
  20. package/yida-skills/SKILL.md +7 -8
  21. package/yida-skills/references/setup-and-env.md +0 -4
  22. package/yida-skills/references/yida-api.md +3 -1
  23. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +4 -2
  24. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-style-implementation-guide.md +2 -0
  25. package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +2 -0
  26. package/yida-skills/skills/yida-canvas-data-binding/SKILL.md +2 -2
  27. package/yida-skills/skills/yida-canvas-data-binding/references/workflow-capabilities.md +29 -1
  28. package/yida-skills/skills/yida-create-page/SKILL.md +1 -0
  29. package/yida-skills/skills/yida-design/SKILL.md +1 -1
  30. package/yida-skills/skills/yida-design/references/application-style-library.md +2 -0
  31. package/yida-skills/skills/yida-design/references/application-theme-consistency.md +4 -0
  32. package/yida-skills/skills/yida-design/sub_skill/page-design/SKILL.md +27 -44
  33. package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +3 -1
  34. package/yida-skills/skills/yida-form-message/SKILL.md +53 -0
  35. package/yida-skills/skills/yida-form-message/references/guide.md +111 -0
  36. package/yida-skills/skills/yida-login/SKILL.md +0 -18
  37. package/yida-skills/skills/yida-solution-template/SKILL.md +98 -0
  38. package/yida-skills/skills/yida-task-center/SKILL.md +3 -1
  39. package/yida-skills/skills-index.json +26 -0
  40. package/bin/yida-pre-beta.js +0 -11
  41. package/lib/auth/qwenwork-client.js +0 -190
  42. package/lib/auth/qwenwork-desktop.js +0 -153
  43. package/lib/auth/qwenwork-login.js +0 -87
  44. package/lib/auth/qwenwork.js +0 -37
  45. package/lib/core/qwenwork-login-guidance.js +0 -32
  46. package/scripts/install-qwenwork-pre-beta.js +0 -40
  47. package/yida-skills/references/qwenwork-desktop-login.md +0 -12
package/README.md CHANGED
@@ -143,25 +143,6 @@ openyida login --check-only --json
143
143
  openyida auth status
144
144
  ```
145
145
 
146
- For QwenWork cloud identity diagnostics, run inside the actual cloud execution environment:
147
-
148
- ```bash
149
- openyida auth qwenwork user-info --json
150
- ```
151
-
152
- This command exchanges the injected `QWENWORK_AGENT_TOKEN` (legacy: `MULE_AGENT_TOKEN` / `AGENT_TOKEN`) at `QWENWORK_GATEWAY_URL` (legacy: `MULE_GATEWAY_URL`), then queries `/user/info` on the same origin. It reports identity and scope metadata without printing or persisting either credential. It does not create an OpenYida login session, and QwenWork organization IDs are not YiDA `corpId` values. Existing OAuth/profile behavior is unchanged.
153
-
154
- Programmatic callers can use `exchangeQwenWorkAgentToken`, `getQwenWorkUserInfo` and `getQwenWorkIdentity` from `lib/auth/qwenwork-client.js`. The exchange function returns an in-memory `access_token` credential for the identity query or a future server-side login exchange; callers must not log or persist it.
155
-
156
- ```js
157
- const { exchangeQwenWorkAgentToken, getQwenWorkUserInfo } = require('openyida/lib/auth/qwenwork-client');
158
- async function readCloudUser() {
159
- const credential = await exchangeQwenWorkAgentToken();
160
- // The result contains QwenWork identifiers, not YiDA IDs.
161
- return getQwenWorkUserInfo(credential);
162
- }
163
- ```
164
-
165
146
  To reuse an existing shared login profile for the current project, switch first instead of starting a new OAuth login:
166
147
 
167
148
  ```bash
@@ -476,9 +457,9 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
476
457
 
477
458
  | Command | Description |
478
459
  |---------|-------------|
479
- | `openyida login [--qwenwork] [target-url] [--env <name>\|--intl\|--overseas\|--global\|--yidaapps\|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser]` | Login with OAuth token mode |
460
+ | `openyida login [target-url] [--env <name>\|--intl\|--overseas\|--global\|--yidaapps\|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser]` | Login with OAuth token mode |
480
461
  | `openyida logout` | Logout / unbind current project auth |
481
- | `openyida auth <status\|login\|refresh\|logout\|profiles\|profile switch\|qwenwork user-info> [--json]` | Token login state and profile management |
462
+ | `openyida auth <status\|login\|refresh\|logout\|profiles\|profile switch>` | Token login state and profile management |
482
463
  | `openyida org <list\|switch> [--json] [--corp-id <corpId>]` | Organization management (list / switch existing profiles first) |
483
464
  | `openyida env [--json\|setup\|list\|show\|switch\|add\|remove] [options]` | Detect AI tool environment & token login state |
484
465
 
@@ -492,6 +473,7 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
492
473
  | `openyida app-entry get <appType> [--json]` | Read frontend and management runtime entries |
493
474
  | `openyida app-entry set <appType> [--frontend <url>\|--frontend-page <formUuid>] [--management <url>\|--management-page <formUuid>] [--clear-frontend] [--clear-management] [--json]` | Register application entries with partial update and readback |
494
475
  | `openyida app-list [--type managed\|created] [--page N] [--size N]` | Page through apps I manage or created |
476
+ | `openyida solution-template <search\|detail\|prepare\|ai-build\|enable\|status> [templateId\|requestId] [options] [--json]` | Solution templates for independent local agents: discovery, cloud AI handoff and direct enable |
495
477
  | `openyida corp-efficiency [overview\|details\|detail\|groups\|notify] [options] [--open\|--no-open]` | Query enterprise efficiency overview and detail reports |
496
478
  | `openyida create-app "<name>"\|--name <name> [options] [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Create a Yida app |
497
479
  | `openyida design-plan catalog [--json]` | List available planning themes and page patterns |
@@ -535,6 +517,7 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
535
517
  | `openyida print-template <list\|inspect\|fields\|build\|create\|copy\|save\|restore\|settings\|status\|order\|render\|files> <appType> <formUuid> [templateId] [--config <file>] [--scene <order\|quotation\|delivery\|receipt\|approval\|codes>] [--bindings <file>] [--name <title>] [--expected-hash <hash>] [--backup <file>] [--out <file>] [--status START\|STOP] [--instance <id>] [--page <n>] [--size <n>] [--allow-permission-change] [--json] \| print-template <list\|render> --detail-url <url> [--template-id <id>] [--json]` | Manage native YiDA print templates, generate document layouts, back up, restore and request PDFs |
536
518
  | `openyida form-view <list\|inspect\|save\|create\|order> <appType> <formUuid> [viewUuid] [--config <file>] [--name <title>] [--with-config] [--expected-hash <hash>] [--backup <file>] [--replace] [--json]` | Native form views: list, inspect and configure columns, widths, frozen columns and filters |
537
519
  | `openyida custom-button <list\|inspect\|options\|create\|update\|delete> <appType> <formUuid> [buttonUuid] [--config <file>] [--expected-hash <hash>] [--backup <file>] [--yes] [--json]` | Native custom buttons: inspect and configure form edits, automation flows and page links |
520
+ | `openyida form-message <list\|inspect\|options\|create\|update\|delete> <appType> <formUuid> [messageId] [--config <file>] [--expected-hash <hash>] [--backup <file>] [--yes] [--json]` | Native form notifications: inspect triggers, recipients and templates, then safely maintain bindings |
538
521
  | `openyida get-schema <appType> <formUuid\|--all> [--summary-json\|--field-map-json\|--analysis-json]` | Get one form Schema or all form Schemas |
539
522
  | `openyida check-prd-completeness <prd.md> --app-type <appType> [--build-manifest <file>] [--json]` | Check resources, frontend menu hiding and application theme delivery |
540
523
  | `openyida check-design <design.md> [--prd <prd.md>] [--base-dir <dir>] [--json]` | Validate design documents, theme tokens and PRD handoff |
@@ -552,7 +535,7 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
552
535
  | Command | Description |
553
536
  |---------|-------------|
554
537
  | `openyida data <query\|get\|create\|update\|transfer> <resource> ... \| delete form <appType> <formUuid> --inst-id <id> --expect-form-name <name> --expect-form-type receipt --confirm [--json]` | Unified data management (form/process/task/subform) |
555
- | `openyida task-center <todo\|created\|done\|cc\|proxy\|capabilities\|scaffold\|options\|inspect\|count> [--spec <file.json> --output <file.canvas.jsx>] [options]` | Global task center (todo/processed/cc etc.) |
538
+ | `openyida task-center <todo\|created\|done\|cc\|proxy\|capabilities\|link\|scaffold\|options\|inspect\|count> [--spec <file.json> --output <file.canvas.jsx>] [options]` | Global task center (todo/processed/cc etc.) |
556
539
  | `openyida basic-info <overview\|commodity\|grant\|capacity\|quota\|abs-path\|dataflow\|i18n\|domain>` | Query organization basic info, capacity, quotas, and domain settings |
557
540
  | `openyida read-dingtalk-doc <docUrl> [--output <file>] [--json]` | Fetch Markdown content from a DingTalk document |
558
541
  | `openyida read-dingtalk-tingji <taskUuid> [--json]` | Fetch DingTalk Tingji details by task UUID |
package/bin/yida.js CHANGED
@@ -588,27 +588,17 @@ async function main() {
588
588
  }
589
589
  return;
590
590
  }
591
+ // Template-only preflight; managed runs retain their existing guard contract.
592
+ if (command === 'solution-template' && !hasHelpFlag(rawArgs) && !readManagedContext()) {
593
+ require('../lib/solution-template/solution-template').assertIndependentLocalAgent();
594
+ }
591
595
  commandReceipt = beginCommandReceipt({ command, args: rawArgs });
592
596
  process.once('exit', code => {
593
597
  try {commandReceipt.finish(code);} catch {process.exitCode = 1;}
594
598
  });
595
599
  assertManagedInvocation(command, rawArgs);
596
600
  applyQuietFlag();
597
- // 一次性票据只在当前进程消费,不能先自更新并重启命令。
598
- switch (command) {
599
- case 'login-qwenwork':
600
- await require('../lib/auth/qwenwork-desktop').run(args);
601
- return;
602
- }
603
601
  applyGlobalEnvironmentFlags();
604
- // 桌面准备完成后继续执行公开命令,由真实登录态决定结果。
605
- if (command === 'login' && args.includes('--qwenwork')) {
606
- const loginArgs = applyLoginEnvironmentFlags(args, { inferTargetUrl: true });
607
- if (hasHelpFlag(loginArgs)) { printLoginHelp(); return; }
608
- assertNoUnsupportedLegacyLoginFlags(rawArgs, loginArgs);
609
- await require('../lib/auth/qwenwork-login').run(loginArgs, buildTokenLoginOptions(loginArgs));
610
- return;
611
- }
612
602
 
613
603
  const { maybeAutoUpdate } = require('../lib/core/update');
614
604
  const updateResult = await maybeAutoUpdate({
@@ -728,10 +718,6 @@ async function main() {
728
718
 
729
719
  case 'auth': {
730
720
  const subCommand = args[0];
731
- if (subCommand === 'qwenwork') {
732
- await require('../lib/auth/qwenwork').run(args.slice(1));
733
- break;
734
- }
735
721
  const authArgs = applyLoginEnvironmentFlags(args.slice(1), { inferTargetUrl: true });
736
722
  const { getAuthStatus } = require('../lib/core/utils');
737
723
  const { tokenLogin, tokenLogout, tokenRefresh } = require('../lib/auth/token-auth');
@@ -808,6 +794,12 @@ async function main() {
808
794
  break;
809
795
  }
810
796
 
797
+ case 'solution-template': {
798
+ const { run } = require('../lib/solution-template/solution-template');
799
+ await run(args);
800
+ break;
801
+ }
802
+
811
803
  case 'corp-efficiency': {
812
804
  const { run } = require('../lib/corp-efficiency/corp-efficiency');
813
805
  await run(args);
@@ -855,6 +847,11 @@ async function main() {
855
847
  await run(args);
856
848
  break;
857
849
  }
850
+ case 'form-message': {
851
+ const { run } = require('../lib/app/form-message');
852
+ await run(args);
853
+ break;
854
+ }
858
855
  case 'print-template': {
859
856
  const { run } = require('../lib/print-template/print-template');
860
857
  await run(args);
@@ -0,0 +1,352 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { isDeepStrictEqual } = require('util');
7
+ const { CliError } = require('../core/cli-error');
8
+ const { t } = require('../core/i18n');
9
+ const { resolveProjectRoot } = require('../core/utils');
10
+ const { createYidaClient } = require('../core/yida-client');
11
+
12
+ const ACTIONS = ['list', 'inspect', 'options', 'create', 'update', 'delete'];
13
+ const RECEIVER_TYPES = new Set(['employee', 'role', 'formField', 'variable']);
14
+ const FORM_TRIGGERS = new Set(['FORM_DATA_CREATE', 'FORM_DATA_EDIT', 'FORM_DATA_DELETE', 'FORM_COMMENT']);
15
+ const SYSTEM_VARIABLES = new Set(['originator']);
16
+ const MESSAGE_FIELD_COMPONENTS = new Set([
17
+ 'TextareaField', 'EmployeeField', 'SelectField', 'MultiSelectField', 'DateField', 'TextField',
18
+ 'RadioField', 'NumberField', 'CheckboxField', 'RateField', 'CascadeDateField', 'DepartmentSelectField',
19
+ ]);
20
+ const RECIPIENT_FIELD_COMPONENTS = new Set(['EmployeeField']);
21
+ const ID = /^[A-Za-z0-9_-]+$/;
22
+ const TEMPLATES = Object.freeze({
23
+ workNotice: { uuid: 'system_default_work_notice', type: 'workNotice', content: true, target: 'person' },
24
+ detailImage: { uuid: '8fbf8a81-c775-4570-b367-86d4d719a85f', type: 'card', fields: true, target: 'person' },
25
+ imageCard: { uuid: '39caf717-df43-46e1-98f9-564fdd1dc789', type: 'card', content: true, target: 'person' },
26
+ detailText: { uuid: '97bd36d0-64d8-4c50-96e8-f02312655700', type: 'card', fields: true, target: 'person' },
27
+ commentCard: { uuid: 'c298cd88-0de6-44c3-9023-c93e0d7db9ac', type: 'card', content: true, target: 'person' },
28
+ pinnedImage: { uuid: 'cdcdf076-3625-4da7-9a61-571591594209', type: 'ceilingCard', content: true, target: 'group' },
29
+ pinnedText: { uuid: '68240de5-8d9e-416a-b5b2-c71dca0ab703', type: 'ceilingCard', content: true, target: 'group' },
30
+ });
31
+ const TEMPLATE_BY_UUID = new Map(Object.entries(TEMPLATES).map(([key, item]) => [item.uuid, { key, ...item }]));
32
+
33
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
34
+ const clone = value => JSON.parse(JSON.stringify(value));
35
+
36
+ /** Build a stable fingerprint for concurrency checks and destructive actions. */
37
+ function hash(value) {
38
+ const stable = item => Array.isArray(item) ? item.map(stable) : object(item)
39
+ ? Object.fromEntries(Object.keys(item).sort().map(key => [key, stable(item[key])])) : item;
40
+ return crypto.createHash('sha256').update(JSON.stringify(stable(value))).digest('hex');
41
+ }
42
+
43
+ function fail(code, key, detail = '', details) {
44
+ throw new CliError(t(`form_message.${key}`, detail), { code: `FORM_MESSAGE_${code}`, details });
45
+ }
46
+
47
+ function safe(value) {
48
+ if (!value || typeof value !== 'object') {return;}
49
+ for (const key of Object.keys(value)) {
50
+ if (['__proto__', 'prototype', 'constructor'].includes(key)) {fail('INVALID_CONFIG', 'invalid_config', key);}
51
+ safe(value[key]);
52
+ }
53
+ }
54
+
55
+ function parseJson(value, label) {
56
+ try { return typeof value === 'string' ? JSON.parse(value) : value; }
57
+ catch (_) { return fail('INVALID_CONFIG', 'invalid_config', label); }
58
+ }
59
+
60
+ function unwrap(response, endpoint) {
61
+ if (!response || response.success !== true || response.__needLogin || response.__csrfExpired || response.content === false) {
62
+ fail('API_ERROR', 'api_error', response?.errorMsg || response?.message || endpoint, {
63
+ endpoint, platformErrorCode: response?.errorCode || null, needLogin: !!response?.__needLogin,
64
+ });
65
+ }
66
+ return response.content;
67
+ }
68
+
69
+ /** Normalize a native binding while retaining fields required for lossless backup and update. */
70
+ function normalize(raw) {
71
+ if (!object(raw) || raw.id === undefined || raw.id === null || !String(raw.id).trim()) {
72
+ fail('INVALID_RESPONSE', 'invalid_response', 'id');
73
+ }
74
+ const cardConfig = raw.cardConfig ? parseJson(raw.cardConfig, 'cardConfig') : {};
75
+ if (!object(cardConfig)) {fail('INVALID_RESPONSE', 'invalid_response', 'cardConfig');}
76
+ const recipients = [];
77
+ const add = (source, type) => String(source || '').split(',').map(value => value.trim()).filter(Boolean)
78
+ .forEach(value => recipients.push({ type, value }));
79
+ add(raw.persons, 'employee'); add(raw.roleId, 'role'); add(raw.formField, 'formField'); add(raw.variable, 'variable');
80
+ const nativeGroups = Array.isArray(cardConfig.groups) ? clone(cardConfig.groups) : [];
81
+ const template = TEMPLATE_BY_UUID.get(cardConfig.templateUuid) || {};
82
+ const buttons = Array.isArray(cardConfig.buttons) ? clone(cardConfig.buttons) : [];
83
+ return {
84
+ id: String(raw.id),
85
+ formUuid: raw.formUuid || null,
86
+ trigger: raw.phase || null,
87
+ messageType: raw.messageType || 'NORMAL',
88
+ recipients,
89
+ groups: nativeGroups,
90
+ template: {
91
+ key: template.key || null,
92
+ uuid: cardConfig.templateUuid || raw.messageUuid || null,
93
+ type: cardConfig.templateType || template.type || null,
94
+ title: cardConfig.cardTitle || '',
95
+ content: cardConfig.mainContent || '',
96
+ fields: Array.isArray(cardConfig.datas) ? clone(cardConfig.datas) : [],
97
+ image: Array.isArray(cardConfig.messageImage) ? clone(cardConfig.messageImage) : [],
98
+ contentType: cardConfig.messageContentType || (raw.messageUuid ? 'TEMPLATE' : 'CUSTOM'),
99
+ platformTemplateUuid: raw.messageUuid && !String(raw.messageUuid).includes('#') ? raw.messageUuid : null,
100
+ button: buttons[0] || null,
101
+ },
102
+ advanced: {
103
+ openMore: !!cardConfig.openMore,
104
+ activityIds: String(raw.activityId || '').split(',').filter(Boolean),
105
+ condition: raw.conditionJson ? parseJson(raw.conditionJson, 'conditionJson') : null,
106
+ processId: raw.processId || null,
107
+ outcome: String(raw.outResult || '').split(',').filter(Boolean),
108
+ currentGroup: raw.curDingGroup === 'y',
109
+ sendWorkNotice: raw.isSendMessage === 'y',
110
+ },
111
+ native: clone(raw),
112
+ };
113
+ }
114
+
115
+ function parseArgs(args) {
116
+ const positional = [];
117
+ const flags = {};
118
+ for (let i = 0; i < args.length; i++) {
119
+ const arg = args[i];
120
+ if (Object.hasOwn(flags, arg)) {fail('USAGE', 'usage');}
121
+ if (['--json', '--yes', '--help', '-h'].includes(arg)) { flags[arg] = true; continue; }
122
+ if (['--config', '--backup', '--expected-hash'].includes(arg)) {
123
+ if (!args[i + 1] || args[i + 1].startsWith('-')) {fail('USAGE', 'usage');}
124
+ flags[arg] = args[++i]; continue;
125
+ }
126
+ if (arg.startsWith('-')) {fail('USAGE', 'usage');}
127
+ positional.push(arg);
128
+ }
129
+ if (flags['--help'] || flags['-h']) {return { help: true };}
130
+ const [action, appType, formUuid, messageId] = positional;
131
+ const targeted = ['inspect', 'update', 'delete'].includes(action);
132
+ if (!ACTIONS.includes(action) || positional.length !== (targeted ? 4 : 3)
133
+ || !ID.test(appType || '') || !ID.test(formUuid || '') || (targeted && !ID.test(messageId || ''))
134
+ || (['create', 'update'].includes(action) && !flags['--config'])
135
+ || (action === 'delete' && (!flags['--yes'] || !flags['--expected-hash']))) {fail('USAGE', 'usage');}
136
+ const allowed = { list: [], inspect: [], options: [], create: ['--config', '--backup'],
137
+ update: ['--config', '--backup', '--expected-hash'], delete: ['--yes', '--backup', '--expected-hash'] };
138
+ if (Object.keys(flags).some(key => key !== '--json' && !allowed[action].includes(key))) {fail('USAGE', 'usage');}
139
+ return { action, appType, formUuid, messageId, flags };
140
+ }
141
+
142
+ function fieldId(field) { return field.fieldId || field.id || field.componentId; }
143
+ function fieldType(field) { return field.componentName || field.fieldType || field.type; }
144
+
145
+ function normalizeTemplate(value) {
146
+ if (!object(value)) {fail('INVALID_CONFIG', 'invalid_config', 'template');}
147
+ const registry = value.key ? TEMPLATES[value.key] : TEMPLATE_BY_UUID.get(value.uuid);
148
+ if (!registry || (value.uuid && value.uuid !== registry.uuid) || (value.type && value.type !== registry.type)) {
149
+ fail('INVALID_CONFIG', 'invalid_config', 'template.key / template.uuid / template.type');
150
+ }
151
+ const result = { ...value, key: value.key || TEMPLATE_BY_UUID.get(registry.uuid).key,
152
+ uuid: registry.uuid, type: registry.type };
153
+ result.title = String(result.title || '').trim();
154
+ result.content = String(result.content || '').trim();
155
+ result.fields = result.fields || [];
156
+ result.image = result.image || [];
157
+ result.contentType = 'CUSTOM';
158
+ result.platformTemplateUuid = null;
159
+ result.button = result.button || null;
160
+ if (!result.title || result.title.replace(/\$!\{\w+\}/g, '').length > 50) {fail('INVALID_CONFIG', 'invalid_config', 'template.title');}
161
+ if (registry.content && !result.content) {fail('INVALID_CONFIG', 'invalid_config', 'template.content');}
162
+ if (registry.fields && (!Array.isArray(result.fields) || !result.fields.length)) {fail('INVALID_CONFIG', 'invalid_config', 'template.fields');}
163
+ if (!Array.isArray(result.fields) || !Array.isArray(result.image)) {fail('INVALID_CONFIG', 'invalid_config', 'template.fields / template.image');}
164
+ if (result.button !== undefined && result.button !== null) {
165
+ if (!object(result.button) || typeof result.button.name !== 'string'
166
+ || !['formInstDetail', 'comment', 'custom'].includes(result.button.type)) {fail('INVALID_CONFIG', 'invalid_config', 'template.button');}
167
+ if (result.button.type === 'custom') {
168
+ let url; try { url = new URL(result.button.value); } catch (_) { fail('INVALID_CONFIG', 'invalid_config', 'template.button.value'); }
169
+ if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password) {fail('INVALID_CONFIG', 'invalid_config', 'template.button.value');}
170
+ }
171
+ }
172
+ return { result, registry };
173
+ }
174
+
175
+ /** Validate references against the target form metadata before a mutation. */
176
+ function validateConfig(value, metadata) {
177
+ if (!object(value) || !FORM_TRIGGERS.has(value.trigger)) {
178
+ fail('UNSUPPORTED_TRIGGER', 'unsupported_trigger', value?.trigger || '');
179
+ }
180
+ const formType = metadata.form.formType;
181
+ if (formType === 'process') {fail('UNSUPPORTED_FORM', 'unsupported_form', formType);}
182
+ const { result: template, registry } = normalizeTemplate(value.template);
183
+ const recipients = value.recipients || [];
184
+ const groups = value.groups || [];
185
+ if (!Array.isArray(recipients) || !Array.isArray(groups)) {fail('INVALID_CONFIG', 'invalid_config', 'recipients / groups');}
186
+ if (registry.target === 'person' && !recipients.length) {fail('INVALID_CONFIG', 'invalid_config', 'recipients');}
187
+ if (registry.target === 'group' && !groups.length) {fail('INVALID_CONFIG', 'invalid_config', 'groups');}
188
+ if (recipients.length > 100) {fail('INVALID_CONFIG', 'invalid_config', 'recipients (max 100)');}
189
+ const seen = new Set();
190
+ for (const receiver of recipients) {
191
+ if (!object(receiver) || !RECEIVER_TYPES.has(receiver.type) || !ID.test(String(receiver.value || ''))
192
+ || seen.has(`${receiver.type}:${receiver.value}`)) {fail('INVALID_CONFIG', 'invalid_config', 'recipients');}
193
+ seen.add(`${receiver.type}:${receiver.value}`);
194
+ if (receiver.type === 'variable' && !SYSTEM_VARIABLES.has(receiver.value)) {fail('INVALID_CONFIG', 'invalid_config', `recipient variable: ${receiver.value}`);}
195
+ if (receiver.type === 'formField' && !metadata.recipientFields.some(field => fieldId(field) === receiver.value)) {
196
+ fail('INVALID_CONFIG', 'invalid_config', `recipient field: ${receiver.value}`);
197
+ }
198
+ }
199
+ for (const group of groups) {
200
+ if (!object(group) || !ID.test(String(group.cid || group.id || ''))) {fail('INVALID_CONFIG', 'invalid_config', 'groups');}
201
+ }
202
+ for (const item of template.fields) {
203
+ if (!object(item) || !['variable', 'formField'].includes(item.type) || typeof item.title !== 'string' || !item.title.trim()
204
+ || !ID.test(String(item.value || ''))) {fail('INVALID_CONFIG', 'invalid_config', 'template.fields');}
205
+ if (item.type === 'variable' && !metadata.systemFields.some(field => field.value === item.value)) {
206
+ fail('INVALID_CONFIG', 'invalid_config', `template variable: ${item.value}`);
207
+ }
208
+ if (item.type === 'formField' && !metadata.messageFields.some(field => fieldId(field) === item.value)) {
209
+ fail('INVALID_CONFIG', 'invalid_config', `template field: ${item.value}`);
210
+ }
211
+ }
212
+ return { trigger: value.trigger, recipients: clone(recipients), groups: clone(groups), template };
213
+ }
214
+
215
+ function patchConfig(base, patch) {
216
+ if (!object(patch) || !Object.keys(patch).length || Object.keys(patch).some(key => !['trigger', 'recipients', 'groups', 'template'].includes(key))) {
217
+ fail('INVALID_CONFIG', 'invalid_config', 'trigger, recipients, groups, template');
218
+ }
219
+ safe(patch);
220
+ const next = { ...base, ...patch };
221
+ if (patch.template) {next.template = { ...base.template, ...patch.template };}
222
+ return next;
223
+ }
224
+
225
+ function editable(message) {
226
+ return { trigger: message.trigger, recipients: clone(message.recipients), groups: clone(message.groups), template: clone(message.template) };
227
+ }
228
+
229
+ function serialize(config, formUuid, messageId, native = {}) {
230
+ const byType = type => config.recipients.filter(item => item.type === type).map(item => item.value).join(',');
231
+ const registered = config.template.key ? TEMPLATES[config.template.key] : TEMPLATE_BY_UUID.get(config.template.uuid);
232
+ const nativeCardConfig = native.cardConfig ? parseJson(native.cardConfig, 'cardConfig') : {};
233
+ const cardConfig = {
234
+ ...nativeCardConfig,
235
+ openMore: false,
236
+ groups: config.groups,
237
+ messageImage: config.template.image,
238
+ cardTitle: config.template.title,
239
+ mainContent: config.template.content,
240
+ datas: config.template.fields,
241
+ templateUuid: config.template.uuid || registered.uuid,
242
+ templateType: config.template.type || registered.type,
243
+ messageContentType: 'CUSTOM',
244
+ buttons: config.template.button ? [config.template.button] : [],
245
+ };
246
+ return {
247
+ ...clone(native),
248
+ service: native.service || '', formUuid, phase: config.trigger, outResult: native.outResult || '',
249
+ formField: byType('formField'), studentField: native.studentField || '', roleId: byType('role'),
250
+ persons: byType('employee'), varField: native.varField || '', messageUuid: '', id: messageId || '',
251
+ variable: byType('variable'), email: native.email || '', formFieldEmail: native.formFieldEmail || '',
252
+ activityId: native.activityId || '', messageType: native.messageType || 'NORMAL',
253
+ curDingGroup: native.curDingGroup || 'n', isSendMessage: native.isSendMessage || 'n',
254
+ processId: native.processId || '', conditionJson: native.conditionJson || '', cardConfig: JSON.stringify(cardConfig),
255
+ };
256
+ }
257
+
258
+ async function execute(parsed, client = createYidaClient()) {
259
+ const { action, appType, formUuid, messageId, flags } = parsed;
260
+ const prefix = `/${appType}/query/`;
261
+ const read = async (endpoint, params = {}) => unwrap(await client.get(prefix + endpoint, { formUuid, ...params }), endpoint);
262
+ const list = async () => {
263
+ const rows = await read('formMsgTpl/listFormMessageBind.json');
264
+ if (!Array.isArray(rows)) {fail('INVALID_RESPONSE', 'invalid_response', 'messages');}
265
+ const messages = rows.map(normalize);
266
+ if (messages.some(item => item.formUuid && item.formUuid !== formUuid)
267
+ || new Set(messages.map(item => item.id)).size !== messages.length) {fail('INVALID_RESPONSE', 'invalid_response', 'messages');}
268
+ return messages;
269
+ };
270
+ const metadata = async () => {
271
+ const info = await read('formdesign/getFormAndAppInfo.json', { appType });
272
+ const form = info?.formDatas?.values?.find(item => item.formUuid === formUuid);
273
+ if (!form || !Array.isArray(form.fields)) {fail('INVALID_RESPONSE', 'invalid_response', 'form metadata');}
274
+ const fields = form.fields;
275
+ return {
276
+ form: { formUuid, formType: form.formType, title: form.title, processCode: form.processCode || null },
277
+ triggers: [...FORM_TRIGGERS],
278
+ readOnlyTriggers: ['RECUR_JOB_FIELD', 'INST_START', 'INST_COMPLETE', 'INST_NODE', 'TASK_GENERATE', 'TASK_COMPLETED'],
279
+ templates: Object.entries(TEMPLATES).map(([key, item]) => ({ key, ...item })),
280
+ systemFields: [
281
+ { value: 'formInstId', componentName: 'TextField' }, { value: 'originatorName', componentName: 'TextField' },
282
+ { value: 'originator', componentName: 'TextField' }, { value: 'createTime', componentName: 'DateField' },
283
+ { value: 'modifiedTime', componentName: 'DateField' }, { value: 'serialNo', componentName: 'TextField' },
284
+ { value: 'formTitle', componentName: 'TextField' }, { value: 'title', componentName: 'TextField' },
285
+ ],
286
+ messageFields: fields.filter(field => MESSAGE_FIELD_COMPONENTS.has(fieldType(field))),
287
+ recipientFields: fields.filter(field => RECIPIENT_FIELD_COMPONENTS.has(fieldType(field))),
288
+ recipientVariables: [...SYSTEM_VARIABLES],
289
+ supportsToggle: false,
290
+ };
291
+ };
292
+ // Link to the form's native notification settings in the authenticated environment.
293
+ const configUrl = new URL(`/${appType}/admin/${formUuid}/settings/messageSetting`, client.getAuthRef().baseUrl).href;
294
+ const output = { success: true, action, appType, formUuid, configUrl };
295
+ if (action === 'options') {return { ...output, options: await metadata() };}
296
+ const beforeList = await list();
297
+ const before = messageId ? beforeList.find(item => item.id === messageId) : null;
298
+ if (messageId && !before) {fail('NOT_FOUND', 'not_found', messageId);}
299
+ if (action === 'list') {return { ...output, messages: beforeList, totalCount: beforeList.length, supportsToggle: false };}
300
+ if (action === 'inspect') {return { ...output, messageId, message: before, configHash: hash(editable(before)), supportsToggle: false };}
301
+ if (flags['--expected-hash'] && flags['--expected-hash'] !== hash(editable(before))) {fail('CONFLICT', 'conflict');}
302
+
303
+ let expected;
304
+ if (action !== 'delete') {
305
+ let patch;
306
+ try { patch = parseJson(fs.readFileSync(path.resolve(flags['--config']), 'utf8'), 'config'); }
307
+ catch (error) { if (error instanceof CliError) {throw error;} fail('INVALID_CONFIG', 'invalid_config', 'config file'); }
308
+ const defaults = { trigger: 'FORM_DATA_CREATE', recipients: [{ type: 'variable', value: 'originator' }], groups: [],
309
+ template: { key: 'workNotice', title: '', content: '', fields: [], image: [], button: null } };
310
+ expected = validateConfig(patchConfig(before ? editable(before) : defaults, patch), await metadata());
311
+ if (before && isDeepStrictEqual(editable(before), expected)) {
312
+ return { ...output, messageId, changed: false, message: before, configHash: hash(expected), readbackVerified: true };
313
+ }
314
+ }
315
+ const snapshot = beforeList.map(item => [item.id, hash(item.native)]);
316
+ if (!isDeepStrictEqual(snapshot, (await list()).map(item => [item.id, hash(item.native)]))) {fail('CONFLICT', 'conflict');}
317
+ const backupPath = flags['--backup'] ? path.resolve(flags['--backup']) : path.join(resolveProjectRoot().projectRoot,
318
+ '.cache', 'openyida', 'form-message', `${Date.now()}-${crypto.randomUUID()}.json`);
319
+ fs.mkdirSync(path.dirname(backupPath), { recursive: true });
320
+ fs.writeFileSync(backupPath, JSON.stringify({ appType, formUuid, action, messages: beforeList }, null, 2), { flag: 'wx', mode: 0o600 });
321
+ const details = { target: { appType, formUuid, ...(messageId ? { messageId } : {}) }, backupPath,
322
+ retrySafe: false, sideEffectState: 'unknown', readbackAllowed: true };
323
+ const endpoint = action === 'delete' ? 'formMsgTpl/removeMessageBind.json' : 'formMsgTpl/saveOrUpdateMessageBind.json';
324
+ const params = action === 'delete' ? { id: messageId } : serialize(expected, formUuid, messageId, before?.native);
325
+ try { unwrap(await client.postFormOnce(prefix + endpoint, params), endpoint); }
326
+ catch (error) { throw new CliError(error.message, { code: error.code || 'FORM_MESSAGE_WRITE_FAILED', details: { ...error.details, ...details } }); }
327
+ try {
328
+ const after = await list();
329
+ if (action === 'delete') {
330
+ if (after.some(item => item.id === messageId)) {fail('READBACK_MISMATCH', 'readback_mismatch');}
331
+ return { ...output, messageId, changed: true, backupPath, readbackVerified: true };
332
+ }
333
+ const added = after.filter(item => !beforeList.some(old => old.id === item.id));
334
+ const saved = action === 'create' ? (added.length === 1 ? added[0] : null) : after.find(item => item.id === messageId);
335
+ if (!saved || !isDeepStrictEqual(editable(saved), expected)) {fail('READBACK_MISMATCH', 'readback_mismatch');}
336
+ return { ...output, messageId: saved.id, changed: true, backupPath, readbackVerified: true,
337
+ message: saved, configHash: hash(expected) };
338
+ } catch (error) {
339
+ throw new CliError(error.message, { code: error.code || 'FORM_MESSAGE_READBACK_FAILED',
340
+ details: { ...details, mutationAccepted: true, sideEffectState: 'accepted_unverified' } });
341
+ }
342
+ }
343
+
344
+ async function run(args, options = {}) {
345
+ const parsed = parseArgs(args);
346
+ if (parsed.help) { process.stdout.write(t('form_message.usage') + '\n'); return; }
347
+ const result = await execute(parsed, options.client);
348
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
349
+ return result;
350
+ }
351
+
352
+ module.exports = { ACTIONS, TEMPLATES, run, execute, parseArgs, normalize, validateConfig, patchConfig, serialize, hash, editable };
@@ -10,7 +10,16 @@ const {
10
10
  findEmojiInValue,
11
11
  } = require('../../core/no-emoji-detector');
12
12
 
13
- const CANVAS_YIDA_API_BRIDGE_SOURCE = `var OPENYIDA_YIDA_FORM_API_METHODS = [
13
+ const { createPageNavigation } = require('../../navigation/page-navigation');
14
+ const { createTaskCenterContract } = require('../../task-center/contract');
15
+ const { createTaskCenterLoader } = require('../../task-center/loader');
16
+ const { createTaskCenterNavigation } = require('../../task-center/navigation');
17
+
18
+ const CANVAS_YIDA_API_BRIDGE_SOURCE = `${createPageNavigation.toString()}
19
+ ${createTaskCenterNavigation.toString()}
20
+ ${createTaskCenterContract.toString()}
21
+ ${createTaskCenterLoader.toString()}
22
+ var OPENYIDA_YIDA_FORM_API_METHODS = [
14
23
  'saveFormData',
15
24
  'updateFormData',
16
25
  'searchFormDataIds',
@@ -239,8 +248,15 @@ function openyidaCreateRouterBridge(utils) {
239
248
 
240
249
  function openyidaCreateUtilsBridge(context, yidaBridge) {
241
250
  var utils = context && context.utils;
251
+ var navigation = createPageNavigation(window);
252
+ var taskNavigation = createTaskCenterNavigation();
253
+ var taskCounts = createTaskCenterLoader(createTaskCenterContract(), function() { return yidaBridge; });
242
254
  var methodNames = openyidaCollectUtilsMethodNames(utils);
243
255
  function invoke(methodName) {
256
+ if (methodName === 'openPage') { return navigation.open.apply(null, Array.prototype.slice.call(arguments, 1)); }
257
+ if (['taskCenterCount', 'taskCenterLink', 'openTaskCenter'].indexOf(methodName) !== -1) {
258
+ return bridge[methodName].apply(null, Array.prototype.slice.call(arguments, 1));
259
+ }
244
260
  if (!utils || typeof utils[methodName] !== 'function') {
245
261
  throw new Error('this.utils.' + methodName + ' is not available');
246
262
  }
@@ -251,6 +267,8 @@ function openyidaCreateUtilsBridge(context, yidaBridge) {
251
267
  apiGroups: {
252
268
  utility: OPENYIDA_UTIL_API_METHODS.slice(),
253
269
  router: ['push'],
270
+ navigation: ['openPage', 'taskCenterLink', 'openTaskCenter'],
271
+ tasks: ['taskCenterCount'],
254
272
  yida: OPENYIDA_KNOWN_YIDA_API_METHODS.slice()
255
273
  },
256
274
  knownMethods: OPENYIDA_UTIL_API_METHODS.slice(),
@@ -259,9 +277,33 @@ function openyidaCreateUtilsBridge(context, yidaBridge) {
259
277
  return !utils || typeof utils[methodName] !== 'function';
260
278
  }),
261
279
  invoke: invoke,
280
+ openPage: navigation.open,
281
+ taskCenterLink: function(options) {
282
+ return taskNavigation.target(Object.assign({ baseUrl: window.location.origin }, options));
283
+ },
284
+ taskCenterCount: async function(options) {
285
+ if (!options || typeof options !== 'object' || Array.isArray(options)
286
+ || Object.keys(options).some(function(key) { return ['scope', 'appType', 'kind', 'filters'].indexOf(key) === -1; })) {
287
+ var error = new Error('TASK_FILTER_INVALID: options');
288
+ error.code = 'TASK_FILTER_INVALID';
289
+ throw error;
290
+ }
291
+ var target = taskNavigation.target({ scope: options.scope, appType: options.appType, kind: options.kind, baseUrl: window.location.origin });
292
+ var filters = createTaskCenterContract().normalizeFilters(target.kind, options.filters === undefined ? {} : options.filters, 'app-query');
293
+ return taskCounts.count(target.kind, Object.assign({}, filters, { transport: 'app-query' },
294
+ target.scope === 'corp' ? { scope: 'corp' } : { appType: target.appType }));
295
+ },
296
+ openTaskCenter: function(options) {
297
+ var target = taskNavigation.target(Object.assign({ baseUrl: window.location.origin }, options));
298
+ return navigation.open(target.pageUrl, { mode: target.mode });
299
+ },
262
300
  router: openyidaCreateRouterBridge(utils),
263
301
  yida: yidaBridge
264
302
  };
303
+ ['openPage', 'taskCenterLink', 'openTaskCenter', 'taskCenterCount'].forEach(function(methodName) {
304
+ if (bridge.availableMethods.indexOf(methodName) === -1) { bridge.availableMethods.push(methodName); }
305
+ });
306
+ bridge.missingKnownMethods = bridge.missingKnownMethods.filter(function(name) { return name !== 'openPage'; });
265
307
  methodNames.forEach(function(methodName) {
266
308
  if (!bridge[methodName]) {
267
309
  bridge[methodName] = function() {
@@ -175,7 +175,6 @@ function normalizeTokenResponse(response, baseUrl, clientId) {
175
175
  );
176
176
  return {
177
177
  auth_mode: 'token',
178
- credential_source: data.credential_source,
179
178
  status: data.status || 'ok',
180
179
  can_auto_use: true,
181
180
  message: data.message || data.errorMsg || response.errorMsg,
@@ -487,9 +486,7 @@ async function tokenRefreshUnlocked(options, session) {
487
486
  // Token exchange follows the active environment endpoint, just like the
488
487
  // legacy login bootstrap. The response may return a different corp base_url
489
488
  // for business requests and generated application links.
490
- // 桌面会话沿用首次换票的可信 origin;刷新不受后续全局环境切换影响。
491
- const desktopSession = session.credential_source === 'qwenwork_desktop';
492
- const authBaseUrlValue = desktopSession ? session.base_url : resolveTokenBaseUrl({
489
+ const authBaseUrlValue = resolveTokenBaseUrl({
493
490
  projectRoot: options.projectRoot,
494
491
  endpoint: options.endpoint,
495
492
  });
@@ -502,9 +499,6 @@ async function tokenRefreshUnlocked(options, session) {
502
499
  requireOkResponse(response);
503
500
  const data = getTokenPayload(response);
504
501
  const responseBusinessBaseUrl = data.base_url || data.baseUrl;
505
- if (desktopSession && responseBusinessBaseUrl && normalizeBaseUrl(responseBusinessBaseUrl, null) !== session.base_url) {
506
- throw createTokenAuthRequiredError('refresh_origin_mismatch');
507
- }
508
502
  const businessBaseUrl = session.base_url || authBaseUrlValue;
509
503
  const normalized = normalizeTokenResponse({
510
504
  ...data,
@@ -513,7 +507,6 @@ async function tokenRefreshUnlocked(options, session) {
513
507
  user_id: data.user_id || data.userId || session.user_id,
514
508
  user_name: data.user_name || data.userName || session.user_name,
515
509
  refresh_token: data.refresh_token || data.refreshToken || session.refresh_token,
516
- credential_source: data.credential_source || session.credential_source,
517
510
  }, businessBaseUrl, clientId);
518
511
  if (!normalized.access_token) {
519
512
  return {
@@ -276,7 +276,6 @@ function normalizeTokenSession(session = {}) {
276
276
  );
277
277
  const normalized = {
278
278
  auth_mode: 'token',
279
- credential_source: session.credential_source,
280
279
  token_type: session.token_type || session.tokenType || 'Bearer',
281
280
  access_token: session.access_token || session.accessToken,
282
281
  refresh_token: session.refresh_token || session.refreshToken,