@inventec/dsh-copilot-auth 1.2.17 → 1.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -21
- package/lib/client.js +305 -214
- package/package.json +2 -2
- package/src/auth-flow.mjs +11 -15
- package/src/auth-host.mjs +24 -41
- package/src/host.mjs +0 -18
- package/src/model-update.mjs +107 -82
- package/src/refresh-flow.mjs +115 -33
- package/src/refresh-service.mjs +28 -13
- package/src/shared.mjs +1 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inventec/dsh-copilot-auth",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "为内置 Copilot 提供方补设备码登录设置页,预置开箱路由;凭据入内置凭据库,模型目录兜底不覆盖定制。v1.2.
|
|
3
|
+
"version": "1.2.19",
|
|
4
|
+
"description": "为内置 Copilot 提供方补设备码登录设置页,预置开箱路由;凭据入内置凭据库,模型目录兜底不覆盖定制。v1.2.19:管理模型列表 v3 目标状态编辑(分组全表/挽留/对齐/清空语义告知)+单次确认+移除请求撤回+退出残影修复。v1.2.18:授权失败附网络失败码+人性化提示。适配 DSH 0.1.7+(≤0.1.5 请用 1.1.x)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "iasi",
|
package/src/auth-flow.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// auth-flow.mjs — 浏览器授权状态机(T5
|
|
2
|
-
// 契约:createAuthFlow({fetchImpl, clock, onState}) -> {init, start,
|
|
1
|
+
// auth-flow.mjs — 浏览器授权状态机(T5;Q38 起「请求撤回」已移除):串行请求、代次保护、轮询退避、卸载停止。
|
|
2
|
+
// 契约:createAuthFlow({fetchImpl, clock, onState}) -> {init, start, logout, refresh, dispose}
|
|
3
3
|
// - 单一 init 聚合 /status;start 202 → 轮询;409 → attempt.shared 并转入对现有 attempt 的轮询
|
|
4
4
|
// - 请求代次(generation)保护:慢响应不得覆盖新操作;dispose 仅停止本页请求
|
|
5
5
|
// - 轮询 1 秒;连续读取失败退避 1/2/4/8/15 秒封顶;累计失败 60 秒转手动(connectivity=manual)
|
|
6
|
-
// - 写操作(start
|
|
6
|
+
// - 写操作(start)绝不自动重发
|
|
7
7
|
import { requestJson } from "./client-http.mjs";
|
|
8
8
|
|
|
9
9
|
const POLL_INTERVAL_MS = 1000;
|
|
@@ -61,7 +61,7 @@ export function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = ()
|
|
|
61
61
|
emit({ phase: "failed", error: s?.error ?? "authorization-failed" });
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
|
-
if (s?.status === "
|
|
64
|
+
if (s?.status === "timed-out-unverified") {
|
|
65
65
|
emit({ phase: "risk", riskKind: s.status });
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
@@ -149,6 +149,12 @@ export function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = ()
|
|
|
149
149
|
emit({ phase: "idle", error: r.error?.messageKey, connectivity: "manual" });
|
|
150
150
|
return;
|
|
151
151
|
}
|
|
152
|
+
// Q38 追加修复(真机验证发现):凭据已不在(configured:false)时,attempt 记录里的
|
|
153
|
+
// 「authorized」是退出前的历史残影而非登录态——如实回到未登录,不得翻转已登录视图
|
|
154
|
+
if (r.body?.status === "authorized" && body.configured !== true) {
|
|
155
|
+
emit({ phase: "idle" });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
152
158
|
applySnapshot(gen, r.body);
|
|
153
159
|
}
|
|
154
160
|
|
|
@@ -189,16 +195,6 @@ export function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = ()
|
|
|
189
195
|
emit({ phase: "failed", error: errorCode ?? r.error?.messageKey });
|
|
190
196
|
}
|
|
191
197
|
|
|
192
|
-
async function cancel() {
|
|
193
|
-
// 写操作:单次调用,绝不自动重发
|
|
194
|
-
const r = await requestJson(fetchImpl, "/copilot-auth/cancel", { method: "POST" });
|
|
195
|
-
if (r.ok) {
|
|
196
|
-
emit({ withdrawalDelivery: r.body?.withdrawalDelivery ?? "unavailable" });
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
emit({ withdrawalDelivery: "failed", error: r.error?.details?.errorCode ?? r.error?.messageKey });
|
|
200
|
-
}
|
|
201
|
-
|
|
202
198
|
async function logout() {
|
|
203
199
|
// 退出登录(写操作,单次调用):pending 态 → 成功后完整重跑 init 恢复真实
|
|
204
200
|
// 状态(若晚到写把凭据写了回来,init 会如实显示 authorized,绝不伪报已退出);
|
|
@@ -230,5 +226,5 @@ export function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = ()
|
|
|
230
226
|
clearTimer();
|
|
231
227
|
}
|
|
232
228
|
|
|
233
|
-
return { init, start,
|
|
229
|
+
return { init, start, logout, refresh, dispose };
|
|
234
230
|
}
|
package/src/auth-host.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
// auth-host.mjs — 授权控制器(T3
|
|
2
|
-
// intentVersion 与首次填充 handoff。
|
|
1
|
+
// auth-host.mjs — 授权控制器(T3;Q38 起软撤回已移除):单实例尝试、等待超时风险锁存、
|
|
2
|
+
// 禁用退出、intentVersion 与首次填充 handoff。
|
|
3
3
|
// 契约见 docs/plans/2026-10-04-copilot-auth-models-unified-implementation-plan.md:
|
|
4
4
|
// - start:先 intentIO.bump() 持久 intentVersion+1(失败不发 begin),再 begin;202 先行语义保留
|
|
5
|
-
// - cancel:先 bump,再调用宿主撤销(invoked/unavailable/failed),设进程内 riskLatch
|
|
6
5
|
// - logout:静止态(无 LIVE attempt+无 riskLatch)deleteRecord+删后核实;
|
|
7
6
|
// 尝试进行中/风险锁存期拒绝。「重新授权」概念随 v1.2.11 移除(退出+登录分步即可)
|
|
8
|
-
// - onAuthorized(handoff) 仅在 outcome=authorized 且 originIntentVersion
|
|
7
|
+
// - onAuthorized(handoff) 仅在 outcome=authorized 且 originIntentVersion 仍当前且无超时标记时触发
|
|
9
8
|
// - riskLatch 进程级:重挂/dispose 不清;15 分钟等待超时由 snapshot 以注入 clock 惰性判定
|
|
9
|
+
// - Q38(2026-10-05):「请求撤回」按钮与 /cancel 路由移除——撤销本地轮询的价值自限
|
|
10
|
+
// (15 分钟超时自然收敛),而其进程级锁存会把用户锁进「登录成功却不能退出」的死局
|
|
10
11
|
import { randomUUID } from "node:crypto";
|
|
11
12
|
import { CREDENTIAL_KEY } from "./shared.mjs";
|
|
12
13
|
|
|
@@ -17,10 +18,18 @@ export const LIVE_STATUSES = ["starting", "waiting", "finishing"];
|
|
|
17
18
|
// 授权错误脱敏:仅保留安全字符集(字母/数字/空格/点/冒号/逗号/连字符),
|
|
18
19
|
// 阻断上游异常文本或疑似 token(含 = ; / + _ 等的串)进入状态面。
|
|
19
20
|
const SAFE_AUTH_ERROR = /^[A-Za-z0-9 .,:-]{1,140}$/;
|
|
21
|
+
// 网络层失败码(undici cause.code):严格大写/数字/下划线白名单,
|
|
22
|
+
// 如 ETIMEDOUT/ECONNRESET/ENOTFOUND/UNABLE_TO_VERIFY_LEAF_SIGNATURE
|
|
23
|
+
const SAFE_CAUSE_CODE = /^[A-Z0-9_]{3,60}$/;
|
|
20
24
|
|
|
21
25
|
export function sanitizeAuthError(err) {
|
|
22
26
|
const msg = String(err?.message ?? err);
|
|
23
|
-
|
|
27
|
+
const base = SAFE_AUTH_ERROR.test(msg) ? msg : "authorization-failed";
|
|
28
|
+
// v1.2.18:网络层失败的原话(如 Node 的 "fetch failed")不携带任何定位信息,
|
|
29
|
+
// 追加 cause.code(严格白名单)让桌面/代理类环境问题一眼可辨(主人 2026-10-05 裁决)
|
|
30
|
+
const causeCode = String(err?.cause?.code ?? "");
|
|
31
|
+
if (SAFE_CAUSE_CODE.test(causeCode)) return `${base} (${causeCode})`;
|
|
32
|
+
return base;
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
function authError(code, message, extra = {}) {
|
|
@@ -66,9 +75,7 @@ export function createAuthorizationController(ctx, { scope, intentIO, clock = {
|
|
|
66
75
|
startedAt: clock.now(),
|
|
67
76
|
notices: [],
|
|
68
77
|
originIntentVersion,
|
|
69
|
-
withdrawalRequested: false,
|
|
70
78
|
timedOut: false,
|
|
71
|
-
withdrawalDelivery: undefined,
|
|
72
79
|
error: undefined,
|
|
73
80
|
staleIntent: false,
|
|
74
81
|
};
|
|
@@ -99,7 +106,7 @@ export function createAuthorizationController(ctx, { scope, intentIO, clock = {
|
|
|
99
106
|
if (attempt !== a) return;
|
|
100
107
|
if (outcome && outcome.status === "authorized") {
|
|
101
108
|
const versionStillCurrent = a.originIntentVersion === intentIO.read();
|
|
102
|
-
const intact = versionStillCurrent && !a.
|
|
109
|
+
const intact = versionStillCurrent && !a.timedOut;
|
|
103
110
|
a.status = "authorized";
|
|
104
111
|
a.staleIntent = !intact;
|
|
105
112
|
if (intact && typeof onAuthorized === "function") {
|
|
@@ -111,9 +118,9 @@ export function createAuthorizationController(ctx, { scope, intentIO, clock = {
|
|
|
111
118
|
});
|
|
112
119
|
}
|
|
113
120
|
} else {
|
|
114
|
-
//
|
|
115
|
-
a.status = "
|
|
116
|
-
|
|
121
|
+
// Q38:撤回通道已移除,cancelled 结局防御性映射为 failed(无晚写风险源,不设锁存)
|
|
122
|
+
a.status = "failed";
|
|
123
|
+
a.error = outcome?.status === "cancelled" ? "authorization-cancelled" : "authorization-failed";
|
|
117
124
|
}
|
|
118
125
|
})
|
|
119
126
|
.catch((err) => {
|
|
@@ -124,34 +131,9 @@ export function createAuthorizationController(ctx, { scope, intentIO, clock = {
|
|
|
124
131
|
return { attemptId: a.attemptId };
|
|
125
132
|
}
|
|
126
133
|
|
|
127
|
-
async function cancel() {
|
|
128
|
-
requireScope();
|
|
129
|
-
// 先持久失效本地旧意图,再发撤回调用
|
|
130
|
-
intentIO.bump();
|
|
131
|
-
let withdrawalDelivery = "unavailable";
|
|
132
|
-
if (attempt) {
|
|
133
|
-
attempt.withdrawalRequested = true;
|
|
134
|
-
if (LIVE_STATUSES.includes(attempt.status)) attempt.status = "withdrawal-pending-unverified";
|
|
135
|
-
}
|
|
136
|
-
const cancelFn = ctx.authorization?.cancel;
|
|
137
|
-
if (typeof cancelFn === "function") {
|
|
138
|
-
try {
|
|
139
|
-
await cancelFn(CREDENTIAL_KEY);
|
|
140
|
-
withdrawalDelivery = "invoked";
|
|
141
|
-
} catch {
|
|
142
|
-
withdrawalDelivery = "failed";
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
// delivery 写入 attempt:刷新页面后 /state 恢复仍如实展示「已请求撤回」事实
|
|
146
|
-
if (attempt) attempt.withdrawalDelivery = withdrawalDelivery;
|
|
147
|
-
// 任何响应都不承诺晚写停止:风险锁存待核实
|
|
148
|
-
setRiskLatch("withdrawal-requested");
|
|
149
|
-
return { withdrawalDelivery };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
134
|
async function logout() {
|
|
153
135
|
// 退出安全条件(v1.2.10 起,经主人裁决替代 D-01 全禁策略):
|
|
154
|
-
// ① 无进行中的授权尝试(begin 晚写窗口)②
|
|
136
|
+
// ① 无进行中的授权尝试(begin 晚写窗口)② 无风险锁存(Q38 起仅剩等待超时未核实)。
|
|
155
137
|
// 满足后凭据库视为静止:deleteRecord+删除后 describe 复核;晚到写无法绝对
|
|
156
138
|
// 排除,以复核与状态页如实展示兜底——退出未核实成功绝不伪报成功。
|
|
157
139
|
requireScope();
|
|
@@ -175,6 +157,9 @@ export function createAuthorizationController(ctx, { scope, intentIO, clock = {
|
|
|
175
157
|
if (err?.code === "LOGOUT_FAILED") throw err;
|
|
176
158
|
throw authError("LOGOUT_FAILED", "logout-failed");
|
|
177
159
|
}
|
|
160
|
+
// Q38 追加修复(真机验证发现):退出成功后清掉 attempt 残影——否则 /state 继续报告
|
|
161
|
+
// 历史「authorized」,客户端会把残影误当登录态,UI 翻回已登录,表现为「退出没作用」
|
|
162
|
+
attempt = null;
|
|
178
163
|
return { ok: true };
|
|
179
164
|
}
|
|
180
165
|
|
|
@@ -196,7 +181,6 @@ export function createAuthorizationController(ctx, { scope, intentIO, clock = {
|
|
|
196
181
|
url: attempt.url,
|
|
197
182
|
expiresAt: attempt.expiresAt,
|
|
198
183
|
waitDeadline: attempt.startedAt + WAIT_LIMIT_MS,
|
|
199
|
-
withdrawalDelivery: attempt.withdrawalDelivery,
|
|
200
184
|
riskLatch,
|
|
201
185
|
staleIntent: attempt.staleIntent,
|
|
202
186
|
notices: [...attempt.notices],
|
|
@@ -204,15 +188,14 @@ export function createAuthorizationController(ctx, { scope, intentIO, clock = {
|
|
|
204
188
|
};
|
|
205
189
|
}
|
|
206
190
|
|
|
207
|
-
// T4 消费:handoff 是否仍完好(同 attempt
|
|
191
|
+
// T4 消费:handoff 是否仍完好(同 attempt、版本仍当前、无超时标记)
|
|
208
192
|
function handoffIntact(handoff) {
|
|
209
193
|
if (!attempt || !handoff) return false;
|
|
210
194
|
return attempt.attemptId === handoff.attemptId
|
|
211
195
|
&& attempt.originIntentVersion === handoff.originIntentVersion
|
|
212
196
|
&& attempt.originIntentVersion === intentIO.read()
|
|
213
|
-
&& !attempt.withdrawalRequested
|
|
214
197
|
&& !attempt.timedOut;
|
|
215
198
|
}
|
|
216
199
|
|
|
217
|
-
return { start,
|
|
200
|
+
return { start, logout, snapshot, handoffIntact };
|
|
218
201
|
}
|
package/src/host.mjs
CHANGED
|
@@ -528,24 +528,6 @@ export function apply(ctx, opts = {}) {
|
|
|
528
528
|
},
|
|
529
529
|
});
|
|
530
530
|
|
|
531
|
-
ctx.webServer.register({
|
|
532
|
-
kind: "exact",
|
|
533
|
-
path: r.cancel,
|
|
534
|
-
handler: async (req, res) => {
|
|
535
|
-
if (!guard(req, res, "POST")) return;
|
|
536
|
-
if (!scope.known) {
|
|
537
|
-
json(res, 503, { ok: false, error: "scope-unavailable" });
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
try {
|
|
541
|
-
const { withdrawalDelivery } = await controller.cancel();
|
|
542
|
-
json(res, 200, { ok: true, withdrawalDelivery });
|
|
543
|
-
} catch (err) {
|
|
544
|
-
json(res, 500, { ok: false, error: sanitizeAuthError(err) });
|
|
545
|
-
}
|
|
546
|
-
},
|
|
547
|
-
});
|
|
548
|
-
|
|
549
531
|
ctx.webServer.register({
|
|
550
532
|
kind: "exact",
|
|
551
533
|
path: r.state,
|
package/src/model-update.mjs
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
// model-update.mjs — 纯模型变更策略(
|
|
1
|
+
// model-update.mjs — 纯模型变更策略(v3):管理模型列表=目标状态编辑(ADR 0005)。
|
|
2
2
|
// 纯函数,不触碰网络/磁盘/settings;视图形状与 host.mjs viewFromUserLayer 同构:
|
|
3
3
|
// {modelsPresent, models, modelOverridesPresent, modelOverrides}
|
|
4
|
-
// 语义(设计§4.1 /
|
|
5
|
-
// -
|
|
6
|
-
// -
|
|
7
|
-
//
|
|
8
|
-
// -
|
|
4
|
+
// 语义(设计§4.1/§4.2 2026-10-05 修订 / Q32–Q36):
|
|
5
|
+
// - 单一操作 manage:勾选集合即期望终态;selectedIds 缺省 → 在列∩(账号∩可解析)(与 UI 初始态一致)
|
|
6
|
+
// - 被保留模型维持原对象/顺序/定制;被移除模型定制随模型消失(overrides 按 kept 过滤,
|
|
7
|
+
// 唯一定制模型被移除时 present:false → unset,防重加复活定制)
|
|
8
|
+
// - rows:addable(账号∩可解析−在列)/ listed(在列∩(账号∩可解析))/ removal-proposal
|
|
9
|
+
// (在列−(账号∩可解析),reason 优先级:不在账号→not-in-account,否则 unresolvable)
|
|
10
|
+
// - removed:raw 在列全表−勾选;reason:not-in-account → unresolvable → unchecked(用户主动取消健康行)
|
|
11
|
+
// - 空目标确认仅约束用户显式提交的空选择(selectedIds!==undefined);确认后落显式空列表
|
|
12
|
+
// (modelsPresent:true, models:[],防 unset 后首次填充静默回填)
|
|
13
|
+
// - clearOverrides:清除可清除定制(present:false);仅继承覆盖(raw 缺失而 effective 有)→ 阻止该项
|
|
9
14
|
export function buildModelChange({
|
|
10
15
|
operation,
|
|
11
16
|
rawView,
|
|
@@ -13,11 +18,17 @@ export function buildModelChange({
|
|
|
13
18
|
accountIds,
|
|
14
19
|
resolvableIds,
|
|
15
20
|
selectedIds,
|
|
21
|
+
clearOverrides = false,
|
|
16
22
|
confirmEmpty = false,
|
|
17
23
|
catalogNewEntryCount = 0,
|
|
18
24
|
}) {
|
|
19
|
-
if (operation !== "
|
|
20
|
-
return {
|
|
25
|
+
if (operation !== "manage") {
|
|
26
|
+
return {
|
|
27
|
+
allowed: false, reason: "invalid-operation", targetView: null, rows: [], selectedIds: [],
|
|
28
|
+
added: [], removed: [], kept: [], warnings: [], removalClass: false,
|
|
29
|
+
overridesMeta: { rawPresent: false, clearable: false, inheritedOnly: false },
|
|
30
|
+
writeSet: { catalogEntries: false, models: false, modelOverrides: false, overridesUnclearable: false },
|
|
31
|
+
};
|
|
21
32
|
}
|
|
22
33
|
const accounts = [...new Set((Array.isArray(accountIds) ? accountIds : []).filter((x) => typeof x === "string"))];
|
|
23
34
|
const resolvable = new Set((Array.isArray(resolvableIds) ? resolvableIds : []).filter((x) => typeof x === "string"));
|
|
@@ -25,94 +36,108 @@ export function buildModelChange({
|
|
|
25
36
|
const eff = effectiveView ?? raw;
|
|
26
37
|
const rawModels = raw.modelsPresent && Array.isArray(raw.models) ? raw.models : [];
|
|
27
38
|
const rawIds = rawModels.map((m) => m?.id).filter((x) => typeof x === "string");
|
|
28
|
-
const
|
|
29
|
-
const
|
|
39
|
+
const rawIdSet = new Set(rawIds);
|
|
40
|
+
const accountSet = new Set(accounts);
|
|
41
|
+
|
|
42
|
+
const overridesKeys = raw.modelOverridesPresent && raw.modelOverrides && typeof raw.modelOverrides === "object"
|
|
43
|
+
? Object.keys(raw.modelOverrides) : [];
|
|
44
|
+
const rawPresent = overridesKeys.length > 0;
|
|
45
|
+
const effOverrides = eff.modelOverridesPresent && eff.modelOverrides && typeof eff.modelOverrides === "object"
|
|
46
|
+
? Object.keys(eff.modelOverrides) : [];
|
|
47
|
+
const inheritedOnly = !rawPresent && effOverrides.length > 0;
|
|
48
|
+
const overridesMeta = { rawPresent, clearable: rawPresent, inheritedOnly };
|
|
49
|
+
|
|
50
|
+
const inUniverse = (id) => accountSet.has(id) && resolvable.has(id);
|
|
51
|
+
const addable = accounts.filter((id) => resolvable.has(id) && !rawIdSet.has(id));
|
|
52
|
+
const addableSet = new Set(addable);
|
|
53
|
+
const rows = [
|
|
54
|
+
...addable.map((id) => ({ id, status: "addable", customized: false })),
|
|
55
|
+
...rawIds.filter((id) => inUniverse(id)).map((id) => ({ id, status: "listed", customized: overridesKeys.includes(id) })),
|
|
56
|
+
...rawIds.filter((id) => !inUniverse(id)).map((id) => ({
|
|
57
|
+
id,
|
|
58
|
+
status: "removal-proposal",
|
|
59
|
+
reason: accountSet.has(id) ? "unresolvable" : "not-in-account",
|
|
60
|
+
customized: overridesKeys.includes(id),
|
|
61
|
+
})),
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// 选择规范化:缺省 → listed(与 UI 初始勾选态一致);显式(含 [])→ 用户意图
|
|
65
|
+
const explicitSelection = selectedIds !== undefined;
|
|
66
|
+
const requested = explicitSelection
|
|
67
|
+
? [...new Set((Array.isArray(selectedIds) ? selectedIds : []).filter((x) => typeof x === "string"))]
|
|
68
|
+
: rawIds.filter((id) => inUniverse(id));
|
|
69
|
+
const selectedSet = new Set(requested);
|
|
70
|
+
// warnings:manage 语义下只产 invalid-selection/unresolvable(already-configured 仅保留在契约枚举中,不可达)
|
|
30
71
|
const warnings = [];
|
|
31
|
-
for (const id of
|
|
32
|
-
if (
|
|
72
|
+
for (const id of requested) {
|
|
73
|
+
if (addableSet.has(id) || rawIdSet.has(id)) continue;
|
|
74
|
+
warnings.push({ id, reason: accountSet.has(id) && !resolvable.has(id) ? "unresolvable" : "invalid-selection" });
|
|
33
75
|
}
|
|
34
|
-
const catalogEntries = catalogNewEntryCount > 0;
|
|
35
76
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
added.push(id);
|
|
47
|
-
} else if (existing.has(id)) {
|
|
48
|
-
warnings.push({ id, reason: "already-configured" });
|
|
49
|
-
} else if (accounts.includes(id) && !resolvable.has(id)) {
|
|
50
|
-
warnings.push({ id, reason: "unresolvable" });
|
|
51
|
-
} else {
|
|
52
|
-
warnings.push({ id, reason: "invalid-selection" });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
const models = added.length > 0 ? [...rawModels, ...added.map((id) => ({ id }))] : rawModels;
|
|
56
|
-
const modelsPresent = raw.modelsPresent || added.length > 0;
|
|
57
|
-
const overridesUnclearable = false;
|
|
58
|
-
return {
|
|
59
|
-
allowed: true,
|
|
60
|
-
reason: null,
|
|
61
|
-
candidates, // 候选=账号∩可解析−已有(Q5:默认全不勾选,勾选后 materialize 新预览)
|
|
62
|
-
targetView: {
|
|
63
|
-
modelsPresent,
|
|
64
|
-
models: modelsPresent ? models : null,
|
|
65
|
-
modelOverridesPresent: raw.modelOverridesPresent,
|
|
66
|
-
modelOverrides: raw.modelOverrides,
|
|
67
|
-
},
|
|
68
|
-
added,
|
|
69
|
-
removed: [],
|
|
70
|
-
kept: effIds.filter((id) => existing.has(id)),
|
|
71
|
-
warnings,
|
|
72
|
-
writeSet: { catalogEntries, models: added.length > 0, modelOverrides: false, overridesUnclearable },
|
|
73
|
-
};
|
|
74
|
-
}
|
|
77
|
+
const kept = rawIds.filter((id) => selectedSet.has(id));
|
|
78
|
+
const keptSet = new Set(kept);
|
|
79
|
+
const added = addable.filter((id) => selectedSet.has(id));
|
|
80
|
+
const removed = rawIds
|
|
81
|
+
.filter((id) => !selectedSet.has(id))
|
|
82
|
+
.map((id) => ({
|
|
83
|
+
id,
|
|
84
|
+
reason: !accountSet.has(id) ? "not-in-account" : !resolvable.has(id) ? "unresolvable" : "unchecked",
|
|
85
|
+
}));
|
|
86
|
+
const removalClass = removed.length > 0;
|
|
75
87
|
|
|
76
|
-
|
|
77
|
-
const effOverrides = eff.modelOverridesPresent && eff.modelOverrides && typeof eff.modelOverrides === "object"
|
|
78
|
-
? Object.keys(eff.modelOverrides) : [];
|
|
79
|
-
if (!raw.modelOverridesPresent && effOverrides.length > 0) {
|
|
88
|
+
if (clearOverrides && inheritedOnly) {
|
|
80
89
|
return {
|
|
81
|
-
allowed: false,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
writeSet: { catalogEntries, models: false, modelOverrides: false, overridesUnclearable: true },
|
|
90
|
+
allowed: false, reason: "inherited-overrides-unclearable", targetView: null, rows, selectedIds: requested,
|
|
91
|
+
added, removed, kept, warnings, removalClass,
|
|
92
|
+
overridesMeta,
|
|
93
|
+
writeSet: { catalogEntries: catalogNewEntryCount > 0, models: false, modelOverrides: false, overridesUnclearable: true },
|
|
85
94
|
};
|
|
86
95
|
}
|
|
87
|
-
|
|
88
|
-
if (intersection.length === 0 && !confirmEmpty) {
|
|
96
|
+
if (explicitSelection && rawIds.length > 0 && requested.length === 0 && !confirmEmpty) {
|
|
89
97
|
return {
|
|
90
|
-
allowed: false,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
writeSet: { catalogEntries, models: false, modelOverrides: false, overridesUnclearable: false },
|
|
98
|
+
allowed: false, reason: "empty-target-unconfirmed", targetView: null, rows, selectedIds: requested,
|
|
99
|
+
added, removed, kept, warnings, removalClass,
|
|
100
|
+
overridesMeta,
|
|
101
|
+
writeSet: { catalogEntries: catalogNewEntryCount > 0, models: false, modelOverrides: false, overridesUnclearable: false },
|
|
94
102
|
};
|
|
95
103
|
}
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
|
|
105
|
+
const keptRawModels = rawModels.filter((m) => keptSet.has(m?.id));
|
|
106
|
+
const modelsPresent = raw.modelsPresent || requested.length > 0;
|
|
107
|
+
const models = modelsPresent ? [...keptRawModels, ...added.map((id) => ({ id }))] : null;
|
|
108
|
+
const clearable = rawPresent;
|
|
109
|
+
let modelOverridesPresent;
|
|
110
|
+
let modelOverrides;
|
|
111
|
+
if (clearOverrides && clearable) {
|
|
112
|
+
modelOverridesPresent = false;
|
|
113
|
+
modelOverrides = null;
|
|
114
|
+
} else if (rawPresent) {
|
|
115
|
+
const filtered = {};
|
|
116
|
+
for (const key of overridesKeys) if (keptSet.has(key)) filtered[key] = raw.modelOverrides[key];
|
|
117
|
+
modelOverridesPresent = Object.keys(filtered).length > 0;
|
|
118
|
+
modelOverrides = modelOverridesPresent ? filtered : null;
|
|
119
|
+
} else {
|
|
120
|
+
modelOverridesPresent = raw.modelOverridesPresent;
|
|
121
|
+
modelOverrides = raw.modelOverrides;
|
|
122
|
+
}
|
|
123
|
+
|
|
102
124
|
return {
|
|
103
125
|
allowed: true,
|
|
104
126
|
reason: null,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
models: intersection.map((id) => ({ id })),
|
|
109
|
-
modelOverridesPresent: false,
|
|
110
|
-
modelOverrides: null,
|
|
111
|
-
},
|
|
112
|
-
added: intersection.filter((id) => !new Set(effIds).has(id)),
|
|
127
|
+
rows,
|
|
128
|
+
selectedIds: requested,
|
|
129
|
+
added,
|
|
113
130
|
removed,
|
|
114
|
-
kept
|
|
131
|
+
kept,
|
|
115
132
|
warnings,
|
|
116
|
-
|
|
133
|
+
removalClass,
|
|
134
|
+
overridesMeta,
|
|
135
|
+
targetView: { modelsPresent, models, modelOverridesPresent, modelOverrides },
|
|
136
|
+
writeSet: {
|
|
137
|
+
catalogEntries: catalogNewEntryCount > 0,
|
|
138
|
+
models: added.length > 0 || removalClass || (!raw.modelsPresent && requested.length > 0),
|
|
139
|
+
modelOverrides: clearOverrides && clearable,
|
|
140
|
+
overridesUnclearable: false,
|
|
141
|
+
},
|
|
117
142
|
};
|
|
118
143
|
}
|