@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.
- package/bin/zan-saas-cli.js +8 -11
- package/internal/registry/manifest.json +11 -10
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/zan-saas/SKILL.md +5 -5
- package/skills/zan-saas/references/host-entry.md +10 -2
- package/skills/zan-saas-org/SKILL.md +4 -0
- package/skills/zan-saas-shared/SKILL.md +16 -5
- package/skills/zan-saas-shared/references/authentication.md +9 -4
- package/skills/zan-saas-shared/references/shop-context.md +37 -0
- package/src/auth/credential-source.js +26 -0
- package/src/auth/credential-store.js +61 -5
- package/src/auth/session.js +80 -27
- package/src/cli/command-paths.js +3 -2
- package/src/cli/program.js +20 -4
- package/src/commands/native/access/commands.index.json +1 -1
- package/src/commands/native/benefit/card-definition/list/command.js +4 -3
- package/src/commands/native/benefit/card-definition/list/schema.json +1 -1
- package/src/commands/native/benefit/commands.index.json +2 -2
- package/src/commands/native/benefit/customer-card/list/command.js +1 -0
- package/src/commands/native/benefit/customer-card/list/schema.json +1 -1
- package/src/commands/native/customer/commands.index.json +1 -1
- package/src/commands/native/customer/customer-tag/list/command.js +2 -2
- package/src/commands/native/member/commands.index.json +1 -1
- package/src/commands/native/org/commands.index.json +4 -4
- package/src/commands/system/api.js +1 -1
- package/src/commands/system/auth.js +13 -2
- package/src/commands/system/contracts.js +27 -19
- package/src/commands/system/shop-contracts.js +66 -0
- package/src/commands/system/shop.js +128 -27
- package/src/identity/context-store.js +14 -10
- package/src/identity/file-store.js +147 -0
- package/src/identity/session-store.js +261 -0
- package/src/identity/shop-location.js +153 -0
- package/src/transport/api.js +78 -11
- package/src/transport/shop-app.js +8 -1
package/src/auth/session.js
CHANGED
|
@@ -8,6 +8,8 @@ const { openBrowser } = require('./browser');
|
|
|
8
8
|
const { OAuthError, exchangeCode, refreshTokens, revokeToken } = require('./oauth-client');
|
|
9
9
|
const store = require('./credential-store');
|
|
10
10
|
const { deleteContext } = require('../identity/context-store');
|
|
11
|
+
const { randomUUID } = require('node:crypto');
|
|
12
|
+
const { authSource, credentialChoice } = require('./credential-source');
|
|
11
13
|
|
|
12
14
|
const dayMs = 24 * 60 * 60 * 1000;
|
|
13
15
|
|
|
@@ -29,7 +31,8 @@ function summary(server, credential, now = Date.now()) {
|
|
|
29
31
|
if (!credential) return { loggedIn: false, server };
|
|
30
32
|
const iso = value => (Number.isFinite(value) ? new Date(value).toISOString() : null);
|
|
31
33
|
return {
|
|
32
|
-
loggedIn:
|
|
34
|
+
loggedIn: !credential.reauthRequired &&
|
|
35
|
+
(Number.isFinite(credential.refreshExpiresAt) ? credential.refreshExpiresAt > now : true),
|
|
33
36
|
server,
|
|
34
37
|
clientId: credential.clientId,
|
|
35
38
|
tool: credential.tool,
|
|
@@ -43,6 +46,7 @@ async function login({
|
|
|
43
46
|
env = process.env, browser = true, port = defaultPort, log = message => process.stderr.write(`${message}\n`),
|
|
44
47
|
open = openBrowser, timeoutMs
|
|
45
48
|
} = {}) {
|
|
49
|
+
authSource(env);
|
|
46
50
|
const config = oauthConfig(env);
|
|
47
51
|
const pkce = createPkce();
|
|
48
52
|
const tool = detectTool(env);
|
|
@@ -72,38 +76,66 @@ async function login({
|
|
|
72
76
|
throw error;
|
|
73
77
|
}
|
|
74
78
|
const credential = {
|
|
75
|
-
...tokens, clientId: config.clientId, oauthBaseUrl: config.oauthBaseUrl, tool, loginAt: Date.now()
|
|
79
|
+
...tokens, clientId: config.clientId, oauthBaseUrl: config.oauthBaseUrl, tool, loginAt: Date.now(),
|
|
80
|
+
generation: randomUUID()
|
|
76
81
|
};
|
|
77
|
-
store.saveCredential(config.server, credential, env);
|
|
78
|
-
|
|
82
|
+
await store.withFileLock(env, async () => store.saveCredential(config.server, credential, env));
|
|
83
|
+
require('../identity/session-store').resetIdentity(env, {
|
|
84
|
+
pendingCredentialRef: credentialChoice({ ...env, ZAN_SAAS_AUTH_SOURCE: 'personal' }, credential).credentialRef
|
|
85
|
+
});
|
|
86
|
+
return status({ env });
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
async function logout({ env = process.env } = {}) {
|
|
82
90
|
const config = oauthConfig(env);
|
|
83
91
|
const credential = store.readCredential(config.server, env);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
let localContext = 'deleted';
|
|
93
|
+
for (const clear of [
|
|
94
|
+
() => deleteContext(env),
|
|
95
|
+
() => require('../identity/session-store').resetIdentity(env, { logout: true })
|
|
96
|
+
]) {
|
|
97
|
+
try { clear(); } catch { localContext = 'delete_failed'; }
|
|
98
|
+
}
|
|
99
|
+
if (!credential && localContext === 'deleted') return { loggedOut: true, revoked: false, server: config.server };
|
|
100
|
+
let remoteRevocation = credential ? 'revoked' : 'not_attempted';
|
|
88
101
|
let httpStatus;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
if (credential) {
|
|
103
|
+
try {
|
|
104
|
+
await revokeToken(config, credential.refreshToken);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
httpStatus = error instanceof OAuthError ? error.httpStatus : undefined;
|
|
107
|
+
remoteRevocation = httpStatus === undefined ? 'unknown' : 'failed';
|
|
108
|
+
}
|
|
94
109
|
}
|
|
95
110
|
let localCredential = 'deleted';
|
|
111
|
+
let loginChanged = false;
|
|
96
112
|
try {
|
|
97
|
-
store.
|
|
113
|
+
await store.withFileLock(env, async () => {
|
|
114
|
+
const current = store.readCredential(config.server, env);
|
|
115
|
+
const sameLogin = !current || (credential?.generation
|
|
116
|
+
? current.generation === credential.generation
|
|
117
|
+
: current.loginAt === credential?.loginAt && current.refreshToken === credential?.refreshToken);
|
|
118
|
+
if (!sameLogin) {
|
|
119
|
+
loginChanged = true;
|
|
120
|
+
throw new Error('The current login changed during logout');
|
|
121
|
+
}
|
|
122
|
+
store.deleteCredential(config.server, env);
|
|
123
|
+
});
|
|
98
124
|
} catch {
|
|
99
125
|
localCredential = 'delete_failed';
|
|
100
126
|
}
|
|
101
|
-
if (
|
|
102
|
-
|
|
127
|
+
if (!['revoked', 'not_attempted'].includes(remoteRevocation) ||
|
|
128
|
+
localCredential !== 'deleted' || localContext !== 'deleted') {
|
|
129
|
+
const error = new Error(loginChanged
|
|
130
|
+
? '退出期间已产生新登录,已保留新凭证;旧凭证的服务端撤销结果请按退出信息核对。'
|
|
131
|
+
: localContext !== 'deleted'
|
|
132
|
+
? '选店记录清理未完成;本机凭证清理与服务端撤销已分别尝试,请按退出结果核对。'
|
|
133
|
+
: localCredential === 'deleted'
|
|
103
134
|
? '本机个人凭证已清除,服务端会话撤销未确认;请核对授权服务状态。'
|
|
104
135
|
: '本机个人凭证清理失败;请按退出结果核对本地凭证与服务端会话。');
|
|
105
136
|
error.cliDetails = {
|
|
106
137
|
command: 'auth.logout', server: config.server, localCredential, remoteRevocation,
|
|
138
|
+
...(localContext === 'deleted' ? {} : { localContext }),
|
|
107
139
|
...(httpStatus === undefined ? {} : { httpStatus })
|
|
108
140
|
};
|
|
109
141
|
throw error;
|
|
@@ -113,7 +145,10 @@ async function logout({ env = process.env } = {}) {
|
|
|
113
145
|
|
|
114
146
|
function status({ env = process.env } = {}) {
|
|
115
147
|
const { server } = oauthConfig(env);
|
|
116
|
-
|
|
148
|
+
const credential = store.readCredential(server, env);
|
|
149
|
+
const choice = credentialChoice(env, credential);
|
|
150
|
+
return { ...summary(server, credential), credentialSource: choice.source,
|
|
151
|
+
environmentTokenIgnored: choice.source === 'login' && Boolean(env.ZAN_SAAS_USER_ACCESS_TOKEN) };
|
|
117
152
|
}
|
|
118
153
|
|
|
119
154
|
function interactive(env) {
|
|
@@ -127,17 +162,24 @@ async function refreshIfNeeded(config, env, now) {
|
|
|
127
162
|
// Another process may have refreshed while this one waited for the lock.
|
|
128
163
|
const credential = store.readCredential(key, env);
|
|
129
164
|
if (!credential) return null;
|
|
130
|
-
if (
|
|
131
|
-
if (credential.refreshExpiresAt <= now) {
|
|
132
|
-
store.deleteCredential(key, env);
|
|
165
|
+
if (credential.reauthRequired || credential.refreshExpiresAt <= now) {
|
|
133
166
|
return null;
|
|
134
167
|
}
|
|
168
|
+
if (!(credential.accessExpiresAt - now < dayMs)) return credential;
|
|
169
|
+
const assertUnchanged = () => {
|
|
170
|
+
const current = store.readCredential(key, env);
|
|
171
|
+
if (!current || current.generation !== credential.generation ||
|
|
172
|
+
current.refreshToken !== credential.refreshToken) {
|
|
173
|
+
throw new AuthRequiredError('个人登录在续期期间改变,请核对当前身份后重试;未改用环境身份。');
|
|
174
|
+
}
|
|
175
|
+
};
|
|
135
176
|
let tokens;
|
|
136
177
|
try {
|
|
137
178
|
tokens = await refreshTokens(config, credential.refreshToken);
|
|
138
179
|
} catch (error) {
|
|
139
180
|
if (error instanceof OAuthError && error.oauthError === 'invalid_grant') {
|
|
140
|
-
|
|
181
|
+
assertUnchanged();
|
|
182
|
+
store.saveCredential(key, { ...credential, reauthRequired: true }, env);
|
|
141
183
|
return null;
|
|
142
184
|
}
|
|
143
185
|
// Keep using an access token that has not expired yet when refresh is unreachable.
|
|
@@ -145,20 +187,29 @@ async function refreshIfNeeded(config, env, now) {
|
|
|
145
187
|
throw error;
|
|
146
188
|
}
|
|
147
189
|
const next = { ...credential, ...tokens };
|
|
190
|
+
assertUnchanged();
|
|
148
191
|
store.saveCredential(key, next, env);
|
|
149
192
|
return next;
|
|
150
193
|
});
|
|
151
194
|
}
|
|
152
195
|
|
|
153
|
-
//
|
|
154
|
-
// existing controlled environments behave as before.
|
|
196
|
+
// Select once before a request; a failed personal login never changes to another identity.
|
|
155
197
|
async function resolveAccessToken(env = process.env, { now = Date.now(), loginImpl = login } = {}) {
|
|
156
|
-
|
|
157
|
-
|
|
198
|
+
const mode = authSource(env);
|
|
199
|
+
if (mode === 'env') {
|
|
200
|
+
if (!env.ZAN_SAAS_USER_ACCESS_TOKEN) throw new AuthRequiredError('环境凭证模式缺少 ZAN_SAAS_USER_ACCESS_TOKEN。');
|
|
201
|
+
return Object.freeze({ token: env.ZAN_SAAS_USER_ACCESS_TOKEN, ...credentialChoice(env) });
|
|
158
202
|
}
|
|
159
203
|
const config = oauthConfig(env);
|
|
160
204
|
let credential = store.readCredential(config.server, env);
|
|
161
|
-
|
|
205
|
+
const choice = credentialChoice(env, credential);
|
|
206
|
+
if (choice.source === 'env') {
|
|
207
|
+
store.syncCredentialPresence(env);
|
|
208
|
+
return Object.freeze({ token: env.ZAN_SAAS_USER_ACCESS_TOKEN, ...choice });
|
|
209
|
+
}
|
|
210
|
+
if (credential?.reauthRequired || credential?.refreshExpiresAt <= now) {
|
|
211
|
+
credential = null;
|
|
212
|
+
} else if (credential && Number.isFinite(credential.accessExpiresAt) &&
|
|
162
213
|
credential.accessExpiresAt - now < dayMs) {
|
|
163
214
|
credential = await refreshIfNeeded(config, env, now);
|
|
164
215
|
}
|
|
@@ -170,7 +221,9 @@ async function resolveAccessToken(env = process.env, { now = Date.now(), loginIm
|
|
|
170
221
|
credential = store.readCredential(config.server, env);
|
|
171
222
|
if (!credential) throw new AuthRequiredError('Login did not produce a credential');
|
|
172
223
|
}
|
|
173
|
-
|
|
224
|
+
store.syncCredentialPresence(env);
|
|
225
|
+
return Object.freeze({ token: credential.accessToken, ...credentialChoice(
|
|
226
|
+
{ ...env, ZAN_SAAS_AUTH_SOURCE: 'personal' }, credential) });
|
|
174
227
|
}
|
|
175
228
|
|
|
176
229
|
module.exports = {
|
package/src/cli/command-paths.js
CHANGED
|
@@ -101,12 +101,13 @@ function commandContract(path, command) {
|
|
|
101
101
|
if (['schema', 'doctor', 'skills.list', 'skills.read'].includes(path)) {
|
|
102
102
|
return { kind: 'system', risk: 'read', availability: 'available' };
|
|
103
103
|
}
|
|
104
|
-
if (['auth.status', 'auth.whoami', 'shop.list', 'shop.current'].includes(path)) {
|
|
104
|
+
if (['auth.status', 'auth.whoami', 'shop.list', 'shop.current', 'shop.resolve'].includes(path)) {
|
|
105
105
|
return { kind: 'system', risk: 'read', availability: 'available' };
|
|
106
106
|
}
|
|
107
107
|
// Login and logout change the local credential and the server-side session; shop use and clear
|
|
108
108
|
// change the saved context.
|
|
109
|
-
if (['install', 'auth.login', 'auth.logout', 'shop.use', 'shop.clear'
|
|
109
|
+
if (['install', 'auth.login', 'auth.logout', 'shop.use', 'shop.clear',
|
|
110
|
+
'shop.session.init', 'shop.unit.use'].includes(path)) {
|
|
110
111
|
return { kind: 'system', risk: 'low', availability: 'available' };
|
|
111
112
|
}
|
|
112
113
|
if (path === 'api') return { kind: 'raw-api', risk: 'unknown', availability: 'available' };
|
package/src/cli/program.js
CHANGED
|
@@ -22,9 +22,16 @@ const { commandSchema } = require('../execution/command-schema');
|
|
|
22
22
|
const { commandContract, commandPath } = require('./command-paths');
|
|
23
23
|
const { configureContextHelp, isContextHelp } = require('./context-help');
|
|
24
24
|
const { configureParserPolicy } = require('./parser-policy');
|
|
25
|
+
const { bindApiContext, runWithApiSession } = require('../transport/api');
|
|
25
26
|
|
|
26
|
-
function createProgram(
|
|
27
|
-
|
|
27
|
+
function createProgram(settings = {}) {
|
|
28
|
+
const { catalog = openRegistry(), root = nativeRoot, rootSkills = skillsRoot,
|
|
29
|
+
eagerDomains = false } = settings;
|
|
30
|
+
const injected = settings.context !== undefined;
|
|
31
|
+
let context;
|
|
32
|
+
let contextError;
|
|
33
|
+
try { context = injected ? settings.context : contextInput(process.env); }
|
|
34
|
+
catch (error) { contextError = error; context = {}; }
|
|
28
35
|
const { domains, skills, businessCommandCount } = catalog.manifest;
|
|
29
36
|
const domainNames = new Set(domains.map(domain => domain.name));
|
|
30
37
|
const loadedDomains = new Set();
|
|
@@ -62,7 +69,7 @@ function createProgram({ catalog = openRegistry(), root = nativeRoot, rootSkills
|
|
|
62
69
|
program.addCommand(createDoctorCommand(businessCommandCount));
|
|
63
70
|
program.addCommand(createApiCommand());
|
|
64
71
|
program.addCommand(createAuthCommand());
|
|
65
|
-
program.addCommand(createShopCommand());
|
|
72
|
+
program.addCommand(createShopCommand(undefined, { context: injected ? context : undefined }));
|
|
66
73
|
program.addCommand(createSkillsCommand(skills, rootSkills));
|
|
67
74
|
program.addCommand(createInstallCommand());
|
|
68
75
|
function configureSystemErrors(command) {
|
|
@@ -83,6 +90,11 @@ function createProgram({ catalog = openRegistry(), root = nativeRoot, rootSkills
|
|
|
83
90
|
program.hook('preAction', (_parent, command) => {
|
|
84
91
|
if (isContextHelp(command)) return;
|
|
85
92
|
const contract = commandContract(commandPath(command), command);
|
|
93
|
+
if ((['atomic', 'task'].includes(contract.kind) && !command.opts().dryRun) ||
|
|
94
|
+
contract.kind === 'raw-api') {
|
|
95
|
+
if (contextError) throw contextError;
|
|
96
|
+
bindApiContext(contextState.context);
|
|
97
|
+
}
|
|
86
98
|
if (['atomic', 'task'].includes(contract.kind)) {
|
|
87
99
|
const details = commandSchema(command)?.error?.properties?.details;
|
|
88
100
|
const stages = details?.properties?.stage?.enum;
|
|
@@ -112,7 +124,7 @@ function createProgram({ catalog = openRegistry(), root = nativeRoot, rootSkills
|
|
|
112
124
|
快速开始:
|
|
113
125
|
1. 登录:zan-saas auth login,按提示在浏览器完成授权
|
|
114
126
|
2. 查看当前店铺:zan-saas shop current;已有有效上下文时沿用
|
|
115
|
-
3.
|
|
127
|
+
3. 宿主会话先 shop session init;账号选店用 shop use,连锁内切换用 shop unit use,临时查询用 shop resolve
|
|
116
128
|
4. 阅读使用指引:zan-saas skills read zan-saas
|
|
117
129
|
|
|
118
130
|
查询命令:
|
|
@@ -128,6 +140,10 @@ function createProgram({ catalog = openRegistry(), root = nativeRoot, rootSkills
|
|
|
128
140
|
if (eagerDomains) {
|
|
129
141
|
for (const domain of domains) loadDomain(domain.name);
|
|
130
142
|
}
|
|
143
|
+
const parseAsync = program.parseAsync;
|
|
144
|
+
program.parseAsync = function (...args) {
|
|
145
|
+
return runWithApiSession(() => parseAsync.apply(this, args));
|
|
146
|
+
};
|
|
131
147
|
return program;
|
|
132
148
|
}
|
|
133
149
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"availability": "available",
|
|
10
10
|
"at": "all",
|
|
11
11
|
"module": "access/role/list/command.js",
|
|
12
|
-
"fingerprint": "
|
|
12
|
+
"fingerprint": "55180dff4d111eee0ae67c4987b71a5ebe0557061228774a5cc9f0b88880d8aa"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"path": "access.staff-perms.get",
|
|
@@ -64,14 +64,15 @@ function createCommand({ context } = {}) {
|
|
|
64
64
|
.choices(['json', 'pretty']).default('json'))
|
|
65
65
|
.option('--dry-run', '本地预览店铺核验和单页查询,不读凭证、不联网')
|
|
66
66
|
.addHelpText('after', `
|
|
67
|
-
total 只描述本次筛选的接口总数,不保证跨页快照;按 nextPageNo
|
|
67
|
+
total 只描述本次筛选的接口总数,不保证跨页快照;按 nextPageNo 继续并保留店铺和筛选。
|
|
68
68
|
nextPageNo=null 不能单独解释为结束;windowLimitReached=true 表示达到页码上限,仍有后续。
|
|
69
69
|
时间点统一输出北京时间 YYYY-MM-DD HH:mm:ss.SSS,字段声明的业务哨兵保留。
|
|
70
|
-
输出 data.cards 和 data.pagination(含 total、nextPageNo、windowLimitReached
|
|
70
|
+
输出 data.cards 和 data.pagination(含 total、nextPageNo、windowLimitReached),不自动翻页或重试。
|
|
71
|
+
分页仅支持 --page-no;页容量由服务端决定,不支持 --page-size。
|
|
71
72
|
仅查询商家卡定义,不表示客户持卡、发卡资格或详情权益;不按名称、类型或状态过滤。
|
|
72
73
|
hasMore 表示是否还有下一页;空页或短页不表示耗尽,不承诺跨页快照。
|
|
73
74
|
服务端最多允许 100 页;第 100 页仍有 hasMore=true 时不能继续遍历,不请求第 101 页。
|
|
74
|
-
不提供 --name
|
|
75
|
+
不提供 --name 或 scan;最多一次店铺核验与一次业务请求。
|
|
75
76
|
真实执行需有效四字段上下文和同店授权凭证;店铺核验不证明员工 OAuth。
|
|
76
77
|
支持微商城单店、零售单店、有赞连锁 D 总部/网店/合伙人,不承诺教育多校区总部。
|
|
77
78
|
契约:zan-saas schema benefit.card-definition.list
|
|
@@ -680,7 +680,7 @@
|
|
|
680
680
|
"diagnosticsVersion": 2
|
|
681
681
|
},
|
|
682
682
|
"notes": [
|
|
683
|
-
"每次只查询一页权益卡定义,默认第 1 页,最大第 100
|
|
683
|
+
"每次只查询一页权益卡定义,默认第 1 页,最大第 100 页;页容量由服务端决定,不支持 --page-size。保留返回顺序和禁用卡。",
|
|
684
684
|
"pageSize 为本次实际页容量,returned 为本次卡定义条数;不按空页或短页判断结束。第 100 页仍有更多时不能继续第 101 页,也不能宣称全量。",
|
|
685
685
|
"可选字段缺失保持省略,已选字段 null 或错型失败;保留空字符串和 false。",
|
|
686
686
|
"支持微商城单店、零售单店、连锁 D 总部、网店及合伙人,不承诺跨页快照。",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"availability": "available",
|
|
19
19
|
"at": "all",
|
|
20
20
|
"module": "benefit/card-definition/list/command.js",
|
|
21
|
-
"fingerprint": "
|
|
21
|
+
"fingerprint": "6e1e404d0773b8360796d76c850f5913b0db5cf17556625bccb3bc1a90a4ef48"
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"path": "benefit.card-grant.list",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"availability": "available",
|
|
46
46
|
"at": "all",
|
|
47
47
|
"module": "benefit/customer-card/list/command.js",
|
|
48
|
-
"fingerprint": "
|
|
48
|
+
"fingerprint": "99950d67fbfed296117eb6ad44be1fb6bae4875128847757ebd958c38a570189"
|
|
49
49
|
}
|
|
50
50
|
]
|
|
51
51
|
}
|
|
@@ -66,6 +66,7 @@ function createCommand({ context } = {}) {
|
|
|
66
66
|
.addHelpText('after', `
|
|
67
67
|
当前接口未提供可靠的匹配总数,因此省略 total,hasMore 为 null、nextPageNo 为 null,不自动续页。
|
|
68
68
|
返回 data.customerCards 和 data.pagination;returned 为本次客户持卡条数。
|
|
69
|
+
分页仅支持 --page-no;页容量由服务端决定,不支持 --page-size。
|
|
69
70
|
cardNo 是完整字符串实例号;cardAlias 仅关联定义,不按定义合并持卡。
|
|
70
71
|
保留源 cardState;时间点统一为北京时间 YYYY-MM-DD HH:mm:ss.SSS,包括 9999 年永久哨兵。
|
|
71
72
|
空页不证明耗尽;all 不保证删除历史完整或跨页快照。
|
|
@@ -721,7 +721,7 @@
|
|
|
721
721
|
"diagnosticsVersion": 2
|
|
722
722
|
},
|
|
723
723
|
"notes": [
|
|
724
|
-
"固定一个店铺和客户,每次读取一页持卡实例,默认第 1
|
|
724
|
+
"固定一个店铺和客户,每次读取一页持卡实例,默认第 1 页;页内卡号唯一,页容量由服务端决定,不支持 --page-size。",
|
|
725
725
|
"--status available 查询可用卡,all 查询返回范围内所有状态;all 不保证已删除历史完整或跨页快照。",
|
|
726
726
|
"只返回模型中的五个实例字段,不返回客户资料,不按日期推算状态;缺必需字段、null 或错型整体失败。",
|
|
727
727
|
"支持微商城单店、零售单店、连锁 D 总部、网店及合伙人。",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"availability": "available",
|
|
46
46
|
"at": "all",
|
|
47
47
|
"module": "customer/customer-tag/list/command.js",
|
|
48
|
-
"fingerprint": "
|
|
48
|
+
"fingerprint": "9847ca5a4011dc084d074f434c0cd4ad24633e12fe6f79395a1dfb173927bd0b"
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
"path": "customer.segment.list",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { withTargetShopDefault } = require('../../../../../execution/target-shop-default');
|
|
4
4
|
|
|
5
5
|
const { Command } = require('commander');
|
|
6
|
-
const { requestApi, ApiConfigurationError } = require('../../../../../transport/api');
|
|
6
|
+
const { requestApi, snapshotApiEnvironment, ApiConfigurationError } = require('../../../../../transport/api');
|
|
7
7
|
const { validateCommandValue } = require('../../../../../execution/command-schema');
|
|
8
8
|
const schema = require('./schema.json');
|
|
9
9
|
|
|
@@ -47,7 +47,7 @@ async function listCustomerTags(options, context) {
|
|
|
47
47
|
}
|
|
48
48
|
let response;
|
|
49
49
|
try {
|
|
50
|
-
response = await requestApi(request);
|
|
50
|
+
response = await requestApi(request, await snapshotApiEnvironment(undefined, input.kdtId));
|
|
51
51
|
} catch (error) {
|
|
52
52
|
if (error instanceof ApiConfigurationError) throw failure('configuration', 'configuration');
|
|
53
53
|
throw failure('transport', 'request', 'unknown');
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"availability": "available",
|
|
37
37
|
"at": "all",
|
|
38
38
|
"module": "member/level/get/command.js",
|
|
39
|
-
"fingerprint": "
|
|
39
|
+
"fingerprint": "23c8774db6b4525aa8fc978babee0b64da63498f21a0e261f06fa63c7ffaa78d"
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
"path": "member.level.list",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"availability": "available",
|
|
10
10
|
"at": "shop",
|
|
11
11
|
"module": "org/staff/get/command.js",
|
|
12
|
-
"fingerprint": "
|
|
12
|
+
"fingerprint": "e73deac7efdda9b214eca98fea4e1d71d44d5a82e8b6cf71126f337c5b5b0a33"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"path": "org.staff-role.get",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"availability": "available",
|
|
19
19
|
"at": "shop",
|
|
20
20
|
"module": "org/staff-role/get/command.js",
|
|
21
|
-
"fingerprint": "
|
|
21
|
+
"fingerprint": "b5c0e152bc037639379c99eac78a26ed716f5e7fbddd5bc0e485011a4d05b22b"
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"path": "org.unit.get",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"availability": "available",
|
|
28
28
|
"at": "all",
|
|
29
29
|
"module": "org/unit/get/command.js",
|
|
30
|
-
"fingerprint": "
|
|
30
|
+
"fingerprint": "4e4a8b3143a619cc50c80afd33bef1d42bd68896abc3bc7f250be26911be3a68"
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"path": "org.unit.list",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"availability": "available",
|
|
37
37
|
"at": "hq",
|
|
38
38
|
"module": "org/unit/list/command.js",
|
|
39
|
-
"fingerprint": "
|
|
39
|
+
"fingerprint": "6b1636404484b8d0823d04fe1b3ee4d8e28afc91b6e1a6489264f84fe5f137bf"
|
|
40
40
|
}
|
|
41
41
|
]
|
|
42
42
|
}
|
|
@@ -32,7 +32,7 @@ function createApiCommand() {
|
|
|
32
32
|
.option('--yes', '确认本次请求的未知副作用并发送')
|
|
33
33
|
.addHelpText('after', `
|
|
34
34
|
目标可以是已登记或未登记的 API,不接收任意 URL。
|
|
35
|
-
|
|
35
|
+
默认优先使用 auth login 保存的个人凭证;无个人记录时使用环境token。明确使用环境身份配置 ZAN_SAAS_AUTH_SOURCE=env。
|
|
36
36
|
发送须 --yes,仅确认未知副作用,不证明服务端授权。
|
|
37
37
|
退出0、HTTP 2xx 和 sent=true 只说明收到响应,不证明业务成功、异步完成或写入完成。
|
|
38
38
|
body 是脱敏后的原始文本;即使 HTTP 200 正文含 success=false,也不按业务失败改变退出码。
|
|
@@ -19,23 +19,33 @@ function localTime(iso) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const cloudAccountHint = '有赞云账号开通未确认,不影响本次个人登录。请先核对账号接口状态。';
|
|
22
|
+
function credentialLine(value) {
|
|
23
|
+
if (value.credentialSource === 'login') {
|
|
24
|
+
return `默认使用个人登录凭证${value.environmentTokenIgnored ? ',已忽略环境 token' : ''}。`;
|
|
25
|
+
}
|
|
26
|
+
if (value.credentialSource === 'env') return '本次配置使用环境 token;个人登录状态与环境凭证有效性分别判断。';
|
|
27
|
+
return value.credentialSource === 'missing' ? '当前没有选中的请求凭证。' : '';
|
|
28
|
+
}
|
|
22
29
|
|
|
23
30
|
const text = {
|
|
24
31
|
login: value => {
|
|
25
32
|
const lines = [value.refreshExpiresAt
|
|
26
33
|
? `登录成功,凭证有效期至 ${localTime(value.refreshExpiresAt)}。` : '登录成功。'];
|
|
27
34
|
if (value.cloudAccount?.ensured === false) lines.push(cloudAccountHint);
|
|
35
|
+
if (credentialLine(value)) lines.push(credentialLine(value));
|
|
28
36
|
lines.push(value.shopId ? `当前店铺上下文:kdtId ${value.shopId}。` : '当前没有有效的店铺上下文。',
|
|
29
37
|
shopSelectionHint);
|
|
30
38
|
return lines.join('\n');
|
|
31
39
|
},
|
|
32
40
|
status: value => {
|
|
33
|
-
if (!value.loggedIn) return `未登录(${value.server})。执行 zan-saas auth login
|
|
41
|
+
if (!value.loggedIn) return `未登录(${value.server})。执行 zan-saas auth login 登录。` +
|
|
42
|
+
(credentialLine(value) ? `\n${credentialLine(value)}` : '');
|
|
34
43
|
const lines = [`已登录(${value.server})`];
|
|
35
44
|
if (value.loginAt) lines.push(`登录时间:${localTime(value.loginAt)}`);
|
|
36
45
|
if (value.refreshExpiresAt) lines.push(`有效期至:${localTime(value.refreshExpiresAt)}`);
|
|
37
46
|
if (value.shopId) lines.push(`当前店铺:${value.shopId}`);
|
|
38
47
|
else lines.push('当前没有有效的店铺上下文。');
|
|
48
|
+
if (credentialLine(value)) lines.push(credentialLine(value));
|
|
39
49
|
lines.push(shopSelectionHint);
|
|
40
50
|
return lines.join('\n');
|
|
41
51
|
},
|
|
@@ -83,7 +93,8 @@ function createAuthCommand(impl = defaults) {
|
|
|
83
93
|
--no-browser 只跳过自动打开浏览器,仍等待授权回调并保存个人凭证,默认超时5分钟。
|
|
84
94
|
人机交接:Agent 启动命令并把输出的登录地址交给用户;用户在浏览器确认后,Agent 等待本命令回调,再执行 zan-saas auth status 确认。用户取消或超时后重新执行本命令。
|
|
85
95
|
登录成功后执行 zan-saas shop current 查看当前店铺。
|
|
86
|
-
|
|
96
|
+
宿主会话首次使用执行 shop session init,恢复时加 --resume;账号选店、连锁组织切换及本次临时查询按共享指引分别处理。
|
|
97
|
+
默认优先使用已保存的个人凭证;仅受控调用明确配置 ZAN_SAAS_AUTH_SOURCE=env 时使用环境 token。
|
|
87
98
|
执行 zan-saas shop current 确认实际生效的店铺,再开始业务查询;切店无需重新登录。
|
|
88
99
|
登录后尝试开通有赞云账号,未确认开通时不影响本次个人登录,应核对账号接口状态。
|
|
89
100
|
浏览器授权完成后仍须成功换取并保存凭证;本命令失败时停止,不进入选店或业务查询。
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { attachCommandSchema } = require('../../execution/command-schema');
|
|
4
4
|
const { parserErrorModel } = require('../../execution/parser-error');
|
|
5
|
+
const shopModels = require('./shop-contracts');
|
|
5
6
|
|
|
6
7
|
function object(properties, required = Object.keys(properties)) {
|
|
7
8
|
return { type: 'object', additionalProperties: false, required, properties };
|
|
@@ -83,9 +84,11 @@ const skill = object({
|
|
|
83
84
|
const serverField = { type: 'string', description: '授权服务地址的 origin,默认生产环境,ZAN_SAAS_OAUTH_BASE_URL 可覆盖;凭证按它分开保存。' };
|
|
84
85
|
const time = { type: ['string', 'null'], description: 'ISO 8601 时间,服务端未返回有效期时为 null。' };
|
|
85
86
|
const credentialStatus = object({
|
|
86
|
-
loggedIn: { type: 'boolean', description: '
|
|
87
|
+
loggedIn: { type: 'boolean', description: '本机个人登录未标记需要重新登录,且已知刷新有效期未到期;未知有效期时保留已有记录。不表示环境token或服务端权限有效。' },
|
|
87
88
|
server: serverField,
|
|
88
|
-
clientId: text, tool: text, loginAt: time, accessExpiresAt: time, refreshExpiresAt: time
|
|
89
|
+
clientId: text, tool: text, loginAt: time, accessExpiresAt: time, refreshExpiresAt: time,
|
|
90
|
+
credentialSource: { enum: ['login', 'env', 'missing'], description: '按当前配置选中的凭证来源:个人登录、环境token或缺失。离线状态不证明服务端有效。' },
|
|
91
|
+
environmentTokenIgnored: { type: 'boolean', description: '已选个人登录且同时存在环境token时为true;环境token不会覆盖该登录。' }
|
|
89
92
|
}, ['loggedIn', 'server']);
|
|
90
93
|
const shopIdField = {
|
|
91
94
|
type: ['string', 'null'], description: '业务命令当前使用的店铺 ID;没有有效上下文时为 null。'
|
|
@@ -108,7 +111,7 @@ const savedShop = object({
|
|
|
108
111
|
context: { $ref: '#/definitions/context' }, shopName: { type: ['string', 'null'] }
|
|
109
112
|
});
|
|
110
113
|
const accountNote = '调用账号接口网关,不需要店铺;默认生产环境,ZAN_SAAS_ACCOUNT_API_BASE_URL 可覆盖,token 规则与业务命令相同。';
|
|
111
|
-
const contextNote = '
|
|
114
|
+
const contextNote = '上下文优先级:显式程序注入、ZAN_SAAS_CONTEXT、宿主会话保存位置;无会话ID时使用本机共享选择。无效显式输入不回退。';
|
|
112
115
|
|
|
113
116
|
const contracts = {
|
|
114
117
|
install: {
|
|
@@ -214,7 +217,7 @@ const contracts = {
|
|
|
214
217
|
'token 写入 ~/.zan-saas/credentials.json(权限 0600),按授权服务地址分开保存;输出不含 token。',
|
|
215
218
|
'用户取消或超时(5 分钟)时失败退出,不自动重试。',
|
|
216
219
|
'登录后用短超时尝试开通有赞云账号,未确认时只记录 ensured: false,不因此要求重新登录。',
|
|
217
|
-
'
|
|
220
|
+
'登录成功后按宿主生命周期初始化或恢复会话,再用 shop current 查看实际位置。账号选店、连锁内切换和临时目标分别读取 shop use、shop unit use、shop resolve 契约。',
|
|
218
221
|
'浏览器授权完成不代表本命令成功;换取凭证失败时未保存新凭证,停止后续选店和业务请求,按错误核查版本、配置及授权服务路由。'
|
|
219
222
|
]
|
|
220
223
|
},
|
|
@@ -232,8 +235,9 @@ const contracts = {
|
|
|
232
235
|
server: serverField,
|
|
233
236
|
localCredential: { enum: ['deleted', 'delete_failed'],
|
|
234
237
|
description: 'deleted 表示当前个人凭证已在本机清除或已不存在;delete_failed 表示本地清理未成功。' },
|
|
235
|
-
remoteRevocation: { enum: ['revoked', 'failed', 'unknown'],
|
|
236
|
-
description: 'revoked
|
|
238
|
+
remoteRevocation: { enum: ['revoked', 'failed', 'unknown', 'not_attempted'],
|
|
239
|
+
description: 'revoked 已确认撤销;failed 接口拒绝;unknown 无法确认结果;not_attempted 表示没有个人凭证可撤销。' },
|
|
240
|
+
localContext: { enum: ['delete_failed'], description: '选店记录清理失败时出现;凭证清理与远端撤销仍已分别尝试。' },
|
|
237
241
|
httpStatus: { type: 'integer', minimum: 100, maximum: 599,
|
|
238
242
|
description: '仅取得撤销接口失败 HTTP 响应时出现,不回显响应正文。' }
|
|
239
243
|
}, ['command', 'server', 'localCredential', 'remoteRevocation'])
|
|
@@ -295,17 +299,17 @@ const contracts = {
|
|
|
295
299
|
]
|
|
296
300
|
},
|
|
297
301
|
'shop.use': {
|
|
298
|
-
input: object({ kdtId: id }),
|
|
299
|
-
output: object({
|
|
302
|
+
input: object({ kdtId: id, sessionOnly: { type: 'boolean', description: '只更新本会话,不更新最近位置;需要会话ID。' } }, ['kdtId']),
|
|
303
|
+
output: { oneOf: [object({
|
|
300
304
|
context: { $ref: '#/definitions/context' }, shopName: { type: ['string', 'null'] },
|
|
301
305
|
overriddenByEnv: { type: 'boolean', description: '为 true 时 ZAN_SAAS_CONTEXT 已设置,业务命令仍以它为准。' }
|
|
302
|
-
}),
|
|
306
|
+
}), shopModels.chosenOutput('use_root')] },
|
|
303
307
|
definitions: { context },
|
|
304
308
|
notes: [
|
|
305
309
|
accountNote,
|
|
306
310
|
'先查询当前用户,再在其店铺列表中确认该店铺,只接受总部或单店;hq 与 shopId 都取该店铺 ID。',
|
|
307
|
-
'
|
|
308
|
-
'
|
|
311
|
+
'当前用户在该店铺的员工账号已停用时拒绝。共享模式员工状态查询失败保留旧兼容行为;会话模式要求核实成功,失败不保存选择。',
|
|
312
|
+
'无会话ID时写入 ~/.zan-saas/context.json;会话模式独立保存并默认更新供新会话继承的位置。上下文不含凭证,不证明权限。',
|
|
309
313
|
'首次选择与切换店铺均使用本命令,无需重新登录;保存后用 shop current 确认实际店铺,overriddenByEnv 为 true 时不能声称当前上下文已切换。',
|
|
310
314
|
contextNote
|
|
311
315
|
]
|
|
@@ -313,19 +317,23 @@ const contracts = {
|
|
|
313
317
|
'shop.current': {
|
|
314
318
|
input: empty,
|
|
315
319
|
output: object({
|
|
316
|
-
source: { enum: ['env', 'saved', 'missing'] },
|
|
320
|
+
source: { enum: ['env', 'saved', 'missing', 'program'] },
|
|
317
321
|
status: { enum: ['ready', 'invalid', 'missing'] },
|
|
318
322
|
context: nullableContext,
|
|
319
323
|
saved: { anyOf: [savedShop, { type: 'null' }] },
|
|
320
|
-
requestShopId: { type: ['string', 'null'], description: '环境变量 ZAN_SAAS_KDT_ID 的有效值;未设置或无效时为 null。不代表所有命令的最终请求店铺。' }
|
|
321
|
-
|
|
324
|
+
requestShopId: { type: ['string', 'null'], description: '环境变量 ZAN_SAAS_KDT_ID 的有效值;未设置或无效时为 null。不代表所有命令的最终请求店铺。' },
|
|
325
|
+
binding: shopModels.binding,
|
|
326
|
+
selection: { anyOf: [shopModels.selection, { type: 'null' }] }
|
|
327
|
+
}, ['source', 'status', 'context', 'saved', 'requestShopId']),
|
|
322
328
|
definitions: { context },
|
|
323
329
|
notes: ['只读本地文件和环境变量,不联网。requestShopId 是环境变量值;部分命令按其目标店铺设置请求头,具体以命令契约为准。', contextNote]
|
|
324
330
|
},
|
|
325
331
|
'shop.clear': {
|
|
326
|
-
input:
|
|
327
|
-
output: object({ cleared: { type: 'boolean', description: '
|
|
328
|
-
|
|
332
|
+
input: object({ recent: { type: 'boolean', description: '同时清除该账号环境的最近位置;仅会话模式支持。' } }, []),
|
|
333
|
+
output: object({ cleared: { type: 'boolean', description: '已清除选中的店铺;重复清除为false。' }, server: serverField,
|
|
334
|
+
binding: shopModels.binding, currentChanged: { type: 'boolean' }, recentChanged: { type: 'boolean' } },
|
|
335
|
+
['cleared', 'server']),
|
|
336
|
+
notes: ['会话模式保留cleared记录,避免重新继承;默认不影响其他会话或最近位置。原文件内改写,不删除或替换上下文文件。']
|
|
329
337
|
},
|
|
330
338
|
'skills.list': {
|
|
331
339
|
input: empty,
|
|
@@ -356,7 +364,7 @@ const contracts = {
|
|
|
356
364
|
}),
|
|
357
365
|
notes: [
|
|
358
366
|
'按明确的 API 名和版本发送 POST,可以是已登记或未登记的接口。风险未知,发送需 --yes;不继承领域命令的对象校验、风险分级和业务结果保证。',
|
|
359
|
-
'默认发送到生产开放 API 网关,ZAN_SAAS_API_BASE_URL
|
|
367
|
+
'默认发送到生产开放 API 网关,ZAN_SAAS_API_BASE_URL 可覆盖。默认优先使用已存个人登录,仅无个人记录时使用环境token;显式 ZAN_SAAS_AUTH_SOURCE=env 可选择环境凭证,不接受任意目标 URL。',
|
|
360
368
|
'退出0、HTTP 2xx 和 sent=true 只表示收到响应;即使正文报告业务失败也不改变退出码,不证明业务成功或写入完成。超时或断连时结果未知,不自动重试。',
|
|
361
369
|
'不提供 --dry-run,不自动应用业务金额或日期转换。'
|
|
362
370
|
]
|
|
@@ -364,7 +372,7 @@ const contracts = {
|
|
|
364
372
|
};
|
|
365
373
|
|
|
366
374
|
function withSystemSchema(command, name) {
|
|
367
|
-
const schema = { schemaVersion: 1, error, ...contracts[name] };
|
|
375
|
+
const schema = { schemaVersion: 1, error, ...(contracts[name] || shopModels.contracts[name]) };
|
|
368
376
|
schema.error = { anyOf: [schema.error, structuredClone(parserErrorModel)] };
|
|
369
377
|
attachCommandSchema(command, schema);
|
|
370
378
|
return command;
|