@youzan-cloud/zan-saas-cli 0.1.0-beta.3 → 0.1.0-beta.7

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 (36) hide show
  1. package/bin/zan-saas-cli.js +8 -11
  2. package/internal/registry/manifest.json +11 -10
  3. package/npm-shrinkwrap.json +2 -2
  4. package/package.json +1 -1
  5. package/skills/zan-saas/SKILL.md +5 -5
  6. package/skills/zan-saas/references/host-entry.md +10 -2
  7. package/skills/zan-saas-org/SKILL.md +4 -0
  8. package/skills/zan-saas-shared/SKILL.md +16 -5
  9. package/skills/zan-saas-shared/references/authentication.md +9 -4
  10. package/skills/zan-saas-shared/references/shop-context.md +37 -0
  11. package/src/auth/credential-source.js +26 -0
  12. package/src/auth/credential-store.js +61 -5
  13. package/src/auth/session.js +80 -27
  14. package/src/cli/command-paths.js +3 -2
  15. package/src/cli/program.js +20 -4
  16. package/src/commands/native/access/commands.index.json +1 -1
  17. package/src/commands/native/benefit/card-definition/list/command.js +4 -3
  18. package/src/commands/native/benefit/card-definition/list/schema.json +1 -1
  19. package/src/commands/native/benefit/commands.index.json +2 -2
  20. package/src/commands/native/benefit/customer-card/list/command.js +1 -0
  21. package/src/commands/native/benefit/customer-card/list/schema.json +1 -1
  22. package/src/commands/native/customer/commands.index.json +1 -1
  23. package/src/commands/native/customer/customer-tag/list/command.js +2 -2
  24. package/src/commands/native/member/commands.index.json +1 -1
  25. package/src/commands/native/org/commands.index.json +4 -4
  26. package/src/commands/system/api.js +1 -1
  27. package/src/commands/system/auth.js +13 -2
  28. package/src/commands/system/contracts.js +27 -19
  29. package/src/commands/system/shop-contracts.js +66 -0
  30. package/src/commands/system/shop.js +128 -27
  31. package/src/identity/context-store.js +14 -10
  32. package/src/identity/file-store.js +147 -0
  33. package/src/identity/session-store.js +261 -0
  34. package/src/identity/shop-location.js +153 -0
  35. package/src/transport/api.js +78 -11
  36. package/src/transport/shop-app.js +8 -1
@@ -2,22 +2,19 @@
2
2
  'use strict';
3
3
 
4
4
  const { createProgram } = require('../src/cli/program');
5
- const { runWithApiSession } = require('../src/transport/api');
6
5
  const { finalizeError } = require('../src/execution/error-details');
7
6
  const { visibleCommands } = require('../src/commands/system/schema');
8
7
  const { commandSchema } = require('../src/execution/command-schema');
9
8
 
10
9
  let program;
