openyida 2026.10.29-beta.2 → 2026.10.29-beta.4
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 +4 -0
- package/lib/auth/org.js +14 -3
- package/lib/auth/qwenwork-cloud.js +60 -24
- package/lib/auth/qwenwork-desktop.js +5 -2
- package/lib/auth/qwenwork-login.js +10 -6
- package/lib/auth/session-identity.js +23 -0
- package/lib/auth/token-auth.js +11 -2
- package/lib/auth/token-store.js +3 -0
- package/lib/connector/connector-delete.js +1 -1
- package/lib/connector/connector-list-connections.js +1 -1
- package/lib/core/doctor.js +2 -2
- package/lib/core/env-manager.js +4 -4
- package/lib/core/locales/en.js +1 -0
- package/lib/core/locales/zh.js +1 -0
- package/lib/core/utils.js +9 -5
- package/lib/corp-efficiency/corp-efficiency.js +3 -3
- package/lib/feedback/feedback.js +1 -1
- package/package.json +6 -18
- package/project/config.json +2 -2
- package/scripts/install-qwenwork-pre-beta.js +0 -40
package/README.md
CHANGED
|
@@ -145,12 +145,16 @@ openyida auth status
|
|
|
145
145
|
|
|
146
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
147
|
|
|
148
|
+
QwenWork login supports DingTalk organizations, QwenWork organizations and personal ODS identities. Local ODS tenant IDs retain their `yida` prefix. When the server supplies a refresh token, the CLI stores it for the standard refresh flow. Optional server fields `identity_type` (`PERSONAL` / `ORGANIZATION`) and `organization_type` (`DINGTALK` / `QWENWORK` / `OTHER`, organizations only) are preserved in the session and `auth status`; historical sessions with both fields absent, null or empty default to `ORGANIZATION` / `DINGTALK`. Authentication failures expose bounded `status`, `stage`, `exchange_id`, HTTP `status_code` and local validation `reason` in JSON diagnostics, without response bodies or credentials.
|
|
149
|
+
|
|
148
150
|
For QwenWork cloud identity diagnostics, run inside the actual cloud execution environment:
|
|
149
151
|
|
|
150
152
|
```bash
|
|
151
153
|
openyida auth qwenwork user-info --json
|
|
152
154
|
```
|
|
153
155
|
|
|
156
|
+
This diagnostic command uses the legacy user-token protocol; it does not validate the current web Agent Token login flow. Use `openyida login --check-only --json` to check the YiDA session.
|
|
157
|
+
|
|
154
158
|
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
159
|
|
|
156
160
|
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.
|
package/lib/auth/org.js
CHANGED
|
@@ -19,6 +19,10 @@ const {
|
|
|
19
19
|
saveTokenSession,
|
|
20
20
|
} = require('./token-store');
|
|
21
21
|
const { tokenLogin } = require('./token-auth');
|
|
22
|
+
const { sessionIdentity } = require('./session-identity');
|
|
23
|
+
const { detectRuntimeCapabilities } = require('../core/utils');
|
|
24
|
+
const { isQwenWorkWebRuntime } = require('../core/qwenwork-login-guidance');
|
|
25
|
+
const { t } = require('../core/i18n');
|
|
22
26
|
|
|
23
27
|
const LIST_CORP_INFOS_PATH = '/query/userservice/listCorpInfos.json';
|
|
24
28
|
|
|
@@ -146,6 +150,7 @@ function buildSwitchResult(status, currentSession, nextSession, message, extra =
|
|
|
146
150
|
auth_mode: 'token',
|
|
147
151
|
status,
|
|
148
152
|
can_auto_use: true,
|
|
153
|
+
...sessionIdentity(nextSession),
|
|
149
154
|
previous_corp_id: currentSession && currentSession.corp_id,
|
|
150
155
|
corp_id: nextSession.corp_id,
|
|
151
156
|
corp_name: nextSession.corp_name,
|
|
@@ -191,6 +196,7 @@ async function switchOrganization(targetCorpId, options = {}) {
|
|
|
191
196
|
ok: true,
|
|
192
197
|
auth_mode: 'token',
|
|
193
198
|
status: 'already_in_org',
|
|
199
|
+
...sessionIdentity(currentSession),
|
|
194
200
|
can_auto_use: true,
|
|
195
201
|
corp_id: currentSession.corp_id,
|
|
196
202
|
corp_name: currentSession.corp_name,
|
|
@@ -231,7 +237,12 @@ async function switchOrganization(targetCorpId, options = {}) {
|
|
|
231
237
|
corpId: targetCorpId,
|
|
232
238
|
};
|
|
233
239
|
|
|
234
|
-
const
|
|
240
|
+
const cloud = isQwenWorkWebRuntime(options.runtime || detectRuntimeCapabilities({ env: options.env || process.env }));
|
|
241
|
+
const login = options.tokenLogin || (cloud ? async opts => {
|
|
242
|
+
const { ensureQwenWorkLogin } = require('./qwenwork-login');
|
|
243
|
+
await ensureQwenWorkLogin(opts);
|
|
244
|
+
return loadTokenSession(opts);
|
|
245
|
+
} : tokenLogin);
|
|
235
246
|
const temporaryLogin = createTemporaryLoginOptions(loginOptions);
|
|
236
247
|
let loginResult;
|
|
237
248
|
try {
|
|
@@ -266,8 +277,8 @@ async function switchOrganization(targetCorpId, options = {}) {
|
|
|
266
277
|
'switched',
|
|
267
278
|
currentSession,
|
|
268
279
|
savedSession,
|
|
269
|
-
'switched organization by OAuth login',
|
|
270
|
-
{ switch_source: 'oauth_login' }
|
|
280
|
+
cloud ? t('qwenwork.organization_switch_success') : 'switched organization by OAuth login',
|
|
281
|
+
{ switch_source: cloud ? 'qwenwork_cloud' : 'oauth_login' }
|
|
271
282
|
);
|
|
272
283
|
}
|
|
273
284
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { sessionIdentity } = require('./session-identity');
|
|
4
|
+
|
|
3
5
|
const { CliError } = require('../core/cli-error');
|
|
4
6
|
const { t } = require('../core/i18n');
|
|
5
7
|
const { DINGTALK_OAUTH_CLIENT_ID } = require('../core/env-manager');
|
|
@@ -9,12 +11,30 @@ function failure(code, details = {}) {
|
|
|
9
11
|
return new CliError(t('qwenwork.login_check_failed'), { code: `QWENWORK_CLOUD_${code}`, details });
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
const ODS_CORP_ID = /^yida[0-9a-f]{32}$/;
|
|
15
|
+
const CORP_ID = /^(?:(?:ding|yida)[0-9a-f]{32}|dingd8e1123006514592)$/;
|
|
16
|
+
|
|
17
|
+
// Only bounded protocol identifiers are diagnostic data; never forward the response body or message.
|
|
18
|
+
function diagnostics(value, statusCode, credential = {}) {
|
|
19
|
+
const details = { stage: 'yida_exchange', status_code: statusCode };
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) { return details; }
|
|
21
|
+
const secrets = [credential.access_token, value.access_token, value.refresh_token, value.refreshToken].filter(token => typeof token === 'string' && token);
|
|
22
|
+
const publicValue = text => typeof text === 'string' && !secrets.some(token => text.includes(token));
|
|
23
|
+
if (publicValue(value.status) && value.status.length <= 64 && /^(?:ok|error|failure|[a-z][a-z0-9]*_[a-z0-9_]+)$/.test(value.status)
|
|
24
|
+
&& !/^(?:qwex|dws)_/.test(value.status)) { details.status = value.status; }
|
|
25
|
+
if (publicValue(value.stage) && ['request', 'qwen_verify', 'token_issue'].includes(value.stage)) { details.stage = value.stage; }
|
|
26
|
+
if (publicValue(value.exchange_id) && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value.exchange_id)) {
|
|
27
|
+
details.exchange_id = value.exchange_id;
|
|
28
|
+
}
|
|
29
|
+
return details;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function origin(value, details = {}) {
|
|
13
33
|
try {
|
|
14
34
|
const url = new URL(value);
|
|
15
35
|
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
36
|
return url.origin;
|
|
17
|
-
} catch { throw failure('ENDPOINT_INVALID'); }
|
|
37
|
+
} catch { throw failure('ENDPOINT_INVALID', details); }
|
|
18
38
|
}
|
|
19
39
|
|
|
20
40
|
async function exchangeSession(credential, baseUrl, options) {
|
|
@@ -26,27 +46,29 @@ async function exchangeSession(credential, baseUrl, options) {
|
|
|
26
46
|
const response = await (options.fetch || globalThis.fetch)(`${baseUrl}/openapi/cli/v1/auth/qwenwork/token`, {
|
|
27
47
|
method: 'POST', redirect: 'error', signal: controller.signal,
|
|
28
48
|
headers: { authorization: `Bearer ${credential.access_token}`, accept: 'application/json', 'content-type': 'application/json' },
|
|
29
|
-
|
|
49
|
+
// corp_id selects a DingTalk organization upstream; ODS identity is checked locally after exchange.
|
|
50
|
+
body: JSON.stringify({ gateway_url: credential.base_url, ...(options.corpId && !ODS_CORP_ID.test(options.corpId) ? { corp_id: options.corpId } : {}) }),
|
|
30
51
|
});
|
|
31
|
-
if (!response.ok) {
|
|
32
|
-
await response.body?.cancel();
|
|
33
|
-
throw failure('HTTP_ERROR', { stage: 'yida_exchange', status_code: response.status });
|
|
34
|
-
}
|
|
35
52
|
const reader = response.body?.getReader();
|
|
36
|
-
if (!reader) { throw failure('RESPONSE_INVALID'); }
|
|
53
|
+
if (!reader) { throw failure(response.ok ? 'RESPONSE_INVALID' : 'HTTP_ERROR', diagnostics(null, response.status)); }
|
|
37
54
|
const chunks = []; let bytes = 0;
|
|
38
55
|
try {
|
|
39
56
|
for (let part = await reader.read(); !part.done; part = await reader.read()) {
|
|
40
57
|
bytes += part.value.byteLength;
|
|
41
|
-
if (bytes > 65536) { await reader.cancel(); throw failure('RESPONSE_INVALID'); }
|
|
58
|
+
if (bytes > 65536) { await reader.cancel(); throw failure(response.ok ? 'RESPONSE_INVALID' : 'HTTP_ERROR', { ...diagnostics(null, response.status), reason: 'response_too_large' }); }
|
|
42
59
|
chunks.push(part.value);
|
|
43
60
|
}
|
|
44
61
|
} finally { reader.releaseLock(); }
|
|
45
|
-
|
|
62
|
+
let value;
|
|
63
|
+
try { value = JSON.parse(Buffer.concat(chunks).toString('utf8')); }
|
|
64
|
+
catch { throw failure(response.ok ? 'RESPONSE_INVALID' : 'HTTP_ERROR', { ...diagnostics(null, response.status), reason: 'invalid_json' }); }
|
|
65
|
+
const details = diagnostics(value, response.status, credential);
|
|
66
|
+
if (!response.ok) { throw failure('HTTP_ERROR', details); }
|
|
67
|
+
return { value, details };
|
|
46
68
|
} catch (error) {
|
|
47
69
|
// 上游正文和异常可能回显凭据,仅保留本模块生成的固定错误码。
|
|
48
70
|
if (error instanceof CliError && /^QWENWORK_CLOUD_/.test(error.code)) { throw error; }
|
|
49
|
-
throw failure(controller.signal.aborted ? 'REQUEST_TIMED_OUT' : 'REQUEST_FAILED');
|
|
71
|
+
throw failure(controller.signal.aborted ? 'REQUEST_TIMED_OUT' : 'REQUEST_FAILED', { stage: 'yida_exchange' });
|
|
50
72
|
} finally { clearTimeout(timer); }
|
|
51
73
|
}
|
|
52
74
|
|
|
@@ -67,24 +89,38 @@ async function exchangeQwenWorkCloudSession(options = {}) {
|
|
|
67
89
|
const credential = sandboxCredential(options);
|
|
68
90
|
try {
|
|
69
91
|
const startedAt = Date.now();
|
|
70
|
-
const value = await exchangeSession(credential, baseUrl, options);
|
|
71
|
-
if (!value || value
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
const { value, details } = await exchangeSession(credential, baseUrl, options);
|
|
93
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) { throw failure('RESPONSE_INVALID', { ...details, reason: 'response_shape' }); }
|
|
94
|
+
if (value.status !== 'ok') { throw failure('EXCHANGE_REJECTED', details); }
|
|
95
|
+
const env = options.env || process.env;
|
|
96
|
+
const hostTokens = ['QWENWORK_AGENT_TOKEN', 'MULE_AGENT_TOKEN', 'AGENT_TOKEN'].map(name => env[name]).filter(Boolean);
|
|
97
|
+
const validToken = token => typeof token === 'string' && !!token && !/\s/.test(token) && token.length <= 32768 && !hostTokens.includes(token);
|
|
98
|
+
// Older cloud deployments may return access-only sessions. Preserve a refresh token when supplied.
|
|
99
|
+
const refreshToken = value.refresh_token === undefined ? value.refreshToken : value.refresh_token;
|
|
100
|
+
const invalid = [
|
|
101
|
+
[value.auth_mode !== 'token', 'auth_mode'],
|
|
102
|
+
[value.can_auto_use !== true, 'can_auto_use'],
|
|
103
|
+
[value.token_type !== 'Bearer', 'token_type'],
|
|
104
|
+
[value.credential_source !== 'qwenwork_cloud', 'credential_source'],
|
|
105
|
+
[!validToken(value.access_token), 'access_token'],
|
|
106
|
+
[!Number.isInteger(value.expires_in) || value.expires_in <= 0 || value.expires_in > 1800, 'expires_in'],
|
|
107
|
+
[typeof value.corp_id !== 'string' || !CORP_ID.test(value.corp_id), 'corp_id'],
|
|
108
|
+
[typeof value.user_id !== 'string' || !value.user_id || value.user_id.trim() !== value.user_id || value.user_id.length > 256, 'user_id'],
|
|
109
|
+
[options.corpId && value.corp_id !== options.corpId, 'corp_id_mismatch'],
|
|
110
|
+
[options.userId && value.user_id !== options.userId, 'user_id_mismatch'],
|
|
111
|
+
['exchange_ticket' in value, 'exchange_ticket'],
|
|
112
|
+
[refreshToken !== undefined && !validToken(refreshToken), 'refresh_token'],
|
|
113
|
+
[value.client_id !== undefined && value.client_id !== (options.clientId || DINGTALK_OAUTH_CLIENT_ID), 'client_id'],
|
|
114
|
+
].find(([rejected]) => rejected);
|
|
115
|
+
if (invalid) { throw failure('RESPONSE_INVALID', { ...details, reason: invalid[1] }); }
|
|
82
116
|
return {
|
|
117
|
+
...sessionIdentity(value),
|
|
83
118
|
auth_mode: 'token', credential_source: 'qwenwork_cloud', token_type: 'Bearer',
|
|
84
119
|
// 可信宜搭签发服务决定业务域名;认证请求仍只发往最初选择的 origin。
|
|
85
|
-
access_token: value.access_token, base_url: value.base_url === undefined ? baseUrl : origin(value.base_url),
|
|
120
|
+
access_token: value.access_token, refresh_token: refreshToken, base_url: value.base_url === undefined ? baseUrl : origin(value.base_url, { ...details, reason: 'base_url' }),
|
|
86
121
|
client_id: options.clientId || DINGTALK_OAUTH_CLIENT_ID,
|
|
87
122
|
corp_id: value.corp_id, user_id: value.user_id,
|
|
123
|
+
user_name: typeof value.user_name === 'string' ? value.user_name : undefined,
|
|
88
124
|
expires_at: startedAt + value.expires_in * 1000,
|
|
89
125
|
};
|
|
90
126
|
} finally { credential.access_token = undefined; }
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { sessionIdentity } = require('./session-identity');
|
|
4
|
+
|
|
3
5
|
const { CliError } = require('../core/cli-error');
|
|
4
6
|
const { buildQwenWorkLoginInvocation } = require('../core/qwenwork-login-guidance');
|
|
5
7
|
const { t } = require('../core/i18n');
|
|
@@ -114,6 +116,7 @@ async function loginQwenWork(options = {}) {
|
|
|
114
116
|
if (verified.status !== 'ok' || verified.auth_mode !== 'token' || verified.can_auto_use !== true ||
|
|
115
117
|
verified.corp_id !== value.corp_id || verified.user_id !== value.user_id || expiresAt <= Date.now()) { throw fail('IDENTITY_MISMATCH'); }
|
|
116
118
|
const session = saveTokenSession({
|
|
119
|
+
...sessionIdentity(value),
|
|
117
120
|
auth_mode: 'token', token_type: 'Bearer', access_token: value.access_token, refresh_token: value.refresh_token,
|
|
118
121
|
expires_at: expiresAt, base_url: businessBaseUrl, client_id: DINGTALK_OAUTH_CLIENT_ID,
|
|
119
122
|
corp_id: value.corp_id, user_id: value.user_id, credential_source: 'qwenwork_desktop',
|
|
@@ -122,8 +125,8 @@ async function loginQwenWork(options = {}) {
|
|
|
122
125
|
if (!['user', 'project'].includes(session.persistence_scope) || !restored ||
|
|
123
126
|
restored.access_token !== value.access_token || restored.refresh_token !== value.refresh_token || restored.base_url !== businessBaseUrl ||
|
|
124
127
|
restored.corp_id !== value.corp_id || restored.user_id !== value.user_id) { throw fail('PERSISTENCE_FAILED'); }
|
|
125
|
-
//
|
|
126
|
-
return { ok: true, status: 'ok', can_auto_use: true };
|
|
128
|
+
// 只返回分类和登录结果;组织/用户标识、环境和存储细节不进入模型可见结果。
|
|
129
|
+
return { ok: true, status: 'ok', can_auto_use: true, ...sessionIdentity(session) };
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
async function run(args, options = {}) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const { t } = require('../core/i18n');
|
|
3
|
+
const { sessionIdentity } = require('./session-identity');
|
|
3
4
|
const { CliError } = require('../core/cli-error');
|
|
4
5
|
const { loadTokenSession, resolveTokenSession, isEnvTokenAuthMode, hasEnvTokenCredential, saveTokenSession } = require('./token-store');
|
|
5
6
|
const { detectRuntimeCapabilities } = require('../core/utils');
|
|
@@ -10,7 +11,7 @@ const { tokenLogin, tokenRefresh, isRefreshAuthRequired } = require('./token-aut
|
|
|
10
11
|
function failure(code) {
|
|
11
12
|
return new CliError(t('qwenwork.login_check_failed'), { code });
|
|
12
13
|
}
|
|
13
|
-
function summary(ok) { return { ok, status: ok ? 'ok' : 'not_logged_in', can_auto_use: ok }; }
|
|
14
|
+
function summary(ok, session = {}) { return { ok, status: ok ? 'ok' : 'not_logged_in', can_auto_use: ok, ...(ok ? sessionIdentity(session) : {}) }; }
|
|
14
15
|
|
|
15
16
|
/** 以服务端状态为准;网络异常不能触发浏览器登录。 */
|
|
16
17
|
async function verify(session, options = {}) {
|
|
@@ -53,7 +54,10 @@ async function ensureQwenWorkLogin(options = {}, dependencies = {}) {
|
|
|
53
54
|
const env = options.env || process.env;
|
|
54
55
|
const runtime = options.runtime || detectRuntimeCapabilities({ env });
|
|
55
56
|
if (isQwenWorkWebRuntime(runtime) && !isEnvTokenAuthMode(env) && !hasEnvTokenCredential(env)) {
|
|
56
|
-
if (options.checkOnly) {
|
|
57
|
+
if (options.checkOnly) {
|
|
58
|
+
const session = loadTokenSession(options);
|
|
59
|
+
return summary(await verify(session, options), session || {});
|
|
60
|
+
}
|
|
57
61
|
const selectedProfile = options.authProfile || options.profile || env.OPENYIDA_AUTH_PROFILE;
|
|
58
62
|
const selectedSession = selectedProfile ? loadTokenSession(options) : null;
|
|
59
63
|
if (selectedProfile && !selectedSession) { throw failure('AUTH_PROFILE_REQUIRED'); }
|
|
@@ -66,7 +70,7 @@ async function ensureQwenWorkLogin(options = {}, dependencies = {}) {
|
|
|
66
70
|
const session = await exchangeQwenWorkCloudSession(cloudOptions);
|
|
67
71
|
if (!await verify(session, options)) { throw failure('QWENWORK_LOGIN_CHECK_FAILED'); }
|
|
68
72
|
saveTokenSession(session, options);
|
|
69
|
-
return summary(true);
|
|
73
|
+
return summary(true, session);
|
|
70
74
|
}
|
|
71
75
|
const resolution = resolveTokenSession(options);
|
|
72
76
|
if (resolution.status === 'profile_required') { throw failure('AUTH_PROFILE_REQUIRED'); }
|
|
@@ -74,12 +78,12 @@ async function ensureQwenWorkLogin(options = {}, dependencies = {}) {
|
|
|
74
78
|
if (session && options.endpoint && new URL(session.base_url).origin !== new URL(options.endpoint).origin) {
|
|
75
79
|
session = null;
|
|
76
80
|
}
|
|
77
|
-
if (await verify(session, options)) { return summary(true); }
|
|
81
|
+
if (await verify(session, options)) { return summary(true, session); }
|
|
78
82
|
if (session?.refresh_token) {
|
|
79
83
|
try {
|
|
80
84
|
const refreshed = await (dependencies.refresh || tokenRefresh)(options);
|
|
81
85
|
if (refreshed && refreshed.ok !== false && refreshed.access_token) {
|
|
82
|
-
if (await verify(refreshed, options)) { return summary(true); }
|
|
86
|
+
if (await verify(refreshed, options)) { return summary(true, refreshed); }
|
|
83
87
|
} else if (!isRefreshAuthRequired(refreshed)) { throw failure('QWENWORK_LOGIN_REFRESH_FAILED'); }
|
|
84
88
|
} catch (error) {
|
|
85
89
|
if (!isRefreshAuthRequired(error) && ![401, 403].includes(error.statusCode)) {
|
|
@@ -94,7 +98,7 @@ async function ensureQwenWorkLogin(options = {}, dependencies = {}) {
|
|
|
94
98
|
if (!result || result.ok === false || !result.access_token || !await verify(result, options)) {
|
|
95
99
|
throw failure('QWENWORK_LOGIN_FAILED');
|
|
96
100
|
}
|
|
97
|
-
return summary(true);
|
|
101
|
+
return summary(true, result);
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
async function run(args, options = {}) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { CliError } = require('../core/cli-error');
|
|
4
|
+
const { t } = require('../core/i18n');
|
|
5
|
+
|
|
6
|
+
/** Explicit server classification wins; historical sessions without either field default to DingTalk organizations. */
|
|
7
|
+
function sessionIdentity(value = {}) {
|
|
8
|
+
const empty = field => field === undefined || field === null || field === '';
|
|
9
|
+
const identity = empty(value.identity_type) ? undefined : value.identity_type;
|
|
10
|
+
const organization = empty(value.organization_type) ? undefined : value.organization_type;
|
|
11
|
+
if (identity === undefined && organization === undefined) {
|
|
12
|
+
return { identity_type: 'ORGANIZATION', organization_type: 'DINGTALK' };
|
|
13
|
+
}
|
|
14
|
+
if ((identity === 'PERSONAL' && organization === undefined) ||
|
|
15
|
+
(identity === 'ORGANIZATION' && ['DINGTALK', 'QWENWORK', 'OTHER'].includes(organization))) {
|
|
16
|
+
return { identity_type: identity, ...(organization === undefined ? {} : { organization_type: organization }) };
|
|
17
|
+
}
|
|
18
|
+
throw new CliError(t('qwenwork.login_check_failed'), {
|
|
19
|
+
code: 'AUTH_IDENTITY_TYPE_INVALID', details: { reason: 'identity_classification' },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { sessionIdentity };
|
package/lib/auth/token-auth.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { sessionIdentity } = require('./session-identity');
|
|
4
|
+
|
|
3
5
|
const http = require('http');
|
|
4
6
|
const https = require('https');
|
|
5
7
|
|
|
@@ -174,6 +176,7 @@ function normalizeTokenResponse(response, baseUrl, clientId) {
|
|
|
174
176
|
baseUrl
|
|
175
177
|
);
|
|
176
178
|
return {
|
|
179
|
+
...sessionIdentity(data),
|
|
177
180
|
auth_mode: 'token',
|
|
178
181
|
credential_source: data.credential_source,
|
|
179
182
|
status: data.status || 'ok',
|
|
@@ -445,6 +448,7 @@ function tokenStatus(options = {}) {
|
|
|
445
448
|
refresh_token: maskToken(session.refresh_token),
|
|
446
449
|
expires_at: session.expires_at ? new Date(session.expires_at).toISOString() : undefined,
|
|
447
450
|
base_url: session.base_url,
|
|
451
|
+
...sessionIdentity(session),
|
|
448
452
|
corp_id: session.corp_id,
|
|
449
453
|
corp_name: session.corp_name,
|
|
450
454
|
user_id: session.user_id,
|
|
@@ -510,11 +514,16 @@ async function tokenRefreshUnlocked(options, session) {
|
|
|
510
514
|
} catch { throw createTokenAuthRequiredError('refresh_origin_mismatch'); }
|
|
511
515
|
}
|
|
512
516
|
const businessBaseUrl = session.base_url || authBaseUrlValue;
|
|
517
|
+
const corpId = data.corp_id || data.corpId || session.corp_id;
|
|
518
|
+
const userId = data.user_id || data.userId || session.user_id;
|
|
519
|
+
const inheritClassification = [data.identity_type, data.organization_type].every(value => value === undefined || value === null || value === '')
|
|
520
|
+
&& corpId === session.corp_id && userId === session.user_id;
|
|
513
521
|
const normalized = normalizeTokenResponse({
|
|
514
522
|
...data,
|
|
515
|
-
|
|
523
|
+
...(inheritClassification ? sessionIdentity(session) : {}),
|
|
524
|
+
corp_id: corpId,
|
|
516
525
|
corp_name: data.corp_name || data.corpName || data.name || session.corp_name,
|
|
517
|
-
user_id:
|
|
526
|
+
user_id: userId,
|
|
518
527
|
user_name: data.user_name || data.userName || session.user_name,
|
|
519
528
|
refresh_token: data.refresh_token || data.refreshToken || session.refresh_token,
|
|
520
529
|
credential_source: data.credential_source || session.credential_source,
|
package/lib/auth/token-store.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { sessionIdentity } = require('./session-identity');
|
|
4
|
+
|
|
3
5
|
const fs = require('fs');
|
|
4
6
|
const path = require('path');
|
|
5
7
|
const os = require('os');
|
|
@@ -275,6 +277,7 @@ function normalizeTokenSession(session = {}) {
|
|
|
275
277
|
session.tokenEndpoint
|
|
276
278
|
);
|
|
277
279
|
const normalized = {
|
|
280
|
+
...sessionIdentity(session),
|
|
278
281
|
auth_mode: 'token',
|
|
279
282
|
credential_source: session.credential_source,
|
|
280
283
|
token_type: session.token_type || session.tokenType || 'Bearer',
|
|
@@ -52,7 +52,7 @@ async function run(args) {
|
|
|
52
52
|
console.log(`\n连接器: ${connector.displayName}`);
|
|
53
53
|
console.log('\n⚠️ CLI 未执行删除操作:当前无法确定性证明该连接器未被表单、页面、流程或集成自动化引用。');
|
|
54
54
|
console.log('请先确认并解除全部依赖,再前往宜搭平台管理后台手工删除:');
|
|
55
|
-
console.log(' https://
|
|
55
|
+
console.log(' https://davidtest.aliwork.com/platformManage/customConnectorFactory');
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
module.exports = { run };
|
|
@@ -52,7 +52,7 @@ async function run(args) {
|
|
|
52
52
|
}
|
|
53
53
|
console.log('📭 暂无鉴权账号');
|
|
54
54
|
console.log('\n💡 提示: 需要在宜搭平台管理后台创建鉴权账号');
|
|
55
|
-
console.log(' https://
|
|
55
|
+
console.log(' https://davidtest.aliwork.com/platformManage/customConnectorFactory');
|
|
56
56
|
return result;
|
|
57
57
|
}
|
|
58
58
|
|
package/lib/core/doctor.js
CHANGED
|
@@ -695,8 +695,8 @@ class FixEngine {
|
|
|
695
695
|
case 'create-config': {
|
|
696
696
|
const configPath = path.join(this.projectRoot, 'config.json');
|
|
697
697
|
const template = {
|
|
698
|
-
loginUrl: 'https://
|
|
699
|
-
defaultBaseUrl: 'https://
|
|
698
|
+
loginUrl: 'https://davidtest.aliwork.com/workPlatform',
|
|
699
|
+
defaultBaseUrl: 'https://davidtest.aliwork.com',
|
|
700
700
|
};
|
|
701
701
|
fs.writeFileSync(configPath, JSON.stringify(template, null, 2), 'utf-8');
|
|
702
702
|
return {
|
package/lib/core/env-manager.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 2. authData.base_url(登录后实际跳转域名)
|
|
10
10
|
* 3. 环境变量 OPENYIDA_ENV 指定的环境配置
|
|
11
11
|
* 4. 当前激活的环境配置(openyida-envs.json current 字段)
|
|
12
|
-
* 5. 默认公有云 https://
|
|
12
|
+
* 5. 默认公有云 https://davidtest.aliwork.com
|
|
13
13
|
*
|
|
14
14
|
* 导出函数:
|
|
15
15
|
* loadEnvsConfig() - 读取环境配置文件(不存在则返回默认公有云配置)
|
|
@@ -25,8 +25,8 @@ const fs = require('fs');
|
|
|
25
25
|
const path = require('path');
|
|
26
26
|
const { findProjectRoot } = require('./utils');
|
|
27
27
|
|
|
28
|
-
const DEFAULT_BASE_URL = 'https://
|
|
29
|
-
const DEFAULT_LOGIN_URL = 'https://
|
|
28
|
+
const DEFAULT_BASE_URL = 'https://davidtest.aliwork.com';
|
|
29
|
+
const DEFAULT_LOGIN_URL = 'https://davidtest.aliwork.com/workPlatform';
|
|
30
30
|
const INTERNATIONAL_BASE_URL = 'https://www.yidaapps.com';
|
|
31
31
|
const DINGTALK_OAUTH_CLIENT_ID = 'suite9xvlxxerybljwheo';
|
|
32
32
|
const DINGTALK_LOGIN_ORIGIN = 'https://login.dingtalk.com';
|
|
@@ -151,7 +151,7 @@ const ENV_ALIASES = {
|
|
|
151
151
|
};
|
|
152
152
|
|
|
153
153
|
const KNOWN_YIDA_HOSTS = new Set([
|
|
154
|
-
'
|
|
154
|
+
'davidtest.aliwork.com',
|
|
155
155
|
'www.yidaapps.com',
|
|
156
156
|
'yida-group.alibaba-inc.com',
|
|
157
157
|
'www.yidaapps.com',
|
package/lib/core/locales/en.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
module.exports = {
|
|
7
7
|
qwenwork: {
|
|
8
|
+
organization_switch_success: 'Switched organization through QwenWork login.',
|
|
8
9
|
login_check_failed: 'Unable to verify the login session. Please retry.',
|
|
9
10
|
desktop_login_failed: 'QwenWork desktop login failed. Check the desktop identity and login handoff before retrying.',
|
|
10
11
|
desktop_login_usage: 'Usage: openyida login --qwenwork [--json] [--check-only]',
|
package/lib/core/locales/zh.js
CHANGED
package/lib/core/utils.js
CHANGED
|
@@ -412,7 +412,7 @@ function isTokenAuthMode(_env = process.env) {
|
|
|
412
412
|
}
|
|
413
413
|
|
|
414
414
|
function normalizeEnvBaseUrl(baseUrl, fallbackBaseUrl) {
|
|
415
|
-
const raw = String(baseUrl || fallbackBaseUrl || 'https://
|
|
415
|
+
const raw = String(baseUrl || fallbackBaseUrl || 'https://davidtest.aliwork.com').trim();
|
|
416
416
|
try {
|
|
417
417
|
const parsed = new URL(raw);
|
|
418
418
|
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
@@ -829,11 +829,13 @@ function resolveTokenBaseUrl(root, fallbackBaseUrl, session) {
|
|
|
829
829
|
function loadTokenAuthData(projectRoot, defaultBaseUrl) {
|
|
830
830
|
try {
|
|
831
831
|
const { loadTokenSession } = require('../auth/token-store');
|
|
832
|
+
const { sessionIdentity } = require('../auth/session-identity');
|
|
832
833
|
const session = loadTokenSession({ projectRoot });
|
|
833
834
|
if (!session || (!session.access_token && !session.refresh_token)) {
|
|
834
835
|
return null;
|
|
835
836
|
}
|
|
836
837
|
return {
|
|
838
|
+
...sessionIdentity(session),
|
|
837
839
|
corp_id: session.corp_id,
|
|
838
840
|
corp_name: session.corp_name,
|
|
839
841
|
user_id: session.user_id,
|
|
@@ -916,7 +918,7 @@ function loadAuthData(projectRoot, defaultBaseUrl) {
|
|
|
916
918
|
};
|
|
917
919
|
}
|
|
918
920
|
const root = projectRoot || findProjectRoot();
|
|
919
|
-
const fallbackBaseUrl = defaultBaseUrl || 'https://
|
|
921
|
+
const fallbackBaseUrl = defaultBaseUrl || 'https://davidtest.aliwork.com';
|
|
920
922
|
return loadTokenAuthData(root, fallbackBaseUrl);
|
|
921
923
|
}
|
|
922
924
|
|
|
@@ -937,7 +939,7 @@ function triggerLogin(options = {}) {
|
|
|
937
939
|
if (isEnvAuthMode()) {
|
|
938
940
|
const status = getAuthStatus({ projectRoot: options.projectRoot || findProjectRoot() });
|
|
939
941
|
if (status && status.ok) {
|
|
940
|
-
return loadTokenAuthData(options.projectRoot || findProjectRoot(), status.base_url || 'https://
|
|
942
|
+
return loadTokenAuthData(options.projectRoot || findProjectRoot(), status.base_url || 'https://davidtest.aliwork.com');
|
|
941
943
|
}
|
|
942
944
|
const reason = status && status.failure_reason
|
|
943
945
|
? status.failure_reason
|
|
@@ -956,7 +958,7 @@ function triggerLogin(options = {}) {
|
|
|
956
958
|
);
|
|
957
959
|
}
|
|
958
960
|
|
|
959
|
-
const tokenAuthData = loadTokenAuthData(options.projectRoot || findProjectRoot(), 'https://
|
|
961
|
+
const tokenAuthData = loadTokenAuthData(options.projectRoot || findProjectRoot(), 'https://davidtest.aliwork.com');
|
|
960
962
|
if (tokenAuthData) {
|
|
961
963
|
return tokenAuthData;
|
|
962
964
|
}
|
|
@@ -1072,7 +1074,7 @@ function resolveBaseUrl(authData, defaultBaseUrl) {
|
|
|
1072
1074
|
return businessBaseUrl;
|
|
1073
1075
|
}
|
|
1074
1076
|
const resolved = resolveEndpoint(null, undefined);
|
|
1075
|
-
if (defaultBaseUrl && resolved === 'https://
|
|
1077
|
+
if (defaultBaseUrl && resolved === 'https://davidtest.aliwork.com' && (!authData || !authData.base_url)) {
|
|
1076
1078
|
return defaultBaseUrl.replace(/\/+$/, '');
|
|
1077
1079
|
}
|
|
1078
1080
|
return resolved;
|
|
@@ -1884,7 +1886,9 @@ function normalizeRefreshedTokenAuthData(refreshResult, fallbackBaseUrl) {
|
|
|
1884
1886
|
if (!refreshResult || !refreshResult.access_token) {
|
|
1885
1887
|
return null;
|
|
1886
1888
|
}
|
|
1889
|
+
const { sessionIdentity } = require('../auth/session-identity');
|
|
1887
1890
|
return {
|
|
1891
|
+
...sessionIdentity(refreshResult),
|
|
1888
1892
|
corp_id: refreshResult.corp_id,
|
|
1889
1893
|
corp_name: refreshResult.corp_name,
|
|
1890
1894
|
user_id: refreshResult.user_id,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* corp-efficiency.js - 平台管理 / 企业效能
|
|
5
5
|
*
|
|
6
|
-
* 对应页面:https://
|
|
6
|
+
* 对应页面:https://davidtest.aliwork.com/platformManage/corpEfficiency
|
|
7
7
|
*
|
|
8
8
|
* 用法:
|
|
9
9
|
* openyida corp-efficiency [overview] [--locale zh_CN|en_US] [--raw]
|
|
@@ -28,7 +28,7 @@ const API = {
|
|
|
28
28
|
|
|
29
29
|
const NOTIFY_TYPES = new Set(['noticeStudy', 'noticeCertify', 'completeStudy']);
|
|
30
30
|
const DETAIL_DOC_URL = 'https://docs.aliwork.com/docs/yida_support/ifpp7g/wow0da';
|
|
31
|
-
const DETAIL_CONSULT_URL = 'https://
|
|
31
|
+
const DETAIL_CONSULT_URL = 'https://davidtest.aliwork.com/hybrid_home';
|
|
32
32
|
const API_MESSAGES = {
|
|
33
33
|
loginMessage: '登录态已失效,请重新登录',
|
|
34
34
|
};
|
|
@@ -143,7 +143,7 @@ function parseDetailReportInfo(detailReportUrl, baseUrl) {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
try {
|
|
146
|
-
const url = new URL(detailReportUrl, baseUrl || 'https://
|
|
146
|
+
const url = new URL(detailReportUrl, baseUrl || 'https://davidtest.aliwork.com');
|
|
147
147
|
const pathParts = url.pathname.split('/').filter(Boolean);
|
|
148
148
|
const previewIndex = pathParts.indexOf('preview');
|
|
149
149
|
const appType = previewIndex > 0 ? pathParts[previewIndex - 1] : '';
|
package/lib/feedback/feedback.js
CHANGED
|
@@ -530,7 +530,7 @@ function inferBaseUrl(formResult, publicPath) {
|
|
|
530
530
|
// ignore
|
|
531
531
|
}
|
|
532
532
|
}
|
|
533
|
-
return publicPath && publicPath.startsWith('http') ? new URL(publicPath).origin : 'https://
|
|
533
|
+
return publicPath && publicPath.startsWith('http') ? new URL(publicPath).origin : 'https://davidtest.aliwork.com';
|
|
534
534
|
}
|
|
535
535
|
|
|
536
536
|
function writeJson(filePath, value) {
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openyida",
|
|
3
|
-
"version": "2026.10.29-beta.
|
|
3
|
+
"version": "2026.10.29-beta.4",
|
|
4
4
|
"description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
|
|
5
5
|
"bin": {
|
|
6
|
-
"openyida": "bin/yida
|
|
7
|
-
"yida": "bin/yida
|
|
6
|
+
"openyida": "bin/yida.js",
|
|
7
|
+
"yida": "bin/yida.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
"!lib/samples/**/assets/**",
|
|
16
16
|
"scripts/postinstall.js",
|
|
17
17
|
"README.md",
|
|
18
|
-
"LICENSE"
|
|
19
|
-
"scripts/install-qwenwork-pre-beta.js"
|
|
18
|
+
"LICENSE"
|
|
20
19
|
],
|
|
21
20
|
"scripts": {
|
|
22
21
|
"test": "jest",
|
|
@@ -64,7 +63,7 @@
|
|
|
64
63
|
"check:package": "npm pack --dry-run",
|
|
65
64
|
"check:release": "npm run check:ci",
|
|
66
65
|
"check:ci": "bash scripts/validate-ci.sh",
|
|
67
|
-
"postinstall": "node scripts/
|
|
66
|
+
"postinstall": "node scripts/postinstall.js"
|
|
68
67
|
},
|
|
69
68
|
"keywords": [
|
|
70
69
|
"yida",
|
|
@@ -113,23 +112,12 @@
|
|
|
113
112
|
"node": ">=18"
|
|
114
113
|
},
|
|
115
114
|
"openyidaAgent": {
|
|
116
|
-
"endpoint": "https://
|
|
115
|
+
"endpoint": "https://davidtest.aliwork.com/openyida/agent-api"
|
|
117
116
|
},
|
|
118
117
|
"optionalDependencies": {
|
|
119
118
|
"@openyida/local-agent-runtime-darwin-arm64": "0.1.4",
|
|
120
119
|
"@openyida/local-agent-runtime-darwin-x64": "0.1.4",
|
|
121
120
|
"@openyida/local-agent-runtime-win32-x64": "0.1.4",
|
|
122
121
|
"@openyida/local-agent-runtime-win32-arm64": "0.1.4"
|
|
123
|
-
},
|
|
124
|
-
"openyidaLocalBeta": {
|
|
125
|
-
"desktopTicketLogin": true
|
|
126
|
-
},
|
|
127
|
-
"openyidaBuild": {
|
|
128
|
-
"sourceCommit": "32eae192ec36e6764878e7123d2902170924911d",
|
|
129
|
-
"sourceBranch": "feature/qwenwork-cloud-auth",
|
|
130
|
-
"mergedMainCommit": "ab4ccf97e03a7469df7eee731b300c59289b8231",
|
|
131
|
-
"distTag": "beta",
|
|
132
|
-
"presetScope": "none",
|
|
133
|
-
"environmentRouting": "standard"
|
|
134
122
|
}
|
|
135
123
|
}
|
package/project/config.json
CHANGED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const fs = require('node:fs');
|
|
3
|
-
const os = require('node:os');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
|
|
6
|
-
/** 技能复制属于可选安装步骤,权限不足时保留旧目录,CLI 仍可使用随包技能。 */
|
|
7
|
-
function install(options = {}) {
|
|
8
|
-
const io = options.fs || fs;
|
|
9
|
-
const home = options.home || os.homedir();
|
|
10
|
-
const root = path.join(home, '.qwenworkcn');
|
|
11
|
-
if (!options.target && !io.existsSync(root)) { return { status: 'skipped' }; }
|
|
12
|
-
const source = options.source || path.resolve(__dirname, '../yida-skills');
|
|
13
|
-
const target = options.target || path.join(root, 'skills', 'yida-skills');
|
|
14
|
-
const unique = `${Date.now()}-${process.pid}`;
|
|
15
|
-
const backup = path.join(options.backupRoot || path.join(options.target ? path.dirname(target) : root, '.openyida-beta-backups'), unique);
|
|
16
|
-
const stage = path.join(path.dirname(target), `.openyida-installing-${unique}`);
|
|
17
|
-
let moved = false;
|
|
18
|
-
try {
|
|
19
|
-
io.mkdirSync(path.dirname(target), { recursive: true });
|
|
20
|
-
io.cpSync(source, stage, { recursive: true });
|
|
21
|
-
if (io.existsSync(target)) {
|
|
22
|
-
io.mkdirSync(path.dirname(backup), { recursive: true });
|
|
23
|
-
io.renameSync(target, backup);
|
|
24
|
-
moved = true;
|
|
25
|
-
}
|
|
26
|
-
io.renameSync(stage, target);
|
|
27
|
-
return { status: 'updated' };
|
|
28
|
-
} catch {
|
|
29
|
-
// 可选复制失败不使 npm 回滚;旧技能已移动时先恢复,备份始终保留在扫描目录外。
|
|
30
|
-
try { if (moved && !io.existsSync(target)) { io.renameSync(backup, target); } } catch { /* 保留备份,避免覆盖现有内容。 */ }
|
|
31
|
-
try { io.rmSync(stage, { recursive: true, force: true }); } catch { /* 权限不足时不扩大清理范围。 */ }
|
|
32
|
-
return { status: 'skipped' };
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (require.main === module) {
|
|
37
|
-
const index = process.argv.indexOf('--target-dir');
|
|
38
|
-
install(index >= 0 && process.argv[index + 1] ? { target: path.resolve(process.argv[index + 1]) } : {});
|
|
39
|
-
}
|
|
40
|
-
module.exports = { install };
|