@sema-agent/client-core 0.46.0 → 0.48.0
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/CHANGELOG.md +188 -0
- package/LICENSE +21 -0
- package/README.md +5 -2
- package/dist/adapt/arms.js +10 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +96 -26
- package/dist/hitl/gateLedger.d.ts +21 -0
- package/dist/hitl/gateLedger.js +17 -0
- package/dist/hitl/hitlBridge.d.ts +61 -1
- package/dist/hitl/hitlBridge.js +49 -0
- package/dist/hitl/parkResolver.d.ts +2 -0
- package/dist/hitl/parkResolver.js +192 -30
- package/dist/hitl/toolApprovalWire.d.ts +36 -2
- package/dist/hitl/toolApprovalWire.js +29 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/dist/interactiveHalt.d.ts +150 -0
- package/dist/interactiveHalt.js +131 -0
- package/dist/retryStatus.d.ts +50 -3
- package/dist/retryStatus.js +31 -7
- package/dist/sessionMemoryStatus.d.ts +138 -0
- package/dist/sessionMemoryStatus.js +181 -0
- package/dist/toolResult.js +9 -0
- package/dist/typePins.d.ts +40 -0
- package/docs/INTEGRATION-CLIENTS.md +353 -26
- package/package.json +6 -6
- package/docs/REFACTOR-LEDGER.md +0 -392
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sessionMemoryStatus.ts — 会话记忆姿态的**三端公共读面**(S-53;server ≥7.53 / core 7.0.2 #511 件1;
|
|
3
|
+
* sdk 7.4.0 `sessions.memoryStatus` + `SessionMemoryStatus`)。
|
|
4
|
+
*
|
|
5
|
+
* ## 本件为什么在库里而不是在壳里
|
|
6
|
+
* 这条读面上有**两处判定**,三端(TUI / web / desktop)各写一遍必然各错一遍:
|
|
7
|
+
* ① **同 status 不同码**:`GET /v1/sessions/:id/memory-status` 的 404 有**两个**互不相干的含义 ——
|
|
8
|
+
* `not_found.session`(会话未知/非属主)与 `not_found.route`(<7.53 的老 server 压根没这条路由,
|
|
9
|
+
* 答通用回退)。按 **status** 分诊必然把「你的部署没这个面」说成「你这个会话不存在」。
|
|
10
|
+
* 判据只能锚 `errorCode`([anchor-on-the-deciding-quantity]:决定处置的量是码,不是 status)。
|
|
11
|
+
* ② **五键缺席语义逐键不同**(sdk `SessionMemoryStatus` 顶注逐字):`optOutSource` / `lastCaptureAt`
|
|
12
|
+
* 在**健康会话**上就合法缺席,其余三键只在源不可读时缺席。零历史会话的真形 =
|
|
13
|
+
* `{captureOptedOut:false, committedCount:0, foldedCount:0}`,**没有任何降级**。
|
|
14
|
+
* 把「缺席」一律读成「没有/关着/0」就是对用户下一个证不出的断言
|
|
15
|
+
* ([honest-absence-not-fabricated-zero])。
|
|
16
|
+
*
|
|
17
|
+
* ## 分工(与仓内既有形同款)
|
|
18
|
+
* **纯判定 + 薄封装**:IO 归宿主注入(`MemoryStatusClientLike`,与 `HitlClientLike` 同款 duck-type),
|
|
19
|
+
* 本件不构造 client、不认 baseUrl、不碰凭据。**一句面向用户的话都不铸** —— 措辞与是否上屏归端。
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* 一次 `memoryStatus` 失败(任意抛出物)→ 处置分型。**永不抛**。
|
|
23
|
+
*
|
|
24
|
+
* 🔴 **结构视图读,不 `instanceof`**(与 `readDecideCurrentPending` 逐字同因):抛出来的是不是 SDK 的
|
|
25
|
+
* `APIError` 由**宿主**决定 —— 跨 realm / 双 SDK 实例下 `instanceof` 会把一个读得懂的错判成读不懂。
|
|
26
|
+
* 🔴 **码优先、status 只作兜底**,且兜底只敢兜 501:
|
|
27
|
+
* · 501 在本路由上无歧义(两条 501 臂都是「面不在/没开」)⇒ 无码时按 `capability` 兜底是安全的;
|
|
28
|
+
* · 404 **恰恰相反**:两个码的处置相反,无码的 404 判不出 ⇒ 落 `failed`(如实说不知道),
|
|
29
|
+
* **绝不**挑一个猜 —— 猜错任一向都是对用户的一句假断言。
|
|
30
|
+
*/
|
|
31
|
+
export function classifyMemoryStatusFailure(e) {
|
|
32
|
+
// 🔴 **取属性本身要设防**(对抗复审 R2 [medium] 采纳,0.48.0):`e` 是**任意抛出物** ——
|
|
33
|
+
// 它完全可以是一个带抛错 getter 的对象或敌意 `Proxy`,那样连 `e.errorCode` 这一下都会抛。
|
|
34
|
+
// 而本函数是被 {@link readSessionMemoryStatus} 在 **`catch` 块里**调用的:`catch` 内抛出的
|
|
35
|
+
// 异常**不会**再被同一个 `try` 接住 ⇒ 分类器一抛,包装器那句「永不抛」当场破功。
|
|
36
|
+
// (R1 那轮把归一化移进 try 只堵了正常返回路,这条是**失败路**上的同一个洞 —— 同形第二处。)
|
|
37
|
+
// 两键**各读一次**存下来,读不动就当「没有可判的机器码」落 `failed`,把原抛出物原样带出。
|
|
38
|
+
let code;
|
|
39
|
+
let status;
|
|
40
|
+
try {
|
|
41
|
+
const o = e;
|
|
42
|
+
const rawCode = o?.errorCode;
|
|
43
|
+
const rawStatus = o?.status;
|
|
44
|
+
code = typeof rawCode === 'string' ? rawCode : undefined;
|
|
45
|
+
status = typeof rawStatus === 'number' ? rawStatus : undefined;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { kind: 'failed', error: e };
|
|
49
|
+
}
|
|
50
|
+
if (code !== undefined) {
|
|
51
|
+
if (code.startsWith('capability.'))
|
|
52
|
+
return { kind: 'unsupported', reason: 'capability' };
|
|
53
|
+
if (code.startsWith('feature.'))
|
|
54
|
+
return { kind: 'unsupported', reason: 'feature' };
|
|
55
|
+
if (code === 'not_found.route')
|
|
56
|
+
return { kind: 'unsupported', reason: 'route' };
|
|
57
|
+
if (code === 'not_found.session')
|
|
58
|
+
return { kind: 'not_found' };
|
|
59
|
+
return { kind: 'failed', error: e };
|
|
60
|
+
}
|
|
61
|
+
if (status === 501)
|
|
62
|
+
return { kind: 'unsupported', reason: 'capability' };
|
|
63
|
+
return { kind: 'failed', error: e };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* `captureOptedOut` × `optOutSource` 的**合读**。两键必须一起读:单读 `captureOptedOut` 的话,
|
|
67
|
+
* 「缺席」既可能是 fault(判不了)也可能是老 server 没给,读成 `false`(capture 开着)是最坏的方向 ——
|
|
68
|
+
* 它会让端向用户断言「你的对话正在被记忆」,而真相是**不知道**。
|
|
69
|
+
*
|
|
70
|
+
* 🔴 **`fault` 一票否决,先于任何布尔位判**(对抗复审 [medium] 采纳,0.48.0):
|
|
71
|
+
* 上游契约里 `optOutSource:"fault"` 与 `captureOptedOut` **缺席**同行,所以
|
|
72
|
+
* `{captureOptedOut:false, optOutSource:"fault"}` 是一个**自相矛盾**的形。首版按「布尔位优先」写,
|
|
73
|
+
* 于是这个形被判成 `active` —— 一个带着故障标记的载荷被读成「记忆确定开着」,正是本模块存在要防的
|
|
74
|
+
* 那件事。矛盾形**不该产出确定判决**:版本斜差 / 畸形 200 体 / 中间层改写都能造出它,而端拿到
|
|
75
|
+
* `active` 之后不会再问第二遍。⇒ 见到 `fault` 一律 `indeterminate`,布尔位说什么都不算数。
|
|
76
|
+
*/
|
|
77
|
+
export function readCaptureOptOut(s) {
|
|
78
|
+
// 🔴 **完整真值表,不是「fault 一票否决 + 看布尔位」**(对抗复审 R2 [medium] 采纳,0.48.0)。
|
|
79
|
+
// 上游契约把这两键**成对**定死,只有两个组合是有定义的:
|
|
80
|
+
// · `true` × `"record"` = 有一条单向 opt-out 记录 ⇒ opted_out
|
|
81
|
+
// · `false` × **缺席** = 记录店可读且无记录(健康默认态)⇒ active
|
|
82
|
+
// · 缺席 × `"fault"` = 记录店失败 ⇒ indeterminate
|
|
83
|
+
// 其余组合(`false`×`record` / `true`×缺席 / `true`×`fault` / …)在契约上**不存在** ——
|
|
84
|
+
// 它们只可能来自版本斜差、畸形 200 体或中间层改写。R1 那轮只挡了带 `fault` 的两格,
|
|
85
|
+
// 于是 `{captureOptedOut:false, optOutSource:"record"}`(记录明明在,布尔位却说没关)
|
|
86
|
+
// 仍被确定判成 `active`,`{captureOptedOut:true}`(没有来源佐证)仍被判成 `opted_out`。
|
|
87
|
+
// 这是**隐私姿态**的断言,方向两边都危险 ⇒ 凡不在真值表上的组合一律 `indeterminate`。
|
|
88
|
+
// ⚠️ 这里**不担心**版本斜差把已知答案降级成「不知道」:五键是同一批(S-53)进的面,
|
|
89
|
+
// 更老的 server 根本没有这条路由(答 404 `not_found.route` ⇒ 走 `unsupported`,到不了这里)。
|
|
90
|
+
if (s.captureOptedOut === true && s.optOutSource === 'record')
|
|
91
|
+
return { state: 'opted_out' };
|
|
92
|
+
if (s.captureOptedOut === false && s.optOutSource === undefined)
|
|
93
|
+
return { state: 'active' };
|
|
94
|
+
return { state: 'indeterminate' };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* `lastCaptureAt` 的**三态**读法 —— 本件存在的理由就是这一格:
|
|
98
|
+
* sdk 顶注逐字说 `lastCaptureAt` 缺席**同时**覆盖「台账不可读」与「根本没有已提交贡献」两形,
|
|
99
|
+
* 所以**单读这一键判不出任何东西**。判别材料是**另一键**:`committedCount` 与它同一次台账读 ⇒
|
|
100
|
+
* · `committedCount === 0`(台账可读、真零)+ 本键缺席 ⇒ 真的没有 → `none`;
|
|
101
|
+
* · `committedCount` 缺席(台账不可读)⇒ 推不出 → `indeterminate`;
|
|
102
|
+
* · `committedCount > 0` 却本键缺席 ⇒ 上游说不会发生;**防御性落 `indeterminate`**,
|
|
103
|
+
* 绝不折成 `none`(那会把「有贡献」渲成「从没有过」)。
|
|
104
|
+
*/
|
|
105
|
+
export function readLastCapture(s) {
|
|
106
|
+
// 🔴 `known` 的**合取**条件(对抗复审 [medium] 采纳,0.48.0):时刻在场 **且** 台账可读
|
|
107
|
+
// **且** 真有贡献。首版只判「时刻在场」,于是 `{committedCount:0, lastCaptureAt:X}` 这个
|
|
108
|
+
// **自相矛盾**形(台账明说零贡献,却给得出「最近一次捕获」的时刻;两者本是**同一次台账读**)
|
|
109
|
+
// 被判成 `known` —— 端会据此确定地渲一个时间。同理 `committedCount` 缺席(台账不可读)时,
|
|
110
|
+
// 那个时刻是从哪来的也说不清 ⇒ 一并落 `indeterminate`。
|
|
111
|
+
// 判据锚在「真正决定这件事的量」上:决定「有没有捕获过」的是**台账计数**,时刻只是它的附属。
|
|
112
|
+
if (typeof s.lastCaptureAt === 'number') {
|
|
113
|
+
return typeof s.committedCount === 'number' && s.committedCount > 0
|
|
114
|
+
? { state: 'known', atMs: s.lastCaptureAt }
|
|
115
|
+
: { state: 'indeterminate' };
|
|
116
|
+
}
|
|
117
|
+
if (s.committedCount === 0)
|
|
118
|
+
return { state: 'none' };
|
|
119
|
+
return { state: 'indeterminate' };
|
|
120
|
+
}
|
|
121
|
+
// ── ③ 薄封装(唯一一处 IO) ───────────────────────────────────────────────────────────
|
|
122
|
+
/**
|
|
123
|
+
* 读一次会话记忆姿态。**永不抛**:一切抛出物过 {@link classifyMemoryStatusFailure} 成判决。
|
|
124
|
+
*
|
|
125
|
+
* 🔴 `sessionId` 必须是**引擎捕获值**(壳从 wire 上拿到的那个 id),不是宿主自铸/自选的串 ——
|
|
126
|
+
* server 侧记录与台账按**裸 sessionId** 键控且**活过会话**,喂一个被回收的 id 会读到**上一代**
|
|
127
|
+
* 的计数/opt-out(元数据,无内容字节;server 侧成文的跨代注意)。空串在本层直接落
|
|
128
|
+
* `failed` 而不发请求:那一发必然是对某个不属于本会话的东西提问。
|
|
129
|
+
* 🔴 200 体**畸形键不铸**(非 number 的计数 / 非 boolean 的 opt-out 一律降键缺席):降键的后果是
|
|
130
|
+
* 上面两个读法答 `indeterminate`(「不知道」),而**采信**一个坏形的后果是拿它当真值渲 ——
|
|
131
|
+
* 两害相权,如实不知道。
|
|
132
|
+
*/
|
|
133
|
+
export async function readSessionMemoryStatus(client, sessionId, opts) {
|
|
134
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0) {
|
|
135
|
+
return { kind: 'failed', error: new Error('memoryStatus: empty sessionId') };
|
|
136
|
+
}
|
|
137
|
+
// 🔴 **归一化整段在 try 内**(对抗复审 [medium] 采纳,0.48.0):首版只把那一发 `await` 圈进
|
|
138
|
+
// try,归一化在 try 外读属性 —— 而本函数吃的是**宿主注入**的 duck-typed client,它完全可以回一个
|
|
139
|
+
// 带抛错 getter 的对象 / 一个 Proxy。那种载荷会让本函数**向外 reject**,当场毁掉顶注写死的
|
|
140
|
+
// 「**永不抛**」承诺,而所有调用点都是照着那句话写的(没人给它套 try)。
|
|
141
|
+
// 圈进来的代价是零,漏在外面的代价是一个只在畸形宿主上才现形的 unhandled rejection。
|
|
142
|
+
try {
|
|
143
|
+
const raw = await client.sessions.memoryStatus(sessionId, opts?.signal ? { signal: opts.signal } : {});
|
|
144
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
145
|
+
return { kind: 'failed', error: new Error('memoryStatus: malformed body') };
|
|
146
|
+
}
|
|
147
|
+
const o = raw;
|
|
148
|
+
// 🔴 **身份绑定:回声的 sessionId 必须与请求的**逐字相等**,否则整只落 `failed`**
|
|
149
|
+
// (对抗复审 [medium] 采纳)。首版对不相等的回声**照收**,并把**对方**的 id 当成
|
|
150
|
+
// 结果返回 ⇒ 一次缓存错配 / 中间层串台 / 依赖故障,就会把**另一条会话**的记忆元数据
|
|
151
|
+
// (计数、opt-out 姿态)呈现在当前会话的面板上。这条通道的上游对同一件事的纪律是
|
|
152
|
+
// **宁缺席不串台**(server 对缺 `sessionId` 的通告如实不投),本层照同一条办。
|
|
153
|
+
// ⚠️ 契约上 `sessionId` 是**必填回显**(`SessionMemoryStatusResponse`),所以缺席/非串
|
|
154
|
+
// 同样是坏形 —— 一并落 failed,**不**退回「就当是我请求的那个」。
|
|
155
|
+
// 🔴 **「严格相等会不会误杀合法场景」已对真 server 源码验真**(不是推断):server 的
|
|
156
|
+
// `GET /v1/sessions/:id/memory-status` 回显的是 `safeDecode(<路径段>)`,而 SDK 发的是
|
|
157
|
+
// `encodeURIComponent(sessionId)` ⇒ 往返是 `decodeURIComponent(encodeURIComponent(x)) === x`
|
|
158
|
+
// 的恒等,对任何合法串都字节相等;且 200 体里 `sessionId` 是**无条件**铸的(不在条件拷贝
|
|
159
|
+
// 那一组里)。⇒ 严格相等**杀不掉**任何合法回声,缺席也确实只可能是坏形。
|
|
160
|
+
// (坏 percent-encoding 由 server 在更早的门答 400 `request.path_malformed`,落 `failed`。)
|
|
161
|
+
if (typeof o.sessionId !== 'string' || o.sessionId !== sessionId) {
|
|
162
|
+
return { kind: 'failed', error: new Error('memoryStatus: response sessionId mismatch') };
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
kind: 'ok',
|
|
166
|
+
sessionId: o.sessionId,
|
|
167
|
+
facts: {
|
|
168
|
+
...(typeof o.captureOptedOut === 'boolean' ? { captureOptedOut: o.captureOptedOut } : {}),
|
|
169
|
+
...(typeof o.committedCount === 'number' ? { committedCount: o.committedCount } : {}),
|
|
170
|
+
...(typeof o.foldedCount === 'number' ? { foldedCount: o.foldedCount } : {}),
|
|
171
|
+
...(o.optOutSource === 'record' || o.optOutSource === 'fault'
|
|
172
|
+
? { optOutSource: o.optOutSource }
|
|
173
|
+
: {}),
|
|
174
|
+
...(typeof o.lastCaptureAt === 'number' ? { lastCaptureAt: o.lastCaptureAt } : {}),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
return classifyMemoryStatusFailure(e);
|
|
180
|
+
}
|
|
181
|
+
}
|
package/dist/toolResult.js
CHANGED
|
@@ -404,6 +404,15 @@ export function wireOutputToBody(toolName, output, isError, truncated, flatten,
|
|
|
404
404
|
if (slots.exitCode === 137) {
|
|
405
405
|
body += `\nCommand was killed (exit 137 — possibly out-of-memory; consider lowering build parallelism e.g. make -j2)`;
|
|
406
406
|
}
|
|
407
|
+
// [5601]/#362(2026-08-29,live 帧直证 core 5.65.0):「跑过然后被掐」家族
|
|
408
|
+
// (timeout/aborted/callback_error)铸 isError:true + 三槽 {stdout:'',stderr:'',exitCode:null}
|
|
409
|
+
// + output 正文带完整说明——重建体在这形上恒为空串,引擎正文整段被丢,下游渲
|
|
410
|
+
// 「no error detail」空卡。回退判据锚**决定量本身**(重建 body 空而正文有话),
|
|
411
|
+
// 不锚 timedOut 等键名:同家族零输出三臂一次覆盖,且 exitCode 在场时 body 至少含
|
|
412
|
+
// `Exit code N` 恒非空 ⇒ 回退结构上只在 exitCode===null 且双槽空触发,权威臂地位不动。
|
|
413
|
+
// 双空(正文也空)⇒ 如实返回空串,消费端「no error detail」兜底句保持可达。
|
|
414
|
+
if (body.trim() === '' && text.trim() !== '')
|
|
415
|
+
body = text;
|
|
407
416
|
return { content: truncated ? `${body}\n…(truncated)` : body, isError: true };
|
|
408
417
|
}
|
|
409
418
|
const stdout = truncated ? `${slots.stdout}\n…(truncated)` : slots.stdout;
|
package/dist/typePins.d.ts
CHANGED
|
@@ -15,3 +15,43 @@
|
|
|
15
15
|
* run-client-core-portability-test.mjs 头注 "①源码级传递闭包…剥 type-only").
|
|
16
16
|
*/
|
|
17
17
|
export type Covers<A, B> = Exclude<A, B> extends never ? true : never;
|
|
18
|
+
/**
|
|
19
|
+
* 🔴 `AllReadonly<T>`(0.48.0,对抗复审 [medium] 采纳后铸的第二把钉)—— 「T 的**每一个**顶层
|
|
20
|
+
* 属性都是 `readonly`」的编译期判据。`true` = 全只读;`false` = 至少有一个可写位。
|
|
21
|
+
*
|
|
22
|
+
* 为什么需要它:把一个自铸形换成上游形的**别名**时,成员名与成员类型都能逐个对上,唯独
|
|
23
|
+
* **修饰符**会静默丢失 —— 而 `readonly` 是已发布的类型契约的一部分。本批实翻:0.47.0 的
|
|
24
|
+
* `GateCurrentPending` 三位全 `readonly`(那三位是 D-1 坐标,契约是**逐字回显、绝不本地重算**),
|
|
25
|
+
* 换成上游 `ApprovalStaleCurrentPending` 的别名之后三位都变成可写 —— 名字、字段、行为门全绿,
|
|
26
|
+
* 只有消费方手上的契约悄悄弱了一档。这类退化**没有任何行为门看得见**(它不改变任何运行期字节),
|
|
27
|
+
* 只有编译期判据抓得住。
|
|
28
|
+
*
|
|
29
|
+
* 实现锚 = TS 的「同一性」惯用法:两个泛型函数签名只有在类型**完全相同**(含修饰符)时才互相
|
|
30
|
+
* 赋值兼容,所以某个位与「剥掉它 readonly 的同一个位」相等 ⇔ 那个位本来就没有 readonly。
|
|
31
|
+
*
|
|
32
|
+
* 🔴 **逐位判,不是整形判**(本批自查命中,首版就是错的):整形写法
|
|
33
|
+
* `IdenticalTo<T, {-readonly [K in keyof T]: T[K]}, false, true>` 只要**有一位**是 readonly 就答
|
|
34
|
+
* `true` —— 它其实是 `SomeReadonly`,名字比它做的事强一档。实测反证:
|
|
35
|
+
* `{readonly a: string; b: number}`(一半只读)在整形写法下答 `true`,于是「三位里有两位被改成可写」
|
|
36
|
+
* 这类**部分退化**能整个溜过去。⇒ 改成把每一位单独拎出来判,全体无可写位才 `true`。
|
|
37
|
+
* (`[X] extends [never]` 的方括号是必须的:裸 `X extends never` 在 X 是联合时会分配,空联合恒真。)
|
|
38
|
+
*
|
|
39
|
+
* 🔴 **守到哪、没守到哪(明写,免得下一棒over-trust 这把钉)**:本判据是**浅层**的 —— 它只判 T 的
|
|
40
|
+
* **顶层**属性(可选位也进得来,`-?` 管这一格)。嵌套对象**内部**的成员是否 readonly、数组元素是否
|
|
41
|
+
* `readonly T[]`,它一概不看,`Readonly<T>` 本身也只做一层。所以它能抓的是「整形/部分位的顶层
|
|
42
|
+
* readonly 退化」,抓不到「某个嵌套成员从 readonly 变可写」。
|
|
43
|
+
* 今天的唯一消费者 `GateCurrentPending` 三位全是**扁平标量**(两 `string` + 一可选 `string`),
|
|
44
|
+
* 浅层判据对它是**完备**的;哪天有人拿它去钉一个带嵌套对象的形,先回来读这一段
|
|
45
|
+
* ——「一道只守住一半却被当成守住全部的门」比没有门更坏([surfacing-is-not-gating] 同族)。
|
|
46
|
+
*/
|
|
47
|
+
type IdenticalTo<X, Y, Yes, No> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? Yes : No;
|
|
48
|
+
/** T 里**可写**位的名字联合(全只读 ⇒ `never`)。`-?` 让可选位也进得来。 */
|
|
49
|
+
type WritableKeys<T> = {
|
|
50
|
+
[P in keyof T]-?: IdenticalTo<{
|
|
51
|
+
[Q in P]: T[P];
|
|
52
|
+
}, {
|
|
53
|
+
-readonly [Q in P]: T[P];
|
|
54
|
+
}, P, never>;
|
|
55
|
+
}[keyof T];
|
|
56
|
+
export type AllReadonly<T> = [WritableKeys<T>] extends [never] ? true : false;
|
|
57
|
+
export {};
|