@yosai/cli 0.2.1-test.0 → 0.2.1-test.10

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 (39) hide show
  1. package/README.md +11 -9
  2. package/dist/cli.js +4 -2
  3. package/dist/commands/doctor.js +17 -11
  4. package/dist/commands/login.js +11 -10
  5. package/dist/commands/ping.js +7 -5
  6. package/dist/commands/update.js +52 -19
  7. package/dist/commands/version.js +9 -7
  8. package/dist/config.js +15 -11
  9. package/dist/domain-commands.js +13 -4
  10. package/dist/domain-executor.js +3 -0
  11. package/dist/endpoints.js +33 -1
  12. package/dist/errors.js +6 -0
  13. package/dist/npm-meta.js +88 -11
  14. package/dist/openapis-client.js +28 -5
  15. package/dist/skill-sync.js +2 -2
  16. package/dist/skills/yos/SKILL.md +23 -14
  17. package/dist/skills/yos-doc/SKILL.md +221 -0
  18. package/dist/skills/yos-doc/references/AGENT_SCHEMA.doc.md +69 -0
  19. package/dist/skills/yos-doc/references/DOC_DATA_API.md +624 -0
  20. package/dist/skills/yos-doc/references/doc-draft.template.json +721 -0
  21. package/dist/skills/yos-doc/references/few-shot.draft.json +195 -0
  22. package/dist/skills/yos-doc/scripts/validate-runtime-doc.mjs +370 -0
  23. package/dist/skills/yos-doc/scripts/yos-doc.mjs +157 -0
  24. package/dist/skills/yos-install/SKILL.md +48 -21
  25. package/dist/skills/{yos-kb → yos-knowledge}/SKILL.md +21 -8
  26. package/dist/skills/yos-retrieval/SKILL.md +1 -1
  27. package/dist/skills/yos-shared/SKILL.md +24 -15
  28. package/dist/skills/yos-sheet/SKILL.md +233 -0
  29. package/dist/skills/yos-sheet/references/AGENT_SCHEMA.sheet.md +105 -0
  30. package/dist/skills/yos-sheet/references/SHEET_DATA_API.md +356 -0
  31. package/dist/skills/yos-sheet/references/few-shot.workbook.json +50 -0
  32. package/dist/skills/yos-sheet/references/sheet-workbook.template.json +147 -0
  33. package/dist/skills/yos-sheet/scripts/validate-runtime-sheet.mjs +404 -0
  34. package/dist/skills/yos-sheet/scripts/yos-sheet.mjs +157 -0
  35. package/dist/skills/yos-support/SKILL.md +1 -1
  36. package/dist/skills/yos-transcript/SKILL.md +1 -1
  37. package/dist/update-check-worker.js +35 -0
  38. package/dist/update-notice.js +86 -0
  39. package/package.json +4 -2
package/README.md CHANGED
@@ -10,7 +10,7 @@ YOS 能力的 Agent 执行入口:终端直调 YOS 工具 + 知识库文件上
10
10
  ## 快速开始
11
11
 
12
12
  ```bash
13
- yos login # 配置访问令牌(客户端「个人设置-访问令牌」页创建并复制)与端点
13
+ yos login # 配置访问令牌(客户端「个人设置-访问令牌」页创建并复制)
14
14
  yos doctor # 诊断配置与连通(显示当前身份)
15
15
  yos ping # 探活:连接与令牌有效性(退出码即结论,客户端对接用)
16
16
  yos kb list # 我的知识库
@@ -27,7 +27,7 @@ yos tool list # 底层工具通道(领域命令未覆盖
27
27
 
28
28
  | 命令 | 说明 |
29
29
  |---|---|
30
- | `yos login` | 配置访问令牌与端点(校验身份后保存) |
30
+ | `yos login` | 配置访问令牌(校验身份后保存) |
31
31
  | `yos doctor` | 诊断配置、访问令牌、连通、Skill 状态与 CLI 版本 |
32
32
  | `yos ping` | 探活:检查连接是否成功、令牌是否有效(退出码 0/2/3/4 即结论,客户端检测对接用) |
33
33
  | `yos kb <动作>` | 知识库:`list` / `detail` / `update` / `upload`(建库、删库请到 YOS 客户端) |
@@ -36,16 +36,16 @@ yos tool list # 底层工具通道(领域命令未覆盖
36
36
  | `yos support <动作>` | 支持:`manual` / `manual-read` / `manual-search` / `upgrade` / `upgrade-log` / `feedback` |
37
37
  | `yos transcript <动作>` | 转写:`list` / `detail` |
38
38
  | `yos tool list` / `schema` / `call` | 底层工具通道(领域命令未覆盖时兜底) |
39
- | `yos version [--check]` | CLI 版本(`--check` 比对 npmjs 最新版) |
40
- | `yos update [--check]` | 升级到 npmjs 最新版并自动重同步 Skill |
39
+ | `yos version [--check]` | CLI 版本(`--check` 比对对应通道最新版) |
40
+ | `yos update [--check] [--version <x.y.z>]` | 升级到对应通道最新版并自动重同步 Skill(命令执行前检测到新版本会有升级提示,照提示执行即可);`--version` 安装指定版本(可升可降,用于回退或锁定版本) |
41
41
  | `yos skill install [--target <dir>]` | 安装内置 Skill |
42
42
 
43
- 所有命令支持 `--json` 输出与统一退出码(0 成功 / 1 业务失败 / 2 参数错误 / 3 网络 / 4 鉴权)。
43
+ 所有命令支持 `--json` 输出与统一退出码(0 成功 / 1 业务失败 / 2 参数错误 / 3 网络 / 4 鉴权 / 5 版本过低)。
44
44
 
45
45
  ## 给 Agent 使用
46
46
 
47
47
  ```bash
