openyida 2026.10.26 → 2026.10.27-beta.2
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 +24 -5
- package/bin/yida-pre-beta.js +11 -0
- package/bin/yida.js +20 -15
- package/lib/app/services/canvas-page-schema-builder.js +1 -43
- package/lib/auth/qwenwork-client.js +190 -0
- package/lib/auth/qwenwork-cloud.js +93 -0
- package/lib/auth/qwenwork-desktop.js +154 -0
- package/lib/auth/qwenwork-login.js +108 -0
- package/lib/auth/qwenwork.js +37 -0
- package/lib/auth/token-auth.js +12 -1
- package/lib/auth/token-store.js +1 -0
- package/lib/core/agent-capabilities.js +60 -5
- package/lib/core/command-manifest.js +10 -73
- package/lib/core/locales/en.js +19 -30
- package/lib/core/locales/zh.js +18 -29
- package/lib/core/qwenwork-login-guidance.js +35 -0
- package/lib/core/task-center.js +4 -32
- package/lib/permission/get-permission.js +38 -11
- package/lib/permission/permit-package-service.js +20 -80
- package/lib/permission/save-permission.js +73 -35
- package/lib/samples/openyida-scaffold/canvas-navigation.jsx +2 -6
- package/lib/task-center/contract.js +1 -26
- package/lib/task-center/loader.js +0 -5
- package/lib/task-center/scaffold.js +0 -9
- package/package.json +18 -5
- package/scripts/install-qwenwork-pre-beta.js +40 -0
- package/yida-skills/SKILL.md +13 -7
- package/yida-skills/references/qwenwork-cloud-login.md +21 -0
- package/yida-skills/references/qwenwork-desktop-login.md +12 -0
- package/yida-skills/references/setup-and-env.md +10 -1
- package/yida-skills/references/yida-api.md +1 -3
- package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +2 -4
- package/yida-skills/skills/yida-canvas-custom-page/references/canvas-style-implementation-guide.md +0 -2
- package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +0 -2
- package/yida-skills/skills/yida-canvas-data-binding/SKILL.md +2 -2
- package/yida-skills/skills/yida-canvas-data-binding/references/workflow-capabilities.md +1 -29
- package/yida-skills/skills/yida-create-page/SKILL.md +0 -1
- package/yida-skills/skills/yida-design/SKILL.md +1 -1
- package/yida-skills/skills/yida-design/references/application-style-library.md +0 -2
- package/yida-skills/skills/yida-design/references/application-theme-consistency.md +0 -4
- package/yida-skills/skills/yida-design/sub_skill/page-design/SKILL.md +44 -27
- package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +1 -3
- package/yida-skills/skills/yida-form-permission/SKILL.md +1 -1
- package/yida-skills/skills/yida-login/SKILL.md +24 -1
- package/yida-skills/skills/yida-task-center/SKILL.md +1 -3
- package/yida-skills/skills-index.json +0 -26
- package/lib/app/form-message.js +0 -352
- package/lib/navigation/page-navigation.js +0 -62
- package/lib/solution-template/solution-template.js +0 -666
- package/lib/task-center/navigation.js +0 -41
- package/yida-skills/skills/yida-form-message/SKILL.md +0 -53
- package/yida-skills/skills/yida-form-message/references/guide.md +0 -111
- package/yida-skills/skills/yida-solution-template/SKILL.md +0 -98
package/README.md
CHANGED
|
@@ -143,6 +143,27 @@ openyida login --check-only --json
|
|
|
143
143
|
openyida auth status
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
+
In QwenWork web, `openyida login --qwenwork --json` (or ordinary `login`) sends the sandbox Agent Token and gateway origin directly to the selected YiDA authentication endpoint. YiDA validates the identity through the trusted gateway’s fixed `/api/agent-gw/yida/user_info` endpoint, then issues its own short-lived session. This login flow no longer calls the legacy exchange or `/user/info` endpoints. Login preparation stays silent; failures stop without browser OAuth. Desktop and other hosts retain their existing authentication paths.
|
|
147
|
+
|
|
148
|
+
For QwenWork cloud identity diagnostics, run inside the actual cloud execution environment:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
openyida auth qwenwork user-info --json
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
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.
|
|
155
|
+
|
|
156
|
+
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.
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
const { exchangeQwenWorkAgentToken, getQwenWorkUserInfo } = require('openyida/lib/auth/qwenwork-client');
|
|
160
|
+
async function readCloudUser() {
|
|
161
|
+
const credential = await exchangeQwenWorkAgentToken();
|
|
162
|
+
// The result contains QwenWork identifiers, not YiDA IDs.
|
|
163
|
+
return getQwenWorkUserInfo(credential);
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
146
167
|
To reuse an existing shared login profile for the current project, switch first instead of starting a new OAuth login:
|
|
147
168
|
|
|
148
169
|
```bash
|
|
@@ -457,9 +478,9 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
|
|
|
457
478
|
|
|
458
479
|
| Command | Description |
|
|
459
480
|
|---------|-------------|
|
|
460
|
-
| `openyida login [target-url] [--env <name>\|--intl\|--overseas\|--global\|--yidaapps\|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser]` | Login with OAuth token mode |
|
|
481
|
+
| `openyida login [--qwenwork] [target-url] [--env <name>\|--intl\|--overseas\|--global\|--yidaapps\|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser]` | Login with OAuth token mode |
|
|
461
482
|
| `openyida logout` | Logout / unbind current project auth |
|
|
462
|
-
| `openyida auth <status\|login\|refresh\|logout\|profiles\|profile switch
|
|
483
|
+
| `openyida auth <status\|login\|refresh\|logout\|profiles\|profile switch\|qwenwork user-info> [--json]` | Token login state and profile management |
|
|
463
484
|
| `openyida org <list\|switch> [--json] [--corp-id <corpId>]` | Organization management (list / switch existing profiles first) |
|
|
464
485
|
| `openyida env [--json\|setup\|list\|show\|switch\|add\|remove] [options]` | Detect AI tool environment & token login state |
|
|
465
486
|
|
|
@@ -473,7 +494,6 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
|
|
|
473
494
|
| `openyida app-entry get <appType> [--json]` | Read frontend and management runtime entries |
|
|
474
495
|
| `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 |
|
|
475
496
|
| `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 |
|
|
477
497
|
| `openyida corp-efficiency [overview\|details\|detail\|groups\|notify] [options] [--open\|--no-open]` | Query enterprise efficiency overview and detail reports |
|
|
478
498
|
| `openyida create-app "<name>"\|--name <name> [options] [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Create a Yida app |
|
|
479
499
|
| `openyida design-plan catalog [--json]` | List available planning themes and page patterns |
|
|
@@ -517,7 +537,6 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
|
|
|
517
537
|
| `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 |
|
|
518
538
|
| `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 |
|
|
519
539
|
| `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 |
|
|
521
540
|
| `openyida get-schema <appType> <formUuid\|--all> [--summary-json\|--field-map-json\|--analysis-json]` | Get one form Schema or all form Schemas |
|
|
522
541
|
| `openyida check-prd-completeness <prd.md> --app-type <appType> [--build-manifest <file>] [--json]` | Check resources, frontend menu hiding and application theme delivery |
|
|
523
542
|
| `openyida check-design <design.md> [--prd <prd.md>] [--base-dir <dir>] [--json]` | Validate design documents, theme tokens and PRD handoff |
|
|
@@ -535,7 +554,7 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
|
|
|
535
554
|
| Command | Description |
|
|
536
555
|
|---------|-------------|
|
|
537
556
|
| `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) |
|
|
538
|
-
| `openyida task-center <todo\|created\|done\|cc\|proxy\|capabilities\|
|
|
557
|
+
| `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.) |
|
|
539
558
|
| `openyida basic-info <overview\|commodity\|grant\|capacity\|quota\|abs-path\|dataflow\|i18n\|domain>` | Query organization basic info, capacity, quotas, and domain settings |
|
|
540
559
|
| `openyida read-dingtalk-doc <docUrl> [--output <file>] [--json]` | Fetch Markdown content from a DingTalk document |
|
|
541
560
|
| `openyida read-dingtalk-tingji <taskUuid> [--json]` | Fetch DingTalk Tingji details by task UUID |
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const config = require('../package.json').openyidaLocalBeta;
|
|
6
|
+
// PRE beta 消费包内测试默认值,真实凭据仍由原认证模块管理。
|
|
7
|
+
process.env.OPENYIDA_ENDPOINT ||= config.baseUrl;
|
|
8
|
+
process.env.OPENYIDA_ENV ||= config.environment;
|
|
9
|
+
process.env.OPENYIDA_AUTH_DIR ||= path.join(os.homedir(), '.openyida', config.authDirectory);
|
|
10
|
+
process.env.OPENYIDA_SKIP_UPDATE_CHECK = '1';
|
|
11
|
+
require('./yida.js');
|
package/bin/yida.js
CHANGED
|
@@ -588,17 +588,29 @@ 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
|
-
}
|
|
595
591
|
commandReceipt = beginCommandReceipt({ command, args: rawArgs });
|
|
596
592
|
process.once('exit', code => {
|
|
597
593
|
try {commandReceipt.finish(code);} catch {process.exitCode = 1;}
|
|
598
594
|
});
|
|
599
595
|
assertManagedInvocation(command, rawArgs);
|
|
600
596
|
applyQuietFlag();
|
|
597
|
+
// 一次性票据只在当前进程消费,不能先自更新并重启命令。
|
|
598
|
+
switch (command) {
|
|
599
|
+
case 'login-qwenwork':
|
|
600
|
+
await require('../lib/auth/qwenwork-desktop').run(args);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
601
603
|
applyGlobalEnvironmentFlags();
|
|
604
|
+
// 网页态由 CLI 消费宿主凭据;桌面态继续消费宿主准备的登录态。
|
|
605
|
+
const webLogin = (command === 'login' || (command === 'auth' && args[0] === 'login')) &&
|
|
606
|
+
require('../lib/core/qwenwork-login-guidance').isQwenWorkWebRuntime(require('../lib/core/utils').detectRuntimeCapabilities());
|
|
607
|
+
if ((command === 'login' && args.includes('--qwenwork')) || webLogin) {
|
|
608
|
+
const loginArgs = applyLoginEnvironmentFlags(command === 'auth' ? args.slice(1) : args, { inferTargetUrl: true });
|
|
609
|
+
if (hasHelpFlag(loginArgs)) { printLoginHelp(); return; }
|
|
610
|
+
assertNoUnsupportedLegacyLoginFlags(rawArgs, loginArgs);
|
|
611
|
+
await require('../lib/auth/qwenwork-login').run(loginArgs, buildTokenLoginOptions(loginArgs));
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
602
614
|
|
|
603
615
|
const { maybeAutoUpdate } = require('../lib/core/update');
|
|
604
616
|
const updateResult = await maybeAutoUpdate({
|
|
@@ -718,6 +730,10 @@ async function main() {
|
|
|
718
730
|
|
|
719
731
|
case 'auth': {
|
|
720
732
|
const subCommand = args[0];
|
|
733
|
+
if (subCommand === 'qwenwork') {
|
|
734
|
+
await require('../lib/auth/qwenwork').run(args.slice(1));
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
721
737
|
const authArgs = applyLoginEnvironmentFlags(args.slice(1), { inferTargetUrl: true });
|
|
722
738
|
const { getAuthStatus } = require('../lib/core/utils');
|
|
723
739
|
const { tokenLogin, tokenLogout, tokenRefresh } = require('../lib/auth/token-auth');
|
|
@@ -794,12 +810,6 @@ async function main() {
|
|
|
794
810
|
break;
|
|
795
811
|
}
|
|
796
812
|
|
|
797
|
-
case 'solution-template': {
|
|
798
|
-
const { run } = require('../lib/solution-template/solution-template');
|
|
799
|
-
await run(args);
|
|
800
|
-
break;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
813
|
case 'corp-efficiency': {
|
|
804
814
|
const { run } = require('../lib/corp-efficiency/corp-efficiency');
|
|
805
815
|
await run(args);
|
|
@@ -847,11 +857,6 @@ async function main() {
|
|
|
847
857
|
await run(args);
|
|
848
858
|
break;
|
|
849
859
|
}
|
|
850
|
-
case 'form-message': {
|
|
851
|
-
const { run } = require('../lib/app/form-message');
|
|
852
|
-
await run(args);
|
|
853
|
-
break;
|
|
854
|
-
}
|
|
855
860
|
case 'print-template': {
|
|
856
861
|
const { run } = require('../lib/print-template/print-template');
|
|
857
862
|
await run(args);
|
|
@@ -10,16 +10,7 @@ const {
|
|
|
10
10
|
findEmojiInValue,
|
|
11
11
|
} = require('../../core/no-emoji-detector');
|
|
12
12
|
|
|
13
|
-
const
|
|
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 = [
|
|
13
|
+
const CANVAS_YIDA_API_BRIDGE_SOURCE = `var OPENYIDA_YIDA_FORM_API_METHODS = [
|
|
23
14
|
'saveFormData',
|
|
24
15
|
'updateFormData',
|
|
25
16
|
'searchFormDataIds',
|
|
@@ -248,15 +239,8 @@ function openyidaCreateRouterBridge(utils) {
|
|
|
248
239
|
|
|
249
240
|
function openyidaCreateUtilsBridge(context, yidaBridge) {
|
|
250
241
|
var utils = context && context.utils;
|
|
251
|
-
var navigation = createPageNavigation(window);
|
|
252
|
-
var taskNavigation = createTaskCenterNavigation();
|
|
253
|
-
var taskCounts = createTaskCenterLoader(createTaskCenterContract(), function() { return yidaBridge; });
|
|
254
242
|
var methodNames = openyidaCollectUtilsMethodNames(utils);
|
|
255
243
|
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
|
-
}
|
|
260
244
|
if (!utils || typeof utils[methodName] !== 'function') {
|
|
261
245
|
throw new Error('this.utils.' + methodName + ' is not available');
|
|
262
246
|
}
|
|
@@ -267,8 +251,6 @@ function openyidaCreateUtilsBridge(context, yidaBridge) {
|
|
|
267
251
|
apiGroups: {
|
|
268
252
|
utility: OPENYIDA_UTIL_API_METHODS.slice(),
|
|
269
253
|
router: ['push'],
|
|
270
|
-
navigation: ['openPage', 'taskCenterLink', 'openTaskCenter'],
|
|
271
|
-
tasks: ['taskCenterCount'],
|
|
272
254
|
yida: OPENYIDA_KNOWN_YIDA_API_METHODS.slice()
|
|
273
255
|
},
|
|
274
256
|
knownMethods: OPENYIDA_UTIL_API_METHODS.slice(),
|
|
@@ -277,33 +259,9 @@ function openyidaCreateUtilsBridge(context, yidaBridge) {
|
|
|
277
259
|
return !utils || typeof utils[methodName] !== 'function';
|
|
278
260
|
}),
|
|
279
261
|
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
|
-
},
|
|
300
262
|
router: openyidaCreateRouterBridge(utils),
|
|
301
263
|
yida: yidaBridge
|
|
302
264
|
};
|
|
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'; });
|
|
307
265
|
methodNames.forEach(function(methodName) {
|
|
308
266
|
if (!bridge[methodName]) {
|
|
309
267
|
bridge[methodName] = function() {
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { CliError } = require('../core/cli-error');
|
|
4
|
+
const { t } = require('../core/i18n');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 20000;
|
|
7
|
+
const MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
function failure(code, message, stage, details = {}) {
|
|
10
|
+
return new CliError(t(`qwenwork.${message}`), {
|
|
11
|
+
code: `QWENWORK_${code}`,
|
|
12
|
+
details: { stage, ...details },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function pick(env, names) {
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
const value = String(env[name] || '').trim();
|
|
19
|
+
if (value) {
|
|
20
|
+
return { name, value };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeBaseUrl(value) {
|
|
27
|
+
try {
|
|
28
|
+
const url = new URL(value);
|
|
29
|
+
const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
|
|
30
|
+
if ((url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) ||
|
|
31
|
+
url.username || url.password || url.search || url.hash) {
|
|
32
|
+
throw new Error('invalid_url');
|
|
33
|
+
}
|
|
34
|
+
return url.href.replace(/\/+$/, '');
|
|
35
|
+
} catch {
|
|
36
|
+
throw failure('INVALID_GATEWAY', 'invalid_url', 'resolve_credentials');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isObject(value) {
|
|
41
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Do not include upstream bodies, headers or exception messages in diagnostics:
|
|
45
|
+
// an authentication endpoint may echo credentials even in an error response.
|
|
46
|
+
async function requestJson(method, url, accessToken, options, stage) {
|
|
47
|
+
const timeoutMs = options.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : options.timeoutMs;
|
|
48
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
49
|
+
throw failure('INVALID_TIMEOUT', 'request_failed', stage);
|
|
50
|
+
}
|
|
51
|
+
const controller = new globalThis.AbortController();
|
|
52
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
53
|
+
try {
|
|
54
|
+
const response = await globalThis.fetch(url, {
|
|
55
|
+
method,
|
|
56
|
+
headers: { accept: 'application/json', authorization: `Bearer ${accessToken}` },
|
|
57
|
+
redirect: 'manual',
|
|
58
|
+
signal: controller.signal,
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
if (response.body) {
|
|
62
|
+
await response.body.cancel();
|
|
63
|
+
}
|
|
64
|
+
throw failure('HTTP_ERROR', 'request_failed', stage, { status_code: response.status });
|
|
65
|
+
}
|
|
66
|
+
const chunks = [];
|
|
67
|
+
let size = 0;
|
|
68
|
+
if (response.body) {
|
|
69
|
+
const reader = response.body.getReader();
|
|
70
|
+
let next = await reader.read();
|
|
71
|
+
while (!next.done) {
|
|
72
|
+
const { value } = next;
|
|
73
|
+
size += value.byteLength;
|
|
74
|
+
if (size > MAX_RESPONSE_BYTES) {
|
|
75
|
+
await reader.cancel();
|
|
76
|
+
throw failure('RESPONSE_TOO_LARGE', 'invalid_response', stage);
|
|
77
|
+
}
|
|
78
|
+
chunks.push(Buffer.from(value));
|
|
79
|
+
next = await reader.read();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
let payload;
|
|
83
|
+
try {
|
|
84
|
+
payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
85
|
+
} catch {
|
|
86
|
+
throw failure('INVALID_RESPONSE', 'invalid_response', stage);
|
|
87
|
+
}
|
|
88
|
+
if (!isObject(payload) || (payload.code !== undefined && payload.code !== 'ok')) {
|
|
89
|
+
throw failure('INVALID_RESPONSE', 'invalid_response', stage);
|
|
90
|
+
}
|
|
91
|
+
return payload;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error && error.isCliError) {
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
throw controller.signal.aborted
|
|
97
|
+
? failure('TIMEOUT', 'request_timeout', stage)
|
|
98
|
+
: failure('NETWORK_ERROR', 'request_failed', stage);
|
|
99
|
+
} finally {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function decodeClaims(token) {
|
|
105
|
+
try {
|
|
106
|
+
const value = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8'));
|
|
107
|
+
return isObject(value) ? value : {};
|
|
108
|
+
} catch {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Exchange a cloud Agent credential for an in-memory scoped user credential.
|
|
114
|
+
* This does not issue or persist an OpenYida session.
|
|
115
|
+
*/
|
|
116
|
+
async function exchangeQwenWorkAgentToken(options = {}) {
|
|
117
|
+
const env = options.env || process.env;
|
|
118
|
+
const agent = pick(env, ['QWENWORK_AGENT_TOKEN', 'MULE_AGENT_TOKEN', 'AGENT_TOKEN']);
|
|
119
|
+
if (!agent || /[\r\n]/.test(agent.value)) {
|
|
120
|
+
throw failure('CREDENTIAL_MISSING', 'credential_missing', 'resolve_credentials');
|
|
121
|
+
}
|
|
122
|
+
const gateway = pick(env, ['QWENWORK_GATEWAY_URL', 'MULE_GATEWAY_URL']);
|
|
123
|
+
if (!gateway) {
|
|
124
|
+
throw failure('GATEWAY_MISSING', 'gateway_missing', 'resolve_credentials');
|
|
125
|
+
}
|
|
126
|
+
const baseUrl = normalizeBaseUrl(gateway.value);
|
|
127
|
+
const response = await requestJson('POST', `${baseUrl}/api/agent-gw/token/exchange`, agent.value, options, 'exchange');
|
|
128
|
+
// The current cloud endpoint returns {token, scope, ttl} without an envelope.
|
|
129
|
+
const payload = isObject(response.data) ? response.data : response;
|
|
130
|
+
if (typeof payload.token !== 'string' || !payload.token.trim() || /\s/.test(payload.token) ||
|
|
131
|
+
typeof payload.scope !== 'string' || !Number.isFinite(payload.ttl) || payload.ttl <= 0) {
|
|
132
|
+
throw failure('INVALID_RESPONSE', 'invalid_response', 'exchange');
|
|
133
|
+
}
|
|
134
|
+
const decoded = decodeClaims(payload.token);
|
|
135
|
+
if (typeof decoded.exp === 'number' && decoded.exp * 1000 <= Date.now()) {
|
|
136
|
+
throw failure('TOKEN_EXPIRED', 'expired_token', 'exchange');
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
access_token: payload.token,
|
|
140
|
+
base_url: baseUrl,
|
|
141
|
+
credential_source: agent.name,
|
|
142
|
+
scope: payload.scope,
|
|
143
|
+
expires_in: payload.ttl,
|
|
144
|
+
expires_at: typeof decoded.exp === 'number' ? decoded.exp * 1000 : Date.now() + payload.ttl * 1000,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Query the credential's own origin; never infer a YiDA endpoint or organization. */
|
|
149
|
+
async function getQwenWorkUserInfo(credential, options = {}) {
|
|
150
|
+
const accessToken = credential && credential.access_token;
|
|
151
|
+
if (typeof accessToken !== 'string' || !accessToken || /[\r\n]/.test(accessToken)) {
|
|
152
|
+
throw failure('CREDENTIAL_MISSING', 'credential_missing', 'user_info');
|
|
153
|
+
}
|
|
154
|
+
const baseUrl = normalizeBaseUrl(credential.base_url);
|
|
155
|
+
const response = await requestJson('GET', `${baseUrl}/user/info`, accessToken, options, 'user_info');
|
|
156
|
+
if (response.code !== 'ok' || !isObject(response.data) ||
|
|
157
|
+
typeof response.data.user_id !== 'string' || !response.data.user_id.trim()) {
|
|
158
|
+
throw failure('INVALID_RESPONSE', 'invalid_response', 'user_info');
|
|
159
|
+
}
|
|
160
|
+
const env = options.env || process.env;
|
|
161
|
+
const decoded = decodeClaims(accessToken);
|
|
162
|
+
// Decoding is not local signature verification. The authenticated /user/info
|
|
163
|
+
// response establishes identity; these comparisons catch context drift.
|
|
164
|
+
for (const expected of [env.QWENWORK_USER_ID, env.MULE_USER_ID, decoded.user_id, decoded.sub]) {
|
|
165
|
+
if (expected && String(expected) !== response.data.user_id) {
|
|
166
|
+
throw failure('IDENTITY_MISMATCH', 'identity_mismatch', 'user_info');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const orgId = response.data.biz && response.data.biz.org_id;
|
|
170
|
+
if (orgId && decoded.biz_org_id && orgId !== decoded.biz_org_id) {
|
|
171
|
+
throw failure('IDENTITY_MISMATCH', 'identity_mismatch', 'user_info');
|
|
172
|
+
}
|
|
173
|
+
return response.data;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Safe composition for identity consumers: the returned object has no credential. */
|
|
177
|
+
async function getQwenWorkIdentity(options = {}) {
|
|
178
|
+
const credential = await exchangeQwenWorkAgentToken(options);
|
|
179
|
+
const profile = await getQwenWorkUserInfo(credential, options);
|
|
180
|
+
return {
|
|
181
|
+
base_url: credential.base_url,
|
|
182
|
+
credential_source: credential.credential_source,
|
|
183
|
+
scope: credential.scope,
|
|
184
|
+
expires_in: credential.expires_in,
|
|
185
|
+
expires_at: credential.expires_at,
|
|
186
|
+
profile,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { exchangeQwenWorkAgentToken, getQwenWorkUserInfo, getQwenWorkIdentity };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { CliError } = require('../core/cli-error');
|
|
4
|
+
const { t } = require('../core/i18n');
|
|
5
|
+
const { DINGTALK_OAUTH_CLIENT_ID } = require('../core/env-manager');
|
|
6
|
+
const { resolveTokenBaseUrl } = require('./token-auth');
|
|
7
|
+
|
|
8
|
+
function failure(code, details = {}) {
|
|
9
|
+
return new CliError(t('qwenwork.login_check_failed'), { code: `QWENWORK_CLOUD_${code}`, details });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function origin(value) {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(value);
|
|
15
|
+
if (typeof value !== 'string' || value.length > 2048 || /\s/.test(value) || url.protocol !== 'https:' || url.username || url.password || url.pathname !== '/' || url.search || url.hash) { throw new Error(); }
|
|
16
|
+
return url.origin;
|
|
17
|
+
} catch { throw failure('ENDPOINT_INVALID'); }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function exchangeSession(credential, baseUrl, options) {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timeoutMs = options.timeoutMs === undefined ? 15000 : options.timeoutMs;
|
|
23
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) { throw failure('INVALID_TIMEOUT'); }
|
|
24
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
25
|
+
try {
|
|
26
|
+
const response = await (options.fetch || globalThis.fetch)(`${baseUrl}/openapi/cli/v1/auth/qwenwork/token`, {
|
|
27
|
+
method: 'POST', redirect: 'error', signal: controller.signal,
|
|
28
|
+
headers: { authorization: `Bearer ${credential.access_token}`, accept: 'application/json', 'content-type': 'application/json' },
|
|
29
|
+
body: JSON.stringify({ gateway_url: credential.base_url, ...(options.corpId ? { corp_id: options.corpId } : {}) }),
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
await response.body?.cancel();
|
|
33
|
+
throw failure('HTTP_ERROR', { stage: 'yida_exchange', status_code: response.status });
|
|
34
|
+
}
|
|
35
|
+
const reader = response.body?.getReader();
|
|
36
|
+
if (!reader) { throw failure('RESPONSE_INVALID'); }
|
|
37
|
+
const chunks = []; let bytes = 0;
|
|
38
|
+
try {
|
|
39
|
+
for (let part = await reader.read(); !part.done; part = await reader.read()) {
|
|
40
|
+
bytes += part.value.byteLength;
|
|
41
|
+
if (bytes > 65536) { await reader.cancel(); throw failure('RESPONSE_INVALID'); }
|
|
42
|
+
chunks.push(part.value);
|
|
43
|
+
}
|
|
44
|
+
} finally { reader.releaseLock(); }
|
|
45
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
// 上游正文和异常可能回显凭据,仅保留本模块生成的固定错误码。
|
|
48
|
+
if (error instanceof CliError && /^QWENWORK_CLOUD_/.test(error.code)) { throw error; }
|
|
49
|
+
throw failure(controller.signal.aborted ? 'REQUEST_TIMED_OUT' : 'REQUEST_FAILED');
|
|
50
|
+
} finally { clearTimeout(timer); }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 只读取本次沙箱凭据,由宜搭服务端向受信任网关验证身份。 */
|
|
54
|
+
function sandboxCredential(options) {
|
|
55
|
+
const env = options.env || process.env;
|
|
56
|
+
const token = ['QWENWORK_AGENT_TOKEN', 'MULE_AGENT_TOKEN', 'AGENT_TOKEN'].map(name => env[name]).find(value => typeof value === 'string' && value);
|
|
57
|
+
if (!token) { throw new CliError(t('qwenwork.login_check_failed'), { code: 'QWENWORK_CREDENTIAL_MISSING' }); }
|
|
58
|
+
if (token.length > 4096 || /\s/.test(token)) { throw failure('CREDENTIAL_INVALID'); }
|
|
59
|
+
const gateway = env.QWENWORK_GATEWAY_URL || env.MULE_GATEWAY_URL;
|
|
60
|
+
if (!gateway) { throw new CliError(t('qwenwork.login_check_failed'), { code: 'QWENWORK_GATEWAY_MISSING' }); }
|
|
61
|
+
return { access_token: token, base_url: origin(gateway) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 只向宜搭提交 Agent Token 和网关地址;返回会话供调用方校验后保存。 */
|
|
65
|
+
async function exchangeQwenWorkCloudSession(options = {}) {
|
|
66
|
+
const baseUrl = origin(options.endpoint || (options.env || process.env).OPENYIDA_ENDPOINT || resolveTokenBaseUrl(options));
|
|
67
|
+
const credential = sandboxCredential(options);
|
|
68
|
+
try {
|
|
69
|
+
const startedAt = Date.now();
|
|
70
|
+
const value = await exchangeSession(credential, baseUrl, options);
|
|
71
|
+
if (!value || value.status !== 'ok' || value.auth_mode !== 'token' || value.can_auto_use !== true ||
|
|
72
|
+
value.token_type !== 'Bearer' || value.credential_source !== 'qwenwork_cloud' ||
|
|
73
|
+
typeof value.access_token !== 'string' || !value.access_token || /\s/.test(value.access_token) || value.access_token.length > 32768 ||
|
|
74
|
+
value.access_token === credential.access_token ||
|
|
75
|
+
['QWENWORK_AGENT_TOKEN', 'MULE_AGENT_TOKEN', 'AGENT_TOKEN'].some(name => value.access_token === (options.env || process.env)[name]) ||
|
|
76
|
+
!Number.isInteger(value.expires_in) || value.expires_in <= 0 || value.expires_in > 1800 ||
|
|
77
|
+
typeof value.corp_id !== 'string' || !/^(?:ding[0-9a-f]{32}|dingd8e1123006514592)$/.test(value.corp_id) ||
|
|
78
|
+
typeof value.user_id !== 'string' || !value.user_id || value.user_id.trim() !== value.user_id || value.user_id.length > 256 ||
|
|
79
|
+
(options.corpId && value.corp_id !== options.corpId) ||
|
|
80
|
+
(options.userId && value.user_id !== options.userId) ||
|
|
81
|
+
'refresh_token' in value || 'refreshToken' in value || 'exchange_ticket' in value) { throw failure('RESPONSE_INVALID'); }
|
|
82
|
+
return {
|
|
83
|
+
auth_mode: 'token', credential_source: 'qwenwork_cloud', token_type: 'Bearer',
|
|
84
|
+
// 可信宜搭签发服务决定业务域名;认证请求仍只发往最初选择的 origin。
|
|
85
|
+
access_token: value.access_token, base_url: value.base_url === undefined ? baseUrl : origin(value.base_url),
|
|
86
|
+
client_id: options.clientId || DINGTALK_OAUTH_CLIENT_ID,
|
|
87
|
+
corp_id: value.corp_id, user_id: value.user_id,
|
|
88
|
+
expires_at: startedAt + value.expires_in * 1000,
|
|
89
|
+
};
|
|
90
|
+
} finally { credential.access_token = undefined; }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { exchangeQwenWorkCloudSession };
|