11
- Promise.resolve().then(() =>
12
- runWithApiSession(() => {
13
- program = createProgram();
14
- if (process.argv.length === 2) {
15
- program.outputHelp();
16
- return undefined;
17
- }
18
- return program.parseAsync(process.argv);
19
- })
20
- ).catch(error => {
10
+ Promise.resolve().then(() => {
11
+ program = createProgram();
12
+ if (process.argv.length === 2) {
13
+ program.outputHelp();
14
+ return undefined;
15
+ }
16
+ return program.parseAsync(process.argv);
17
+ }).catch(error => {
21
18
  if (['commander.helpDisplayed', 'commander.version'].includes(error.code)) return;
22
19
  if (error.code === 'commander.help' && error.exitCode === 0) return;
23
20
  const registered = program && visibleCommands(program)
@@ -10,7 +10,7 @@
10
10
  "indexFile": "access/commands.index.json",
11
11
  "commandCount": 2,
12
12
  "indexSize": 652,
13
- "indexSha256": "77d7b2ec53cfff2a40f6dee4adb5501f12cf9681a09e7e759c78570feff46546"
13
+ "indexSha256": "0b87b02e700ae06f5835f54599259a0d7c1739a7cb9f83f7db370010a6872bce"
14
14
  },
15
15
  {
16
16
  "name": "aftersale",
@@ -30,7 +30,7 @@
30
30
  "indexFile": "benefit/commands.index.json",
31
31
  "commandCount": 5,
32
32
  "indexSize": 1584,
33
- "indexSha256": "a528ed46344e3f751e45e492e4be4e95df229516df381cd1eae2352048d7248e"
33
+ "indexSha256": "fef946b05d691ffdc0e016c4def2c922d29b8f8000e199a4a1a39e80f6c5c50c"
34
34
  },
35
35
  {
36
36
  "name": "coupon",
@@ -50,7 +50,7 @@
50
50
  "indexFile": "customer/commands.index.json",
51
51
  "commandCount": 10,
52
52
  "indexSize": 3042,
53
- "indexSha256": "f978a5016e0f30f32b4808d05f258992e1e162cff4c5448bd85672dc266a97b0"
53
+ "indexSha256": "3aa80cf1681c76e7d720f7f70c411850b53d8d312b5468124caa7cfa1a987652"
54
54
  },
55
55
  {
56
56
  "name": "fulfillment",
@@ -90,7 +90,7 @@
90
90
  "indexFile": "member/commands.index.json",
91
91
  "commandCount": 6,
92
92
  "indexSize": 1811,
93
- "indexSha256": "62618e138aa9ef06743cfc9b3c1a08fd40883d7ebf8bc058e880f1b8a4a1bb65"
93
+ "indexSha256": "3f122369a09e91ac0d1ab8981bffef4d7a05ce99b8f4b0ae6cbd5b7588a9679c"
94
94
  },
95
95
  {
96
96
  "name": "order",
@@ -106,11 +106,11 @@
106
106
  "name": "org",
107
107
  "description": "组织:组织资料、员工与角色关联",
108
108
  "skill": "zan-saas-org",
109
- "fingerprint": "e2584b58e4c8ed6b2c080096e7e0fe1ba9c7f5297a1854d49f939749eef848c6",
109
+ "fingerprint": "a37293793a4944a5afa4ea2cf85168852f93b876e8e57d7f7b8d7b2a49e75ce0",
110
110
  "indexFile": "org/commands.index.json",
111
111
  "commandCount": 4,
112
112
  "indexSize": 1185,
113
- "indexSha256": "553820c277b0965b1a4e9d485290588e5939ca5a4b7986ea66501ceb5197f17f"
113
+ "indexSha256": "88abc0f5b6605a408ef88dd969f6ed2bb4473b7fa36001b2036c40d68e8a5173"
114
114
  },
115
115
  {
116
116
  "name": "points",
@@ -157,7 +157,7 @@
157
157
  {
158
158
  "name": "zan-saas",
159
159
  "type": "entry",
160
- "description": "有赞 SaaS CLI 入口。用户希望通过 zan-saas 查询商家业务数据或执行当前版本提供的操作时使用;按已发行领域选择指引,再用 help 和 schema 核实命令。",
160
+ "description": "有赞 SaaS 产品的命令行工具入口。用户需要查询有赞店铺的商品、订单、客户、会员、营销、库存等业务数据,或执行当前版本支持的店铺操作时使用。具体能力与适用范围以当前安装版指引为准。",
161
161
  "dependsOn": [
162
162
  "zan-saas-shared"
163
163
  ],
@@ -165,7 +165,7 @@
165
165
  "SKILL.md",
166
166
  "references/host-entry.md"
167
167
  ],
168
- "fingerprint": "501e06cc8c97bd9a2352fed7779e7609f97266a8c4b1b29dd25ec9a6e93668cf"
168
+ "fingerprint": "5b40c6bec46af43e31ea6406475c59afae705e9cc247640deafc4626b6c599b1"
169
169
  },
170
170
  {
171
171
  "name": "zan-saas-shared",
@@ -175,9 +175,10 @@
175
175
  "files": [
176
176
  "SKILL.md",
177
177
  "references/authentication.md",
178
+ "references/shop-context.md",
178
179
  "references/value-processing.md"
179
180
  ],
180
- "fingerprint": "5e4b2681b29e7e383fe0a39ca91327f7b9f00cded29c8f4da30eef8daca9dab0"
181
+ "fingerprint": "05045a2660b7801297f0883650aabc240fda7b6115b7cbe6a0edbdf43b1e2676"
181
182
  },
182
183
  {
183
184
  "name": "zan-saas-access",
@@ -359,7 +360,7 @@
359
360
  "references/unit-get.md",
360
361
  "references/unit-list.md"
361
362
  ],
362
- "fingerprint": "1ba9227cd426f9770a57c87fa335e5cac87f4c0e8d7d65f62437ab3bcbf1fa47"
363
+ "fingerprint": "2732c7e06c009a15f5b00fb028e72ceb518e1f5b1618c58523310962e14e1d4e"
363
364
  },
364
365
  {
365
366
  "name": "zan-saas-points",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@youzan-cloud/zan-saas-cli",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.7",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@youzan-cloud/zan-saas-cli",
9
- "version": "0.1.0-beta.3",
9
+ "version": "0.1.0-beta.7",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "ajv": "8.20.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youzan-cloud/zan-saas-cli",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.7",
4
4
  "description": "Youzan SaaS CLI preview",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: zan-saas
3
- description: 有赞 SaaS CLI 入口。用户希望通过 zan-saas 查询商家业务数据或执行当前版本提供的操作时使用;按已发行领域选择指引,再用 help 和 schema 核实命令。
3
+ description: 有赞 SaaS 产品的命令行工具入口。用户需要查询有赞店铺的商品、订单、客户、会员、营销、库存等业务数据,或执行当前版本支持的店铺操作时使用。具体能力与适用范围以当前安装版指引为准。
4
4
  ---
5
5
 
6
- # zan-saas 入口
6
+ # 有赞 SaaS CLI 入口
7
7
 
8
8
  ## 人机交接
9
9
 
@@ -11,13 +11,13 @@ Agent 负责运行命令、定位对象和解释结果。用户只需完成浏
11
11
 
12
12
  需要登录时,Agent 运行 `zan-saas auth login` 并把输出的授权地址交给用户;用户在浏览器确认授权,Agent 等待命令成功,再运行 `zan-saas auth status`。浏览器显示授权完成但命令失败时,按错误核查并停止,不将已有凭证状态当作本次登录成功。用户取消或授权超时后,需要继续时重新运行登录。
13
13
 
14
- 先运行 `zan-saas shop current` 查看当前店铺。已有有效上下文且用户没有要求切店时继续使用,不重复询问。用户明确指定店名时,Agent 用 `zan-saas shop list` 查找可选店铺。核对相关候选和分页后,只有店名唯一匹配时才用结果中的 `kdtId` 执行 `zan-saas shop use <kdtId>`。同名或含义不明确时,展示店名和类型让用户区分,不从名称片段推定目标。
14
+ 先读取 `zan-saas skills read zan-saas-shared`,按公共规则确定会话位置及本次任务范围。商家所说的“店铺”可能指账号下的单店或连锁,也可能指连锁内部的店铺;明确目标后区分持久切换和本次临时查询,再选择命令。
15
15
 
16
- 没有上下文且用户没有指定店铺时,展示候选店名供选择。即使只有一个候选,也简短询问“使用某某店吗?”。用户可以按店名或本次展示的序号回答,Agent 保留候选与 ID 的对应关系,无需用户理解或输入 `kdtId`。不默认选第一家,也不把未查完的一页当成全部候选。
16
+ 宿主提供稳定会话标识时,新会话首次业务使用前初始化,恢复时使用原标识及恢复模式。需要初始化、恢复、临时查询或处理覆盖时,读取 `zan-saas skills read zan-saas-shared references/shop-context.md`。已就绪且用户没有要求切换时沿用,不重复询问。
17
17
 
18
18
  店铺列表的 `total` 是员工停用过滤前的匹配总数,未知时为 `null`。`nextPageNo` 非空时,保留名称、业务线、容量及 `--all` 等选项,用 `--page-no` 继续;`hasMore=null` 表示是否还有后续未知。不要因本页候选少或为空就认定查完;`windowLimitReached=true` 时停止翻页,缩小筛选,不宣称已查全。
19
19
 
20
- 选店后用 `zan-saas shop current` 核对实际生效的店铺和来源。切店无需重新登录,浏览器中的店铺选择不改变 CLI 上下文。账号接口路由或 OAuth 地址错误时,按共享指引的[认证排查](../zan-saas-shared/references/authentication.md)核查版本、配置和服务端路由,停止后续请求。`whoami`、`shop current`、`help`、`schema`、`--dry-run`、业务请求和错误判断由 Agent 完成。
20
+ 选店后用 `zan-saas shop current` 核对实际位置和来源。宿主未提供稳定会话标识时使用本机共享选择,不能声称会话隔离。切店无需重新登录,浏览器中的店铺选择不改变 CLI 上下文。账号路由或 OAuth 地址错误按共享指引的[认证排查](../zan-saas-shared/references/authentication.md)处理。
21
21
 
22
22
  ## 选择当前能力
23
23
 
@@ -7,10 +7,10 @@
7
7
  ```markdown
8
8
  ---
9
9
  name: zan-saas
10
- description: 用户希望通过 zan-saas 查询商家业务或使用当前版本提供的操作时使用。读取当前安装 CLI 的指引,再按领域 help 和 schema 继续。
10
+ description: 有赞 SaaS 产品的命令行工具入口。用户需要查询有赞店铺的商品、订单、客户、会员、营销、库存等业务数据,或执行当前版本支持的店铺操作时使用。具体能力与适用范围以当前安装版指引为准。
11
11
  ---
12
12
 
13
- # zan-saas
13
+ # 有赞 SaaS CLI
14
14
 
15
15
  开始时运行 `zan-saas --version` 和 `zan-saas skills read zan-saas`。
16
16
  使用 CLI 返回的同版正文,再按其中的共享规则与领域指引继续。
@@ -23,3 +23,11 @@ description: 用户希望通过 zan-saas 查询商家业务或使用当前版本
23
23
  宿主侧只保存这个轻量入口,不复制整份领域知识。CLI 升级后重新读取同版指引;宿主不支持按需读取时,需要显式安装匹配版本的静态副本。
24
24
 
25
25
  重复安装只更新本项目管理且未被手工修改的入口;同名其他 Skill 保留并报错。安装不执行登录、选店或业务请求。完成后在宿主技能列表确认 `zan-saas` 已启用,再打开新会话。
26
+
27
+ ## 会话选店接入
28
+
29
+ 安装成功只表示 CLI 和入口文件已就绪。会话隔离还要求宿主从可信元数据取得稳定标识及新建/恢复状态,每次调用注入 `ZAN_SAAS_SESSION_ID`。恢复复用标识,新建生成新标识;已接入的启动器漏传标识时应拒绝业务调用。
30
+
31
+ 新会话先初始化,恢复使用 `shop session init --resume`。业务任务固定 current 或 resolve 返回的四字段 context,并在每次调用显式注入;任务结束丢弃。位置管理命令使用会话环境,不携带旧任务覆盖。具体步骤读取 `zan-saas skills read zan-saas-shared references/shop-context.md`。
32
+
33
+ 宿主未具备或尚未验证这些能力时,说明使用本机共享模式,不把入口 Skill 的安装或模型记住店铺当作会话隔离。
@@ -17,6 +17,10 @@ description: 查询当前组织资料、总部下级组织、微商城单店员
17
17
 
18
18
  组织角色描述组织在体系中的身份,员工角色关联描述授角事实,角色定义目录描述有哪些角色,员工权限点描述本次可读权限。四者分别判断,角色名称不能推算权限或可授予性;业务线也不证明店铺类型。
19
19
 
20
+ 商家在连锁内说的“店铺”通常指组织单元,先核对所属连锁和组织角色。账号层店铺入口由公共 `shop list` 发现,本域读取组织事实;当前位置由公共选店机制管理。需要内部切换、临时查询其他组织,或在分店浏览同连锁下级候选时,读取 `zan-saas skills read zan-saas-shared references/shop-context.md`。
21
+
22
+ 组织详情仍固定当前任务上下文;不能把下级 ID 直接代入原上下文的详情参数。准备其他组织的任务上下文后,所选叶子的产品、位置和权限限制仍适用。能够切到分店不扩大微商城单店员工入口的支持范围。
23
+
20
24
  ## 按目标选择命令
21
25
 
22
26
  | 用户目标 | 命令入口与读取条件 |
@@ -15,15 +15,26 @@ description: zan-saas 跨领域公共使用规则。首次调用或 CLI 版本
15
15
 
16
16
  需要登录时使用 `zan-saas auth login`,本地查看已存凭证状态使用 `zan-saas auth status`;用户要求退出时使用 `zan-saas auth logout`。先读取相应 help/schema,登录需要用户在浏览器中完成授权,退出尝试撤销当前授权服务下的个人会话并清理本机凭证。
17
17
 
18
- 认证统一由公共入口与传输层处理,领域命令不选择凭证或决定登录、续期策略。普通使用先通过 `zan-saas auth login` 获取本机个人凭证;受控环境显式注入 `ZAN_SAAS_USER_ACCESS_TOKEN` 时,实际 API 请求优先使用环境凭证,否则使用本机凭证,并在需要时自动续期。同一次命令从首个实际请求开始固定调用环境和凭证;参数拒绝、发现及预览不会触发认证。
18
+ 认证统一由公共入口与传输层处理,领域命令不选择凭证或决定登录、续期策略。普通使用通过 `zan-saas auth login` 保存个人登录,后续默认优先使用该个人凭证并按需续期;环境中存在 `ZAN_SAAS_USER_ACCESS_TOKEN` 不会覆盖已保存的个人登录。没有个人登录记录时才使用环境 token;受控调用明确要求环境身份时配置 `ZAN_SAAS_AUTH_SOURCE=env`。个人登录失效或续期被拒绝时重新登录,不自动切换环境身份。同一次命令从首个实际请求开始固定调用环境、凭证及来源;参数拒绝、发现及预览不会触发认证。
19
19
 
20
20
  缺少凭证时,交互终端可进入公共登录流程,非交互环境返回登录指引。不索取聊天中的 token,也不读取旧 `youzan-cli` 配置。`auth status` 只检查本地保存状态,不证明环境覆盖凭证有效、业务权限已获准或目标店铺匹配。登录和续期不能作为业务请求失败后的自动重放理由。
21
21
 
22
22
  需要配置、自定义授权环境、判断续期、退出结果、请求计数,或登录与选店收到 OAuth HTTP 404、账号网关 `4003` 时,读取 [公共认证与会话](references/authentication.md)。业务命令声明的请求预算计算业务 API,不包含公共认证前置;上下文与服务端业务授权仍须分别满足。
23
23
 
24
- 先用 `zan-saas shop current` 检查当前店铺;有效且用户未要求切店时沿用。需要设置店铺时,用 `zan-saas shop list` 查可选店铺,按用户明确的店名或选择映射到结果中的 ID,再执行 `zan-saas shop use <kdtId>`;名称匹配及歧义处理见下文“人机交接”。环境上下文可显式覆盖已保存的选择。
24
+ 商家说的“店铺”可能指账号下的单店或连锁,也可能指连锁内部的分店、网店。按本次明确限定、已确认任务范围、会话位置和真实候选依次核实;历史只作线索。账号层入口由 `shop list` 发现,连锁下级候选按组织领域指引发现。
25
25
 
26
- 业务调用的用户上下文由入口注入,或通过 `ZAN_SAAS_CONTEXT` 提供仅含 `userId`、`hq`、`shopId`、`orgType` 的 JSON;未设置时使用 `zan-saas shop use <kdtId>` 保存的上下文,`zan-saas shop current` 可查看当前来源。`orgType` 只取 `hq` 或 `shop`;总部时 `shopId` 等于 `hq`,`shop` 时二者可以相同,单店按 `orgType: shop` 判断可用位置。员工、店铺与总部关系仍由服务端授权,不能把本地上下文当作权限证明。凭证不放进该 JSON。
26
+ | 用户意图 | 操作 |
27
+ | --- | --- |
28
+ | 切到另一家单店或连锁,之后默认使用它 | `shop use`,进入根组织 |
29
+ | 切到当前连锁内部的店铺,之后默认使用它 | `shop unit use`,保留当前连锁根 |
30
+ | 本次查询另一家店铺 | `shop resolve` 后固定本次任务上下文 |
31
+ | 继续当前范围且未要求切换 | 沿用已确认的位置 |
32
+
33
+ 新会话首次初始化继承一次最近位置,已有会话独立保持。明确切换默认更新本会话及供未来会话继承的位置;只影响本会话时按 `--session-only` 契约处理。查询和恢复不更新最近位置。需要初始化、恢复、临时执行、组织消歧或覆盖处理时,读取 `zan-saas skills read zan-saas-shared references/shop-context.md`。
34
+
35
+ 多次 CLI 调用属于同一任务时,开始时固定四字段上下文,逐次显式传入。对象引用保留环境、所属租户及领域需要的组织范围;改变范围后按领域契约核实。当前范围查无结果或请求失败时,结论只覆盖实际查询范围;历史同名对象不能替换本次对象或触发自动跨店补查。首次继承、切换、临时查询或旧请求返回可能造成混淆时,使用“某某连锁 / 杭州店”说明实际范围。
36
+
37
+ 业务调用的用户上下文由入口注入,或通过 `ZAN_SAAS_CONTEXT` 提供仅含 `userId`、`hq`、`shopId`、`orgType` 的 JSON;否则取已初始化会话位置,无会话标识时取本机共享选择。`shop current` 查看实际来源。`hq` 表示租户根,单店为自身;`shopId` 表示当前组织;`orgType` 只取 `hq/shop`,后者为受支持的非总部位置,不能据此认定单店或分店角色。凭证不放进该 JSON,组织关系与权限仍由服务端核验。
27
38
 
28
39
  业务命令的 `at` 声明为 `hq`、`shop` 或 `all`。有有效上下文时,根与领域帮助按位置展示适用命令;无有效上下文时全量展示并提示未筛选。单条帮助始终可查,schema 始终保留完整契约。命令未展示时不自动切店或改用 Raw API;真实执行仍检查位置。
29
40
 
@@ -76,9 +87,9 @@ description: zan-saas 跨领域公共使用规则。首次调用或 CLI 版本
76
87
 
77
88
  `auth login` 会输出浏览器授权地址并等待回调。Agent 把地址交给用户,用户确认后等待命令成功,再执行 `zan-saas auth status`。浏览器授权完成只代表取得授权码;命令失败时按认证错误核查并停止,已有凭证状态不能证明本次登录完成。用户取消或五分钟超时后,需要继续时重新执行登录。
78
89
 
79
- 登录成功后先执行 `zan-saas shop current`,向用户展示当前 CLI 店铺。已有有效店铺上下文且用户未要求切店时沿用。用户明确指定店名时,Agent 查询可选店铺并核对相关候选和分页。只有店名唯一匹配时才执行 `zan-saas shop use <kdtId>`。同名或目标不明确时展示店名和类型让用户区分。没有上下文又没指定店铺时,请用户按店名或本次候选序号选择,ID 由 Agent 映射。只有一个候选时也简短确认。不默认选第一家,不要求用户理解内部 ID,不把一页唯一候选视为全账号唯一。选店后执行 `zan-saas shop current` 核对实际生效的店铺和来源。
90
+ 登录成功后按宿主生命周期初始化或恢复,再用 `shop current` 展示当前位置。无位置且用户没有指定目标时,请用户按店名或本次候选序号选择,只有一个候选也简短确认;明确名字唯一且范围无歧义时继续。ID 由 Agent 映射,不默认第一家,不把一页唯一候选视为全账号唯一。选定对象及动作后按上表执行,成功后读取实际位置。
80
91
 
81
- 切店无需重新登录,浏览器内切店不会改变 CLI 上下文。`shop use` 的 `overriddenByEnv=true` 表示只保存了选择,实际仍由 `ZAN_SAAS_CONTEXT` 覆盖;Agent 应说明当前来源,按受控环境配置处理后再核对,不声称已经切换成功。
92
+ 切店无需重新登录,浏览器内切店不会改变 CLI 上下文。会话模式的显式覆盖冲突会拒绝切换;共享模式 `overriddenByEnv=true` 仍表示只保存、未实际生效。说明实际来源,按受控配置处理后核对,不声称切换成功。
82
93
 
83
94
  列表的 `returned` 只表示本页条数,不能当作匹配总数;`pagination.total` 表示匹配查询条件的总记录数,仅在有可靠来源时提供;省略表示未知,不能用本页条数补值。提供 `nextPageNo` 和 `windowLimitReached` 的命令中,下一页码非 null 才有已确认且合法的下一页;达到窗口上限不等于数据已读完。保留原筛选和页容量,不能从其他命令类推分页上限。
84
95
 
@@ -4,11 +4,15 @@
4
4
 
5
5
  ## 配置与优先级
6
6
 
7
- 普通使用先执行 `zan-saas auth login`,读取本机保存的个人凭证。受控环境配置 `ZAN_SAAS_USER_ACCESS_TOKEN` 时,业务请求优先使用该环境凭证;否则使用本机个人凭证。默认生产 API 网关为 `https://open.youzanyun.com/api`,`ZAN_SAAS_API_BASE_URL` 可以覆盖。没有显式网关配置不等于缺少认证。
7
+ 普通使用先执行 `zan-saas auth login`。默认使用已保存的个人登录并按需续期,环境 token 不会覆盖它。仅没有个人登录记录时才使用 `ZAN_SAAS_USER_ACCESS_TOKEN`。个人登录已失效或续期被拒绝时返回重新登录指引,保留需重新登录状态,后续调用也不会悄悄换成环境身份。
8
+
9
+ `ZAN_SAAS_AUTH_SOURCE` 可取 `auto`、`personal` 或 `env`。默认 auto 采用上述优先级;personal 只使用个人登录;env 是受控调用的明确选择,只使用环境 token,缺少该 token 时失败。非法配置失败,不回退其他身份。请求发出后不会因401、权限或业务错误更换凭证重放。
10
+
11
+ 默认生产 API 网关为 `https://open.youzanyun.com/api`,`ZAN_SAAS_API_BASE_URL` 可以覆盖。没有显式网关配置不等于缺少认证。
8
12
 
9
13
  `ZAN_SAAS_CONFIG_DIR` 可以指定凭证目录;默认是 `~/.zan-saas`。`ZAN_SAAS_OAUTH_BASE_URL` 可以指定授权服务,个人凭证按授权服务 origin 分开保存。配置目录和授权服务必须与登录时一致。
10
14
 
11
- 业务命令的目标店铺可由 `--kdt-id` 显式指定;省略时取本次有效上下文的 shopId,两者都没有则失败。显式值非法时不回退。已有有效上下文且用户未要求切店时沿用;需要切店时,按用户明确的店名在可选店铺中唯一匹配,歧义时请用户选择,ID 由 Agent 映射。目标解析不证明权限,原有同店、总部及服务端授权校验仍适用。
15
+ 业务命令的目标店铺可由 `--kdt-id` 显式指定;省略时取本次有效上下文的 shopId,两者都没有则失败。显式值非法时不回退。目标解析不证明权限,原有同店、总部及服务端授权校验仍适用。会话位置、组织切换和临时目标执行按 [会话选店](shop-context.md) 处理。
12
16
 
13
17
  业务请求的店铺头优先取 `ZAN_SAAS_KDT_ID`,否则取环境或 `shop use` 保存的有效上下文 shopId;公共同店查询会绑定本次明确目标。店铺头、本地上下文与实际服务端授权不是同一件事,查询仍须通过各领域的目标范围检查。
14
18
 
@@ -16,7 +20,7 @@
16
20
 
17
21
  运行 `zan-saas auth login` 完成浏览器授权。`--no-browser` 只跳过自动打开浏览器,仍打印地址并等待手动授权回调,再交换与保存凭证;默认超时五分钟。
18
22
 
19
- 本命令成功退出后才进入选店流程:用 `zan-saas shop current` 展示当前店铺;需要首次选择或切换时,用 `zan-saas shop list` 查候选,按用户本次选择执行 `zan-saas shop use <kdtId>`,再用 `zan-saas shop current` 确认实际生效的店铺和来源。切店复用当前登录凭证,浏览器切店不会改变 CLI 保存的上下文;环境上下文覆盖时,以 `shop current` 为准。
23
+ 本命令成功退出后才接续会话初始化或恢复,再用 `shop current` 核对实际位置;缺目标时按公共选店规则补齐。切店复用当前登录凭证,浏览器切店不会改变 CLI 上下文;显式覆盖以实际来源为准。
20
24
 
21
25
  首次实际 API 请求才解析认证,并为本次命令固定调用环境与已取得凭证。help、schema、参数校验拒绝和 `--dry-run` 不读取凭证文件、不触发登录或续期、不发送业务请求。
22
26
 
@@ -38,7 +42,7 @@
38
42
 
39
43
  ## 状态与退出
40
44
 
41
- `zan-saas auth status` 只读取当前授权服务下的本机个人凭证。loggedIn 表示本地存在记录且已知刷新有效期未过期,未知有效期时保留已有登录记录;它不联网,也不判断环境 token 或服务端权限是否有效。
45
+ `zan-saas auth status` 离线查看当前授权服务下的个人登录和凭证选择。loggedIn 表示本地个人记录未要求重新登录且已知刷新有效期未过期;credentialSource 分别以 login/env/missing 表示按配置选择个人、环境或没有凭证。environmentTokenIgnored 为 true 表示个人登录优先,忽略了同时存在的环境 token。状态不联网,不证明实际凭证或服务端权限有效。
42
46
 
43
47
  `zan-saas auth logout` 尝试撤销当前授权服务下保存的个人会话,并清除对应本机凭证和已选店铺。它不清除或撤销环境注入的 token,不退出其他授权服务下的个人会话。没有本机凭证时返回 revoked=false,不表示外部凭证已失效。
44
48
 
@@ -48,5 +52,6 @@
48
52
  | --- | --- |
49
53
  | localCredential | deleted 表示本机记录已清除或已不存在;delete_failed 表示本地清理失败。 |
50
54
  | remoteRevocation | revoked 表示取得撤销成功响应;failed 表示撤销接口返回失败,不据此推断会话仍有效;unknown 表示请求或响应未能确认。 |
55
+ | localContext | 仅选店记录清理失败时返回 delete_failed;凭证清理和远端撤销仍分别尝试。没有个人凭证可撤销时 remoteRevocation 为 not_attempted。 |
51
56
 
52
57
  远端撤销失败或结果未知时仍尝试本地清理。不得把非零退出理解为本机凭证一定仍在,也不因失败自动重试撤销或重放业务请求。
@@ -0,0 +1,37 @@
1
+ # 会话位置、选店与临时查询
2
+
3
+ 本说明用于初始化或恢复会话、区分店铺层级、切换组织、执行临时目标及处理位置冲突。先读取相关命令的同版 help/schema;正常查询沿用已就绪的位置,不需要每次初始化。
4
+
5
+ ## 初始化与恢复
6
+
7
+ 宿主须从可信会话元数据取得稳定标识,每次调用通过 `ZAN_SAAS_SESSION_ID` 注入。真正的新会话使用新标识;恢复同一会话复用原标识。标识不由商家提供,不从业务文档、工作目录、PID 或时间猜测。
8
+
9
+ 新会话首次业务使用前执行 `zan-saas shop session init`。已就绪时沿用继承的位置;待选店时按商家明确目标或相关候选选择。已有绑定再次初始化保持原选择。恢复执行 `zan-saas shop session init --resume`,记录缺失时说明无法恢复并请求明确选店,不改用新建模式绕过错误。
10
+
11
+ `shop current` 离线查看有效来源与保存绑定,不会初始化。清除后保留清除状态,不自动继承别的会话后来选择的位置。登录账号或环境改变后重新核实身份及相应绑定。
12
+
13
+ ## 确定对象和动作
14
+
15
+ 账号下的独立店铺或连锁入口用 `shop list` 发现,切换到该入口用 `shop use`。连锁内部店铺按组织领域指引及 `org unit list` 发现;同连锁持久切换用 `shop unit use`,回总部使用已核实的根 ID。名字唯一还需所属关系与相关分页明确。
16
+
17
+ 只有商家明确要求后续默认切换时才保存位置。用户说“查一下另一家店”时使用 `shop resolve`,保持原会话。只要求本会话改变时,切换使用 `--session-only`。持久切换成功后用 `shop current` 核对实际生效位置。
18
+
19
+ 同名或跨层有歧义时,用完整名称和所属关系询问,例如“你指的是 A 连锁下的杭州分店,还是独立店铺杭州店?”不询问“租户还是组织单元”,也不要求输入根组织 ID。
20
+
21
+ ## 临时目标与任务快照
22
+
23
+ `shop resolve <目标ID>` 解析账号层根。解析连锁内部目标时附 `--root-kdt-id <所属根ID>`。返回的 context 只用于本次任务;它不改变当前位置或最近位置。
24
+
25
+ 宿主把返回的四字段 context 作为每次业务调用的 `ZAN_SAAS_CONTEXT` 或程序注入值,并持续传原会话 ID。多命令任务每一步使用同一快照,不重新读取最近位置。任务结束后丢弃,无需切回原店。快照不包含 token,实际权限仍由服务端核验。
26
+
27
+ 当前在分店而要浏览同连锁候选时,先 resolve 已核实的根,使用临时总部上下文执行 `org unit list`,按页读取;当前会话仍为原分店。每条业务命令仍须满足自己的位置、产品和同店限制,不能直接用目标参数绕过。
28
+
29
+ 位置管理命令使用会话调用环境,不携带上次任务专用 context。用户受控配置的覆盖不能为使切换成功而擅自删除。
30
+
31
+ 切换期间已经发出的请求按原目标解释。迟到结果标明原店铺;新范围的后续步骤重新核验对象。查无结果、权限拒绝或未知结果不能用历史其他店铺的数据补答案。
32
+
33
+ ## 覆盖、冲突及共享模式
34
+
35
+ 实际来源以 `shop current` 为准。显式上下文无效不回退;会话模式显式上下文或请求头与切换目标冲突时拒绝保存。并发版本冲突先读取当前位置,再根据仍有效的明确意图处理,不自动重放业务操作。
36
+
37
+ 没有宿主标识时使用本机共享选店,其他会话可能影响下一次调用,不能声称会话隔离。临时查询仍可 resolve 后显式传入任务上下文。内部持久切换需要会话模式;安装入口文件不会自动提供稳定会话标识和逐次注入能力。
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ const { createHash } = require('node:crypto');
4
+ const { AuthConfigurationError } = require('./config');
5
+
6
+ function authSource(env) {
7
+ const source = env.ZAN_SAAS_AUTH_SOURCE ?? 'auto';
8
+ if (!['auto', 'personal', 'env'].includes(source)) {
9
+ throw new AuthConfigurationError('ZAN_SAAS_AUTH_SOURCE 只接受 auto、personal 或 env。');
10
+ }
11
+ return source;
12
+ }
13
+
14
+ function credentialChoice(env, credential) {
15
+ const mode = authSource(env);
16
+ if (mode !== 'env' && credential) {
17
+ return { source: 'login', credentialRef: `login:${credential.generation || credential.loginAt || 'legacy'}` };
18
+ }
19
+ if (mode !== 'personal' && env.ZAN_SAAS_USER_ACCESS_TOKEN) {
20
+ const hash = createHash('sha256').update(env.ZAN_SAAS_USER_ACCESS_TOKEN).digest('hex');
21
+ return { source: 'env', credentialRef: `env:${hash}` };
22
+ }
23
+ return { source: 'missing', credentialRef: null };
24
+ }
25
+
26
+ module.exports = { authSource, credentialChoice };
@@ -3,7 +3,8 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const crypto = require('node:crypto');
6
- const { configDir } = require('./config');
6
+ const { configDir, AuthConfigurationError } = require('./config');
7
+ const { credentialChoice } = require('./credential-source');
7
8
 
8
9
  const fileName = 'credentials.json';
9
10
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
@@ -53,16 +54,70 @@ const readAll = env => readStore(fileName, env);
53
54
  const writeAll = (data, env) => writeStore(fileName, data, env);
54
55
 
55
56
  // Credentials are keyed by oauth origin so logins to different environments do not overwrite each other.
57
+ function credentialDocument(env) {
58
+ let data;
59
+ let descriptor;
60
+ try {
61
+ descriptor = fs.openSync(credentialPath(env), 'r');
62
+ const stat = fs.fstatSync(descriptor);
63
+ data = JSON.parse(fs.readFileSync(descriptor, 'utf8'));
64
+ if (!data || typeof data !== 'object' || Array.isArray(data)) throw new Error();
65
+ return { data, metadata: { mtimeMs: stat.mtimeMs, size: stat.size, ino: stat.ino, dev: stat.dev } };
66
+ } catch (error) {
67
+ if (error.code === 'ENOENT') return { data: {}, metadata: null };
68
+ throw new AuthConfigurationError('个人凭证文件损坏或不可读,请重新登录;未改用环境身份。');
69
+ } finally {
70
+ if (descriptor !== undefined) fs.closeSync(descriptor);
71
+ }
72
+ }
73
+
56
74
  function readCredential(server, env = process.env) {
57
- const entry = readAll(env)[server];
58
- return entry && typeof entry.accessToken === 'string' && typeof entry.refreshToken === 'string'
59
- ? entry : null;
75
+ const { data } = credentialDocument(env);
76
+ if (!Object.hasOwn(data, server)) return null;
77
+ const entry = data[server];
78
+ if (!entry || typeof entry.accessToken !== 'string' || !entry.accessToken ||
79
+ typeof entry.refreshToken !== 'string' || !entry.refreshToken) {
80
+ throw new AuthConfigurationError('个人凭证记录不完整,请重新登录;未改用环境身份。');
81
+ }
82
+ return entry;
83
+ }
84
+
85
+ function syncCredentialPresence(env = process.env) {
86
+ const filename = 'credential-state.json';
87
+ const { data, metadata } = credentialDocument(env);
88
+ const records = Object.fromEntries(Object.entries(data).map(([server, credential]) => [server, {
89
+ present: true,
90
+ credentialRef: credential && typeof credential.accessToken === 'string' &&
91
+ typeof credential.refreshToken === 'string'
92
+ ? credentialChoice({ ZAN_SAAS_AUTH_SOURCE: 'personal' }, credential).credentialRef : 'login:pending'
93
+ }]));
94
+ const next = { schemaVersion: 1, metadata, records };
95
+ if (JSON.stringify(readStore(filename, env)) === JSON.stringify(next)) return;
96
+ writeStore(filename, next, env);
97
+ }
98
+
99
+ // Offline context selection reads only this non-secret index and file metadata.
100
+ function credentialPresence(server, env = process.env) {
101
+ const stat = fs.existsSync(credentialPath(env)) ? fs.statSync(credentialPath(env)) : null;
102
+ if (!stat) return { present: false, credentialRef: null };
103
+ const index = readStore('credential-state.json', env);
104
+ const metadata = index.metadata;
105
+ if (index.schemaVersion === 1 && metadata && metadata.mtimeMs === stat.mtimeMs &&
106
+ metadata.size === stat.size && metadata.ino === stat.ino && metadata.dev === stat.dev &&
107
+ index.records && typeof index.records === 'object' && !Array.isArray(index.records)) {
108
+ if (!Object.hasOwn(index.records, server)) return { present: false, credentialRef: null };
109
+ const entry = index.records[server];
110
+ if (entry?.present === true && typeof entry.credentialRef === 'string' &&
111
+ entry.credentialRef.startsWith('login:')) return entry;
112
+ }
113
+ return { present: true, credentialRef: 'login:pending' };
60
114
  }
61
115
 
62
116
  function saveCredential(server, credential, env = process.env) {
63
117
  const data = readAll(env);
64
118
  data[server] = credential;
65
119
  writeAll(data, env);
120
+ syncCredentialPresence(env);
66
121
  }
67
122
 
68
123
  function deleteCredential(server, env = process.env) {
@@ -70,6 +125,7 @@ function deleteCredential(server, env = process.env) {
70
125
  if (!Object.hasOwn(data, server)) return false;
71
126
  delete data[server];
72
127
  writeAll(data, env);
128
+ syncCredentialPresence(env);
73
129
  return true;
74
130
  }
75
131
 
@@ -105,5 +161,5 @@ async function withFileLock(env, task, { staleMs = 30000, waitMs = 15000 } = {})
105
161
 
106
162
  module.exports = {
107
163
  credentialPath, deleteCredential, readCredential, readStore, saveCredential, storePath, withFileLock,
108
- writeStore
164
+ writeStore, credentialPresence, syncCredentialPresence
109
165
  };