48
- yos skill install # 安装 7 个内置 Skill(总入口 + 领域 + 安装引导)到 Claude Code / Cursor / Codex / Qoder / WorkBuddy 等技能目录
48
+ yos skill install # 安装 9 个内置 Skill(总入口 + 领域 + 安装引导)到 Claude Code / Cursor / Codex / Qoder / WorkBuddy 等技能目录
49
49
  ```
50
50
 
51
51
  安装后重启 Agent 工具,即可用自然语言驱动(如「把这份报告上传到知识库」)。`yos` 为能力总入口(何时使用 / 能力地图 / 命令使用方式),Agent 据此路由到各领域细则;`yos-shared` 收录跨领域约定(输出解析、退出码契约、仍需自行校验内容的场景、安全纪律);`yos-install` 是自包含的安装配置引导,可单独下载到发布仓库供用户引导装机。
@@ -55,13 +55,15 @@ yos skill install # 安装 7 个内置 Skill(总入口 + 领域 + 安装
55
55
  ## 配置
56
56
 
57
57
  - 配置文件:`~/.config/yos/config.json`(权限 0600;目录可用 `YOS_CONFIG_DIR` 覆盖)
58
- - 环境变量(均优先于配置文件,适合 CI/容器场景):`YOS_API_KEY` 访问令牌、`YOS_ENDPOINT` 服务端点
59
- - 端点:默认生产环境 `https://yos-bff.linkduoo.com`,`yos login --endpoint <url>` 持久切换(本地联调可用 http://localhost:8080);`yos doctor` 显示当前生效端点及其来源
58
+ - 环境变量(均优先于配置文件,适合 CI/容器场景):`YOS_API_KEY` 访问令牌、`YOS_ENV` 服务环境(prod / test / dev)
59
+ - 版本检查:命令执行前会检查对应通道最新版(后台进程查询、结果缓存 10 分钟),落后时以 stderr 提示「请先执行 yos update 升级再继续任务」;升级通道自动跟随服务环境(测试环境跟 test 通道)。CLI 不静默安装,升级由用户或 Agent 按提示显式执行
60
+ - 服务环境(测试/开发用):`yos login --env test` 切到测试环境,`--env dev` 指向本机开发服务(http://localhost:8080)
60
61
 
61
62
  ## 故障排查
62
63
 
63
64
  - 退出码 4(鉴权失败):访问令牌无效/已吊销/已过期,重新 `yos login`
64
65
  - 退出码 2(参数错误):CLI 本地拦截、未发请求,按 `提示:` 里的用法改写参数
66
+ - 退出码 5(版本过低):服务端要求的 CLI 最低版本高于当前版本,执行 `yos update` 升级后重试
65
67
  - 上传报 20MB 上限:单文件上限 20MB,更大文件暂不支持
66
68
  - 下载报「没有该资源的访问权限:DOWNLOAD.xxx」:只有阅读权限的库不支持下载原文件,需库管理员授权
67
- - 退出码 3(网络):确认端点可达;`yos version --check` 报网络错则是 npm 或 registry 不可达
69
+ - 退出码 3(网络):确认服务地址可达;`yos version --check` 报网络错则是 npm 或 registry 不可达
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@ import { parseArgs } from 'node:util';
2
2
  import { DOMAIN_META } from './domain-commands.js';
3
3
  import { CliError, EXIT, UsageError } from './errors.js';
4
4
  import { print, printErr, renderRows } from './output.js';
5
+ import { handleUpdateNotice } from './update-notice.js';
5
6
  const registry = [];
6
7
  const GROUP_ORDER = ['知识库 kb', '文档 docs', '检索 retrieve', '支持 support', '转写 transcript', '工具通道', 'Agent 接入', '基础'];
7
8
  const RISK_LABEL = { read: '只读', write: '写入', danger: '高危' };
@@ -20,12 +21,12 @@ export function renderHelp() {
20
21
  '',
21
22
  '约定:',
22
23
  ' - 输出默认人类可读;--json 输出结构化结果(工具返回原文在 text 字段内)',
23
- ' - 退出码:0 成功 / 1 业务失败 / 2 参数错误 / 3 网络错误 / 4 鉴权失败',
24
+ ' - 退出码:0 成功 / 1 业务失败 / 2 参数错误 / 3 网络错误 / 4 鉴权失败 / 5 版本过低需升级',
24
25
  ' - 删除类操作前先用 yos docs count <node_code> 查询影响范围,并取得用户明确确认',
25
26
  '',
26
27
  '环境变量(均优先于配置文件,便于 CI 免落盘):',
27
28
  ' YOS_API_KEY 访问令牌',
28
- ' YOS_ENDPOINT BFF 服务地址',
29
+ ' YOS_ENV 服务环境(prod / test / dev;缺省生产)',
29
30
  ' YOS_CONFIG_DIR 配置目录(缺省 ~/.config/yos)',
30
31
  '',
31
32
  '示例:',
@@ -170,6 +171,7 @@ export async function dispatch(argv) {
170
171
  print(renderCommandHelp(command));
171
172
  return EXIT.OK;
172
173
  }
174
+ handleUpdateNotice(command.name);
173
175
  try {
174
176
  const exitCode = await command.run(parseCommandArgs(command, rest));
175
177
  return typeof exitCode === 'number' ? exitCode : EXIT.OK;
@@ -1,5 +1,6 @@
1
1
  import { register } from '../cli.js';
2
- import { loadConfig, maskKey, resolveEndpoint, resolveKey } from '../config.js';
2
+ import { loadConfig, maskKey, resolveEnv, resolveKey } from '../config.js';
3
+ import { DEFAULT_ENV, endpointOfEnv, envLabel } from '../endpoints.js';
3
4
  import { CliError, EXIT } from '../errors.js';
4
5
  import { formatIdentity, OpenApisClient } from '../openapis-client.js';
5
6
  import { print, printJson } from '../output.js';
@@ -15,16 +16,20 @@ register({
15
16
  async run({ flags }) {
16
17
  const checks = [];
17
18
  const config = loadConfig();
19
+ const env = resolveEnv(config);
20
+ const endpoint = endpointOfEnv(env);
18
21
  checks.push({
19
22
  name: '配置文件',
20
23
  ok: config !== null,
21
24
  detail: config ? '已找到' : '未找到,请执行 yos login',
22
25
  });
23
- checks.push({
24
- name: '服务端点',
25
- ok: true,
26
- detail: `${resolveEndpoint(config)}(来自 ${endpointSource(config)})`,
27
- });
26
+ if (env !== DEFAULT_ENV) {
27
+ checks.push({
28
+ name: '服务环境',
29
+ ok: true,
30
+ detail: `${envLabel(env)}(来自 ${endpointSource(config)})`,
31
+ });
32
+ }
28
33
  let key = null;
29
34
  try {
30
35
  key = resolveKey(config);
@@ -39,7 +44,7 @@ register({
39
44
  }
40
45
  else {
41
46
  try {
42
- const identity = await new OpenApisClient(`${resolveEndpoint(config)}/open-apis`, key).whoami();
47
+ const identity = await new OpenApisClient(`${endpoint}/open-apis`, key).whoami();
43
48
  checks.push({ name: '服务连通', ok: true, detail: `身份 ${formatIdentity(identity)}` });
44
49
  }
45
50
  catch (e) {
@@ -55,10 +60,11 @@ register({
55
60
  detail: skillDirs.length > 0 ? skillDirs.join(', ') : '尚未安装,执行 yos skill install',
56
61
  });
57
62
  const meta = readPackageMeta();
63
+ const envNote = env === DEFAULT_ENV ? '' : `(环境=${envLabel(env)})`;
58
64
  checks.push({
59
65
  name: 'CLI 版本',
60
66
  ok: meta !== null,
61
- detail: meta ? `${meta.version}(endpoint=${resolveEndpoint(config)})` : 'unknown(未找到 package.json)',
67
+ detail: meta ? `${meta.version}${envNote}` : 'unknown(未找到 package.json)',
62
68
  });
63
69
  const state = loadVersionState();
64
70
  checks.push({
@@ -80,10 +86,10 @@ register({
80
86
  },
81
87
  });
82
88
  function endpointSource(config) {
83
- if (process.env.YOS_ENDPOINT) {
84
- return 'YOS_ENDPOINT';
89
+ if (process.env.YOS_ENV) {
90
+ return 'YOS_ENV';
85
91
  }
86
- return config?.endpoint ? '配置文件' : '内置缺省';
92
+ return config?.env ? '配置文件' : '内置缺省';
87
93
  }
88
94
  function skillSyncDetail(meta, state, skillDirs) {
89
95
  if (skillDirs.length === 0) {
@@ -1,27 +1,27 @@
1
1
  import { register } from '../cli.js';
2
- import { loadConfig, maskKey, normalizeEndpoint, saveConfig } from '../config.js';
3
- import { DEFAULT_ENDPOINT } from '../endpoints.js';
2
+ import { loadConfig, maskKey, resolveEnv, saveConfig } from '../config.js';
3
+ import { DEFAULT_ENV, endpointOfEnv, envLabel } from '../endpoints.js';
4
4
  import { UsageError } from '../errors.js';
5
5
  import { formatIdentity, OpenApisClient } from '../openapis-client.js';
6
6
  import { print } from '../output.js';
7
7
  import { readHiddenLine } from '../prompt.js';
8
8
  register({
9
9
  name: 'login',
10
- description: '配置访问令牌与 BFF 端点(校验身份后保存)',
11
- usage: 'yos login [--endpoint <url>] [--key <key>]',
10
+ description: '配置访问令牌与服务环境(校验身份后保存)',
11
+ usage: 'yos login [--env <name>] [--key <key>]',
12
12
  group: '基础',
13
13
  options: {
14
- endpoint: { type: 'string' },
14
+ env: { type: 'string' },
15
15
  key: { type: 'string' },
16
16
  },
17
17
  paramNotes: [
18
- ['--endpoint', 'BFF 服务地址(缺省读 YOS_ENDPOINT,再缺省生产环境;本地联调 http://localhost:8080)'],
18
+ ['--env', '服务环境(prod / test / dev;缺省读 YOS_ENV,再缺省生产)'],
19
19
  ['--key', '访问令牌(40 位小写 hex,缺省读 YOS_API_KEY,否则交互输入)'],
20
20
  ],
21
21
  async run({ flags }) {
22
22
  const config = loadConfig();
23
- const endpoint = normalizeEndpoint(flags.endpoint
24
- ?? process.env.YOS_ENDPOINT ?? config?.endpoint ?? DEFAULT_ENDPOINT);
23
+ const env = resolveEnv(config, { env: flags.env });
24
+ const endpoint = endpointOfEnv(env);
25
25
  const key = flags.key
26
26
  ?? process.env.YOS_API_KEY
27
27
  ?? await readHiddenLine('请粘贴访问令牌(输入不回显,客户端「个人设置-访问令牌」页创建并复制): ');
@@ -29,7 +29,8 @@ register({
29
29
  throw new UsageError('令牌格式不正确(应为 40 位小写 hex)', '请在客户端「个人设置-访问令牌」页创建并复制完整令牌');
30
30
  }
31
31
  const identity = await new OpenApisClient(`${endpoint}/open-apis`, key).whoami();
32
- saveConfig({ ...config, endpoint, apiKey: key });
33
- print(`已保存配置:endpoint=${endpoint},key=${maskKey(key)},身份 ${formatIdentity(identity)}`);
32
+ saveConfig({ ...config, env, apiKey: key });
33
+ const envNote = env === DEFAULT_ENV ? '' : `环境=${envLabel(env)},`;
34
+ print(`已保存配置:${envNote}key=${maskKey(key)},身份 ${formatIdentity(identity)}`);
34
35
  },
35
36
  });
@@ -1,5 +1,6 @@
1
1
  import { register } from '../cli.js';
2
- import { loadConfig, resolveEndpoint, resolveKey } from '../config.js';
2
+ import { loadConfig, resolveEnv, resolveKey } from '../config.js';
3
+ import { endpointOfEnv } from '../endpoints.js';
3
4
  import { CliError, EXIT } from '../errors.js';
4
5
  import { formatIdentity, OpenApisClient } from '../openapis-client.js';
5
6
  import { print, printErr, printJson } from '../output.js';
@@ -9,14 +10,15 @@ register({
9
10
  usage: 'yos ping [--json]',
10
11
  group: '基础',
11
12
  options: { json: { type: 'boolean' } },
12
- paramNotes: [['--json', '结构化输出(ok / endpoint / userCode / userName / error)']],
13
+ paramNotes: [['--json', '结构化输出(ok / env / userCode / userName / error)']],
13
14
  async run({ flags }) {
14
15
  const config = loadConfig();
15
- const endpoint = resolveEndpoint(config);
16
+ const env = resolveEnv(config);
17
+ const endpoint = endpointOfEnv(env);
16
18
  try {
17
19
  const identity = await new OpenApisClient(`${endpoint}/open-apis`, resolveKey(config)).whoami();
18
20
  if (flags.json) {
19
- printJson({ ok: true, endpoint, userCode: identity.userCode, userName: identity.userName });
21
+ printJson({ ok: true, env, userCode: identity.userCode, userName: identity.userName });
20
22
  }
21
23
  else {
22
24
  print(`✓ 连接成功,令牌有效:身份 ${formatIdentity(identity)}`);
@@ -26,7 +28,7 @@ register({
26
28
  catch (e) {
27
29
  const failure = e instanceof CliError ? e : new CliError(String(e), EXIT.BUSINESS);
28
30
  if (flags.json) {
29
- printJson({ ok: false, endpoint, error: failure.message });
31
+ printJson({ ok: false, env, error: failure.message });
30
32
  }
31
33
  else {
32
34
  printErr(`错误: ${failure.message}`);
@@ -1,5 +1,5 @@
1
1
  import { register } from '../cli.js';
2
- import { loadConfig } from '../config.js';
2
+ import { loadConfig, resolveChannel } from '../config.js';
3
3
  import { BusinessError } from '../errors.js';
4
4
  import { compareVersions, detectGlobalInstall, fetchLatestVersion, PUBLIC_REGISTRY, readPackageMeta, runNpm } from '../npm-meta.js';
5
5
  import { print, printJson } from '../output.js';
@@ -7,25 +7,34 @@ import { installSkillsTo } from '../skill-sync.js';
7
7
  import { saveVersionState } from '../version-state.js';
8
8
  register({
9
9
  name: 'update',
10
- description: '检查并升级 CLI 到 npmjs 最新版(升级后自动重同步 Skill)',
11
- usage: 'yos update [--check] [--json]',
10
+ description: '检查并升级 CLI 到对应通道最新版(升级后自动重同步 Skill)',
11
+ usage: 'yos update [--check] [--json] [--version <x.y.z>]',
12
12
  group: '基础',
13
13
  risk: 'write',
14
- options: { check: { type: 'boolean' }, json: { type: 'boolean' } },
14
+ options: { check: { type: 'boolean' }, json: { type: 'boolean' }, version: { type: 'string' } },
15
15
  paramNotes: [
16
16
  ['--check', '只检查是否有新版本,不安装、不写入状态'],
17
17
  ['--json', '结构化输出(仅 --check 生效)'],
18
+ ['--version <x.y.z>', '安装指定版本(可升可降,用于回退或锁定版本);与 --check 互斥'],
18
19
  ],
19
20
  async run({ flags }) {
20
21
  const meta = readPackageMeta();
21
22
  if (!meta) {
22
23
  throw new BusinessError('无法确定包名(未找到 package.json)', '请通过 npm 全局安装后重试');
23
24
  }
24
- const latest = await fetchLatestVersion(meta.name);
25
+ if (flags.check && typeof flags.version === 'string') {
26
+ throw new BusinessError('--check 与 --version 不能同时使用', '查看最新版用 yos update --check;安装指定版本用 yos update --version <x.y.z>');
27
+ }
28
+ if (typeof flags.version === 'string') {
29
+ await installVersion(meta, flags.version);
30
+ return;
31
+ }
32
+ const channel = resolveChannel(loadConfig()) ?? 'latest';
33
+ const latest = await fetchLatestVersion(meta.name, channel);
25
34
  const outdated = compareVersions(meta.version, latest) < 0;
26
35
  if (flags.check) {
27
36
  if (flags.json) {
28
- printJson({ version: meta.version, latest, outdated });
37
+ printJson({ version: meta.version, latest, outdated, channel });
29
38
  }
30
39
  else {
31
40
  print(outdated
@@ -40,25 +49,49 @@ register({
40
49
  print(`已是最新(${meta.version})`);
41
50
  return;
42
51
  }
43
- const shape = await detectGlobalInstall(meta);
44
- if (!shape.global) {
45
- print(`当前安装不在 npm 全局目录(${meta.dir}),不代跑升级`);
46
- throw new BusinessError('非全局安装,已跳过自动升级', `手动升级:npm install -g ${meta.name}@latest --registry=${PUBLIC_REGISTRY};或先 npm uninstall -g ${meta.name} 再全局安装`);
47
- }
48
- const install = await runNpm(['install', '-g', `${meta.name}@latest`, `--registry=${PUBLIC_REGISTRY}`]);
49
- if (!install.ok) {
50
- const output = `${install.stderr}\n${install.stdout}`;
51
- if (/EACCES|EPERM|permission denied/i.test(output)) {
52
- throw new BusinessError('npm 全局目录没有写权限', '把 npm 前缀改到用户目录后重试(npm config set prefix ~/.npm-global 并把其 bin 加入 PATH),不要 sudo npm install -g');
53
- }
54
- throw new BusinessError(`npm install -g ${meta.name}@latest 失败:${firstLine(install.stderr) || firstLine(install.stdout) || '无输出'}`);
55
- }
52
+ await installGlobally(meta, `${meta.name}@${channel}`);
56
53
  syncSkills();
57
54
  saveVersionState(latest);
58
55
  print(`已升级 ${meta.version} → ${latest}`);
59
56
  print('提示:重启 Agent 工具以加载刷新后的 Skill');
60
57
  },
61
58
  });
59
+ const INSTALLABLE_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
60
+ async function installVersion(meta, version) {
61
+ if (!INSTALLABLE_VERSION.test(version)) {
62
+ throw new BusinessError(`版本格式非法:${version}`, '示例:0.2.0 或 0.2.1-test.9;安装通道最新版请直接执行 yos update');
63
+ }
64
+ if (compareVersions(version, meta.version) === 0) {
65
+ syncSkills();
66
+ saveVersionState(meta.version);
67
+ print(`已是指定版本(${version})`);
68
+ return;
69
+ }
70
+ const probe = await runNpm(['view', `${meta.name}@${version}`, 'version', `--registry=${PUBLIC_REGISTRY}`]);
71
+ if (!probe.ok || probe.stdout.trim() === '') {
72
+ throw new BusinessError(`npm registry 上不存在版本 ${version}`, `查看可用版本:npm view ${meta.name} versions --registry=${PUBLIC_REGISTRY}`);
73
+ }
74
+ await installGlobally(meta, `${meta.name}@${version}`);
75
+ syncSkills();
76
+ saveVersionState(version);
77
+ print(`${compareVersions(version, meta.version) > 0 ? '已升级' : '已回退'} ${meta.version} → ${version}`);
78
+ print('提示:重启 Agent 工具以加载刷新后的 Skill');
79
+ }
80
+ async function installGlobally(meta, spec) {
81
+ const shape = await detectGlobalInstall(meta);
82
+ if (!shape.global) {
83
+ print(`当前安装不在 npm 全局目录(${meta.dir}),不代跑安装`);
84
+ throw new BusinessError('非全局安装,已跳过自动安装', `手动安装:npm install -g ${spec} --registry=${PUBLIC_REGISTRY};或先 npm uninstall -g ${meta.name} 再全局安装`);
85
+ }
86
+ const install = await runNpm(['install', '-g', spec, `--registry=${PUBLIC_REGISTRY}`]);
87
+ if (!install.ok) {
88
+ const output = `${install.stderr}\n${install.stdout}`;
89
+ if (/EACCES|EPERM|permission denied/i.test(output)) {
90
+ throw new BusinessError('npm 全局目录没有写权限', '把 npm 前缀改到用户目录后重试(npm config set prefix ~/.npm-global 并把其 bin 加入 PATH),不要 sudo npm install -g');
91
+ }
92
+ throw new BusinessError(`npm install -g ${spec} 失败:${firstLine(install.stderr) || firstLine(install.stdout) || '无输出'}`);
93
+ }
94
+ }
62
95
  function syncSkills() {
63
96
  const dirs = loadConfig()?.installedSkillDirs ?? [];
64
97
  if (dirs.length === 0) {
@@ -1,27 +1,28 @@
1
1
  import { register } from '../cli.js';
2
- import { loadConfig, resolveEndpoint } from '../config.js';
2
+ import { loadConfig, resolveChannel, resolveEnv } from '../config.js';
3
3
  import { BusinessError } from '../errors.js';
4
4
  import { compareVersions, detectGlobalInstall, fetchLatestVersion, readPackageMeta } from '../npm-meta.js';
5
5
  import { print, printJson } from '../output.js';
6
6
  register({
7
7
  name: 'version',
8
- description: '显示 CLI 版本(--check 比对 npmjs 最新版)',
8
+ description: '显示 CLI 版本(--check 比对对应通道最新版)',
9
9
  usage: 'yos version [--json] [--check]',
10
10
  group: '基础',
11
11
  risk: 'read',
12
12
  options: { json: { type: 'boolean' }, check: { type: 'boolean' } },
13
13
  paramNotes: [
14
- ['--check', '与 npmjs 最新版本比对是否可升级(只读,不写入任何状态)'],
14
+ ['--check', '与对应通道最新版本比对是否可升级(只读,不写入任何状态)'],
15
15
  ['--json', '结构化输出'],
16
16
  ],
17
17
  async run({ flags }) {
18
18
  const meta = readPackageMeta();
19
19
  const version = meta?.version ?? 'unknown';
20
- const endpoint = resolveEndpoint(loadConfig());
20
+ const config = loadConfig();
21
+ const env = resolveEnv(config);
21
22
  if (!flags.check) {
22
23
  if (flags.json) {
23
24
  const installedGlobally = meta ? (await detectGlobalInstall(meta)).global : false;
24
- printJson({ version, installedGlobally, endpoint });
25
+ printJson({ version, installedGlobally, env });
25
26
  }
26
27
  else {
27
28
  print(`yos CLI ${version}`);
@@ -31,10 +32,11 @@ register({
31
32
  if (!meta) {
32
33
  throw new BusinessError('无法确定包名(未找到 package.json)', '请通过 npm 全局安装后重试');
33
34
  }
34
- const latest = await fetchLatestVersion(meta.name);
35
+ const channel = resolveChannel(config) ?? 'latest';
36
+ const latest = await fetchLatestVersion(meta.name, channel);
35
37
  const outdated = compareVersions(meta.version, latest) < 0;
36
38
  if (flags.json) {
37
- printJson({ version, latest, outdated, installedGlobally: (await detectGlobalInstall(meta)).global, endpoint });
39
+ printJson({ version, latest, outdated, installedGlobally: (await detectGlobalInstall(meta)).global, env, channel });
38
40
  return;
39
41
  }
40
42
  print(outdated
package/dist/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
- import { DEFAULT_ENDPOINT } from './endpoints.js';
4
+ import { channelOfEnv, DEFAULT_ENV, endpointOfEnv } from './endpoints.js';
5
5
  import { UsageError } from './errors.js';
6
6
  export function configDir() {
7
7
  return process.env.YOS_CONFIG_DIR ?? join(homedir(), '.config', 'yos');
@@ -15,7 +15,9 @@ export function loadConfig() {
15
15
  return null;
16
16
  }
17
17
  try {
18
- return JSON.parse(readFileSync(path, 'utf8'));
18
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
19
+ delete parsed.endpoint;
20
+ return parsed;
19
21
  }
20
22
  catch {
21
23
  return null;
@@ -35,21 +37,23 @@ export function resolveKey(config) {
35
37
  }
36
38
  throw new UsageError('未配置访问令牌', '请执行 `yos login` 或设置环境变量 YOS_API_KEY');
37
39
  }
38
- export function resolveEndpoint(config) {
39
- const fromEnv = process.env.YOS_ENDPOINT;
40
- if (fromEnv) {
41
- return normalizeEndpoint(fromEnv);
42
- }
43
- return normalizeEndpoint(config?.endpoint ?? DEFAULT_ENDPOINT);
40
+ export function resolveEnv(config, flags) {
41
+ const env = flags?.env || process.env.YOS_ENV || config?.env || DEFAULT_ENV;
42
+ endpointOfEnv(env);
43
+ return env;
44
+ }
45
+ export function resolveEndpoint(config, flags) {
46
+ return endpointOfEnv(resolveEnv(config, flags));
44
47
  }
45
- export function normalizeEndpoint(endpoint) {
46
- return endpoint.replace(/\/+$/, '');
48
+ export function resolveChannel(config) {
49
+ return channelOfEnv(resolveEnv(config));
47
50
  }
48
51
  export function maskKey(key) {
49
52
  return key.length <= 12 ? '****' : `${key.slice(0, 8)}****${key.slice(-4)}`;
50
53
  }
51
54
  export function updateConfig(patch) {
52
- const merged = { endpoint: DEFAULT_ENDPOINT, ...loadConfig(), ...patch };
55
+ const existing = loadConfig();
56
+ const merged = existing ? { ...existing, ...patch } : { env: DEFAULT_ENV, ...patch };
53
57
  saveConfig(merged);
54
58
  return merged;
55
59
  }
@@ -24,19 +24,28 @@ export const DOMAIN_COMMANDS = [
24
24
  },
25
25
  { flag: 'keyword', param: 'keyword', kind: 'string', description: '名称关键词(仅匹配库名,不搜正文)' },
26
26
  { flag: 'page', param: 'page', kind: 'number', description: '页码(缺省 1)', min: 1 },
27
- { flag: 'size', param: 'size', kind: 'number', description: '每页条数(缺省 10,上限 50)', min: 1, max: 50 },
27
+ { flag: 'size', param: 'size', kind: 'number', description: '每页条数(缺省 50)', min: 1, max: 50, defaultValue: 50 },
28
28
  ],
29
29
  render: (result) => {
30
30
  const data = parseToolJson(result);
31
31
  const records = data.records ?? [];
32
+ const total = data.total ?? records.length;
33
+ const page = data.page ?? 1;
32
34
  if (records.length === 0) {
33
- print('(无知识库)');
35
+ print(total > 0 ? `(本页无数据:共 ${total} 条,--page ${page} 超出范围)` : '(无知识库)');
34
36
  return;
35
37
  }
36
38
  print(renderRows(records.map((r) => [
37
39
  String(r.kbCode ?? r.kb_code ?? ''),
38
40
  String(r.kbName ?? r.kb_name ?? r.name ?? ''),
39
41
  ])));
42
+ const shown = (page - 1) * (data.size ?? records.length) + records.length;
43
+ if (shown < total) {
44
+ print(`(共 ${total} 条,还有 ${total - shown} 条;翻页加 --page ${page + 1})`);
45
+ }
46
+ else if (page > 1) {
47
+ print(`(共 ${total} 条,已到末页)`);
48
+ }
40
49
  },
41
50
  },
42
51
  {
@@ -116,7 +125,7 @@ export const DOMAIN_COMMANDS = [
116
125
  {
117
126
  flag: 'type', param: 'doc_type', kind: 'string',
118
127
  description: '文档类型(缺省 md):md=Markdown 文本 / cloud=云文档 / sheet=表格;'
119
- + 'cloud 与 sheet 的正文须为对应草稿 JSON,一般场景一律用 md',
128
+ + 'cloud 与 sheet 的正文为对应草稿 JSON(结构分别见 yos-doc / yos-sheet 技能),一般场景一律用 md',
120
129
  values: ['md', 'cloud', 'sheet'],
121
130
  },
122
131
  { flag: 'content', param: 'content', kind: 'string', description: '正文文本(与 --file 二选一)' },
@@ -154,7 +163,7 @@ export const DOMAIN_COMMANDS = [
154
163
  flags: [
155
164
  { flag: 'title', param: 'title', kind: 'string', description: '新标题' },
156
165
  { flag: 'content', param: 'content', kind: 'string', description: '新正文,整体覆盖原正文(与 --file 二选一)' },
157
- { flag: 'file', param: 'content', kind: 'string', description: '新正文文件路径,整体覆盖原正文(与 --content 二选一)' },
166
+ { flag: 'file', param: 'content', kind: 'string', description: '新正文文件路径,整体覆盖原正文(与 --content 二选一);云文档/云表格节点传对应草稿 JSON(结构分别见 yos-doc / yos-sheet 技能)' },
158
167
  { flag: 'description', param: 'description', kind: 'string', description: '新简介(≤100 字)' },
159
168
  ],
160
169
  buildArgs: (context) => {
@@ -35,6 +35,9 @@ function buildDeclaredArgs(spec, context) {
35
35
  if (flag.required) {
36
36
  throw new UsageError(`缺少 --${flag.flag}`, `用法: ${buildUsageForSpec(spec)}`);
37
37
  }
38
+ if (flag.defaultValue !== undefined) {
39
+ args[flag.param] = flag.defaultValue;
40
+ }
38
41
  continue;
39
42
  }
40
43
  const values = Array.isArray(raw) ? raw : [raw];
package/dist/endpoints.js CHANGED
@@ -1 +1,33 @@
1
- export const DEFAULT_ENDPOINT = 'https://yos-bff.linkduoo.com';
1
+ import { UsageError } from './errors.js';
2
+ export const ENVIRONMENTS = {
3
+ prod: { url: 'https://yos-bff.linkduoo.com', label: '生产' },
4
+ test: { url: 'https://yos-bff-test.linkduoo.com', label: '测试' },
5
+ dev: { url: 'http://localhost:8080', label: '开发' },
6
+ };
7
+ export const DEFAULT_ENV = 'prod';
8
+ export function endpointOfEnv(env) {
9
+ const found = ENVIRONMENTS[env];
10
+ if (!found) {
11
+ throw new UsageError(`无法识别的环境:${env}`, `可用环境:${Object.keys(ENVIRONMENTS).join(' / ')}`);
12
+ }
13
+ return found.url;
14
+ }
15
+ const CHANNEL_OF_ENV = {
16
+ prod: 'latest',
17
+ test: 'test',
18
+ };
19
+ export function channelOfEnv(env) {
20
+ return CHANNEL_OF_ENV[env] ?? null;
21
+ }
22
+ export function envLabel(env) {
23
+ return ENVIRONMENTS[env]?.label ?? env;
24
+ }
25
+ export function envNoteOfEndpoint(endpoint) {
26
+ const normalized = endpoint.replace(/\/+$/, '');
27
+ for (const [name, env] of Object.entries(ENVIRONMENTS)) {
28
+ if (env.url === normalized) {
29
+ return name === DEFAULT_ENV ? '' : `(${env.label})`;
30
+ }
31
+ }
32
+ return `(${normalized})`;
33
+ }
package/dist/errors.js CHANGED
@@ -4,6 +4,7 @@ export const EXIT = {
4
4
  USAGE: 2,
5
5
  NETWORK: 3,
6
6
  AUTH: 4,
7
+ UPGRADE: 5,
7
8
  };
8
9
  export class CliError extends Error {
9
10
  exitCode;
@@ -35,3 +36,8 @@ export class AuthError extends CliError {
35
36
  super(message, EXIT.AUTH, '请执行 `yos login` 重新配置访问令牌');
36
37
  }
37
38
  }
39
+ export class UpgradeError extends CliError {
40
+ constructor(message, hint) {
41
+ super(message, EXIT.UPGRADE, hint ?? '执行 `yos update` 升级后重试');
42
+ }
43
+ }