openyida 2026.9.22-beta.2 → 2026.9.22-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.
@@ -10,12 +10,19 @@ const FAILURE_STATUSES = new Set([
10
10
  'dws_identity_unmapped', 'dws_upstream_unavailable', 'dws_policy_rejected', 'dws_session_expired',
11
11
  ]);
12
12
 
13
- function failure(reason) {
13
+ function failure(reason, extra = {}) {
14
14
  return new CliError(`DWS sync failed: ${reason}`, {
15
- code: 'DWS_SYNC_FAILED', details: { reason },
15
+ code: 'DWS_SYNC_FAILED', details: { reason, ...extra },
16
16
  });
17
17
  }
18
18
 
19
+ function rateLimitFailure(response, headers = {}) {
20
+ const data = response && (response.data || response.content || response);
21
+ if (!data || !['dws_rate_limited', 'dws_rate_limit_unavailable'].includes(data.status)) { return null; }
22
+ const delay = Number(data.retry_after ?? headers['retry-after']);
23
+ return failure(data.status, { retry_after: Number.isInteger(delay) && delay > 0 && delay <= 60 ? delay : 1 });
24
+ }
25
+
19
26
  function readHandoff(input = process.stdin, timeoutMs = 10000) {
20
27
  return new Promise((resolve, reject) => {
21
28
  if (input.isTTY) { reject(failure('STDIN_REQUIRED')); return; }
@@ -80,11 +87,13 @@ async function syncDws(options = {}) {
80
87
  response = await requestJson('POST', `${target.origin}/openapi/cli/v1/auth/dws/token`, {
81
88
  ...(expectedCorp ? { corpId: expectedCorp } : {}), environment: handoff.environment,
82
89
  }, { Authorization: `Bearer ${handoff.accessToken}` }, { timeoutMs: 30000, maxResponseBytes: 65536 });
83
- } catch {
84
- throw failure('EXCHANGE_REQUEST_FAILED');
90
+ } catch (error) {
91
+ throw rateLimitFailure(error && error.payload, (error && error.headers) || {}) || failure('EXCHANGE_REQUEST_FAILED');
85
92
  } finally {
86
93
  delete handoff.accessToken;
87
94
  }
95
+ const limited = rateLimitFailure(response);
96
+ if (limited) { throw limited; }
88
97
  const data = response && (response.data || response.content || response);
89
98
  if (!data || response.success === false || response.ok === false || data.status !== 'ok') {
90
99
  throw failure(data && FAILURE_STATUSES.has(data.status) ? data.status : 'EXCHANGE_REJECTED');
@@ -96,7 +96,9 @@ function compactSkillsDiagnostics(skillsDiagnostics) {
96
96
  reason: candidate.reason,
97
97
  exists: candidate.exists,
98
98
  writable: candidate.writable,
99
+ readable: candidate.readable,
99
100
  usable: candidate.usable,
101
+ installable: candidate.installable,
100
102
  workspace_only: candidate.workspace_only,
101
103
  }
102
104
  : null;
@@ -105,6 +107,7 @@ function compactSkillsDiagnostics(skillsDiagnostics) {
105
107
  schema_version: skillsDiagnostics.schema_version,
106
108
  active_tool: skillsDiagnostics.active_tool,
107
109
  selected: compactCandidate(skillsDiagnostics.selected),
110
+ install_target: compactCandidate(skillsDiagnostics.install_target),
108
111
  candidates: (skillsDiagnostics.candidates || []).map(compactCandidate),
109
112
  diagnostics: skillsDiagnostics.diagnostics || {},
110
113
  };
package/lib/core/copy.js CHANGED
@@ -390,7 +390,7 @@ function run(args = process.argv.slice(3)) {
390
390
  // 若宿主用户级目录不可写,按 capabilities 诊断降级到工作区可发现目录。
391
391
  const activeResult = envResults.find((r) => r.isActive);
392
392
  const skillsDiagnostics = buildSkillsDiagnostics({ ...runtimeOptions, projectResolution });
393
- const selectedSkills = skillsDiagnostics.selected;
393
+ const selectedSkills = skillsDiagnostics.install_target;
394
394
 
395
395
  if (selectedSkills && selectedSkills.path) {
396
396
  assertCopyDestinationSafe(packageYidaSkillsDir, selectedSkills.path);
package/lib/core/utils.js CHANGED
@@ -670,6 +670,14 @@ function buildSkillsCandidate(destPath, source, scope, reason) {
670
670
  const parentDir = path.dirname(destPath);
671
671
  const nearestWritableBase = nearestExistingAncestor(parentDir);
672
672
  const writable = canWritePath(destPath);
673
+ const entryPath = path.join(destPath, 'SKILL.md');
674
+ let readable = false;
675
+ try {
676
+ readable = fs.statSync(entryPath).isFile();
677
+ fs.accessSync(entryPath, fs.constants.R_OK);
678
+ } catch {
679
+ readable = false;
680
+ }
673
681
  return {
674
682
  path: destPath,
675
683
  source,
@@ -680,7 +688,9 @@ function buildSkillsCandidate(destPath, source, scope, reason) {
680
688
  parent_exists: fs.existsSync(parentDir),
681
689
  nearest_existing_parent: nearestWritableBase,
682
690
  writable,
683
- usable: writable,
691
+ readable,
692
+ usable: readable,
693
+ installable: scope !== 'package' && writable,
684
694
  workspace_only: scope === 'workspace',
685
695
  };
686
696
  }
@@ -787,7 +797,13 @@ function buildSkillsDiagnostics(options = {}) {
787
797
  );
788
798
  }
789
799
 
790
- const selected = candidates.find((candidate) => candidate.usable) || candidates[0] || null;
800
+ // Bundled documentation is a read fallback, never an installation destination.
801
+ addCandidate(
802
+ options.bundledSkillsPath || path.resolve(__dirname, '../../yida-skills'),
803
+ 'package:yida-skills', 'package', 'bundled_skills_read_fallback'
804
+ );
805
+ const selected = candidates.find((candidate) => candidate.usable) || null;
806
+ const installTarget = candidates.find((candidate) => candidate.installable) || null;
791
807
  const diagnostics = {
792
808
  config_content_error: configSkillPaths.error,
793
809
  fallback: selected && selected.workspace_only
@@ -808,6 +824,7 @@ function buildSkillsDiagnostics(options = {}) {
808
824
  }
809
825
  : null,
810
826
  selected,
827
+ install_target: installTarget,
811
828
  candidates,
812
829
  diagnostics,
813
830
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.9.22-beta.2",
3
+ "version": "2026.9.22-beta.4",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",
@@ -11,7 +11,8 @@ description: 宜搭登录态管理。以 OpenYida auth snapshot 为准;默认
11
11
  - 不要根据 agent 名称、宿主产品、workspace 路径或猜测的环境变量推断认证模式。
12
12
  - 先读 `openyida agent-capabilities --summary-json`;只有需要更多信息时,才降级执行 `openyida login --check-only --json`。
13
13
  - snapshot 返回 `login.auth_source=env` 或 `failure_reason=env_token_missing` 时,进入运行环境注入 token 模式。凭证只来自运行环境注入的 `OPENYIDA_ACCESS_TOKEN`、`OPENYIDA_REFRESH_TOKEN` 等环境变量。
14
- - 其他 `auth_mode=token` 场景使用默认 OAuth token session。
14
+ - snapshot 明确返回 `credential_source=dws`,或当前任务已由 DWS prepare 交接时,优先使用下文 DWS 恢复流程;不自动改走 OAuth。
15
+ - 除上述 DWS 和 env 注入场景外,其他 `auth_mode=token` 场景使用默认 OAuth token session。
15
16
  - 不要从 `.cache/cookies*.json` 推断登录态。
16
17
  - 浏览器归属以 `builder_path.interactive_login.mode` 为准:`not_required` 不触发 OAuth;`cli_auto_open` 执行 `openyida login` 并等待;`caller_open_url` 执行 `openyida login --no-browser`,由 Agent 优先调用沙箱浏览器 / 内置 Browser 打开 CLI 输出的授权 URL 一次;`unsupported` 停止并说明没有可用浏览器能力,不要默认安装 Playwright。
17
18
  - `caller_open_url` 模式下,Agent 不要只把 URL 贴给用户然后等待。只有当前宿主没有浏览器工具,或浏览器工具调用失败时,才退回让用户手动打开 URL。
@@ -42,9 +43,15 @@ openyida login --check-only --json
42
43
  | `auth_mode=token`,未登录,且 `interactive_login.mode=caller_open_url` | 只执行一次 `openyida login --no-browser`,由 Agent 优先调用沙箱浏览器 / 内置 Browser 打开输出 URL 一次,并等待原命令结束;无浏览器工具或调用失败时才让用户手动打开 |
43
44
  | `auth_mode=token`,未登录,且 `interactive_login.mode=unsupported` | 停止并向用户说明当前运行环境没有桌面浏览器或 Agent 浏览器能力 |
44
45
 
46
+ ## DWS 登录态恢复
47
+
48
+ 仅当 snapshot 明确 credential_source=dws 或任务已有可信的 DWS 交接上下文时适用,不根据宿主名字或目录猜测。宜搭会话过期/已退出不等于 DWS 已退出。用户授权继续任务时,交回 DWS 路由,通过原任务目录的 dws yida prepare 重新交接;环境与 workdir 沿用已确认值,完成后重新核对用户、组织及 execution。Agent 不读取 token、不自行调用 auth sync-dws 拼接凭据,也不先触发 openyida login。
49
+
50
+ 用户明确退出时只完成退出,不立即恢复登录。限流、网络或服务故障、业务权限不足不触发登录恢复循环。DWS 不可用时明确告知需要宿主恢复交接;只有确认 DWS 凭证本身失效,才引导恢复 DWS 登录。写入结果未知时先查询,不能因重新认证重复写入。
51
+
45
52
  ## Token 模式命令
46
53
 
47
- 只有 auth snapshot 未返回 env 注入模式时,才使用 OAuth 登录。
54
+ 只有 auth snapshot 未返回 env 注入模式,且当前任务不属于上述 DWS 交接路径时,才使用 OAuth 登录。
48
55
 
49
56
  ```bash
50
57
  openyida login