dsh-manage-session 2.0.3 → 2.0.5
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.ja.md +33 -33
- package/CHANGELOG.ko.md +33 -33
- package/CHANGELOG.md +33 -33
- package/INSTALL.ja.md +164 -164
- package/INSTALL.ko.md +164 -164
- package/INSTALL.md +164 -164
- package/INSTALL.zh.md +164 -164
- package/LICENSE +21 -21
- package/README.en.md +76 -62
- package/README.ja.md +53 -39
- package/README.ko.md +52 -38
- package/README.md +18 -4
- package/cordis.patch.yml +19 -19
- package/dsh.plugin.json +2 -2
- package/locale/de.json +6 -6
- package/locale/en.json +6 -6
- package/locale/es.json +6 -6
- package/locale/fr.json +6 -6
- package/locale/it.json +6 -6
- package/locale/ja.json +6 -6
- package/locale/ko.json +6 -6
- package/locale/ru.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +9 -9
- package/screenshots.json +3 -3
- package/src/client/bridge-scope.ts +202 -202
- package/src/client/card.tsx +154 -154
- package/src/client/host-api.ts +61 -61
- package/src/client/index.ts +594 -594
- package/src/client/locales.ts +893 -893
- package/src/client/manage-locales.ts +178 -178
- package/src/client/manage.tsx +430 -430
- package/src/client/panel.tsx +131 -131
- package/src/config.ts +43 -43
- package/src/host/health/attribution.ts +118 -118
- package/src/host/health/lossless.ts +128 -128
- package/src/host/health/source-kind.ts +350 -350
- package/src/host/history/archive-source.ts +250 -250
- package/src/host/history/archive.ts +249 -249
- package/src/host/history/favorites.ts +71 -71
- package/src/host/history/purge.ts +370 -370
- package/src/host/registry/archive-registry.ts +317 -317
- package/src/host/settings-bridge.ts +222 -222
|
@@ -1,128 +1,128 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 无损 JSON 判定 —— 本次事故的核心 gate 判据。
|
|
3
|
-
*
|
|
4
|
-
* 判定口径与宿主 `@deepseek-ai/dsh-util-values` 的 `isJsonValue` 一致:
|
|
5
|
-
* 普通(或 null)原型的对象、可枚举的字符串键、有限数(含拒绝 -0)、非稀疏数组、
|
|
6
|
-
* 无环;undefined / NaN / ±Infinity / 函数 / Symbol / BigInt 一律视为不可无损序列化。
|
|
7
|
-
*
|
|
8
|
-
* 通过宿主 `assertJsonArgs` / 投影缓存写入的数据都会先过这道口径,所以投影**状态**
|
|
9
|
-
* 一旦含上述值,`api-session/added` 推送与检查点写入都会失败(症状错位的根因)。
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/** 首个违规点位。 */
|
|
13
|
-
export interface LosslessViolation {
|
|
14
|
-
/** 到违规值的属性路径,例如 `.activeStep.lastSettled.actualTokens`。 */
|
|
15
|
-
path: string
|
|
16
|
-
/** 违规原因的可读描述。 */
|
|
17
|
-
reason:
|
|
18
|
-
| 'undefined'
|
|
19
|
-
| 'NaN'
|
|
20
|
-
| 'Infinity'
|
|
21
|
-
| '-Infinity'
|
|
22
|
-
| '-0'
|
|
23
|
-
| 'function'
|
|
24
|
-
| 'symbol'
|
|
25
|
-
| 'bigint'
|
|
26
|
-
| 'sparse-array-hole'
|
|
27
|
-
| 'non-plain-prototype'
|
|
28
|
-
| 'non-enumerable-or-symbol-key'
|
|
29
|
-
| 'cycle'
|
|
30
|
-
/** 违规处的实际类型(便于人工核对)。 */
|
|
31
|
-
actual: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** 单个值是否可无损 JSON 序列化。 */
|
|
35
|
-
export function isLossless(value: unknown): boolean {
|
|
36
|
-
return firstLosslessViolation(value) === undefined
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** 判定一个普通对象是否「普通原型」(Object.prototype 或 null)。 */
|
|
40
|
-
function isPlainObjectPrototype(value: object): boolean {
|
|
41
|
-
const prototype = Object.getPrototypeOf(value) as object | null
|
|
42
|
-
return prototype === null || prototype === Object.prototype
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* 找出第一个破坏无损 JSON 的值;全部合规时返回 undefined。
|
|
47
|
-
* @param value - 待判定的值(通常是投影状态)。
|
|
48
|
-
* @param limit - 最多访问的节点数(防御超大对象)。
|
|
49
|
-
* @returns 首个违规点位或 undefined。
|
|
50
|
-
*/
|
|
51
|
-
export function firstLosslessViolation(value: unknown, limit = 200_000): LosslessViolation | undefined {
|
|
52
|
-
const ancestors = new Set<object>()
|
|
53
|
-
const stack: { node: unknown; path: string; leave?: boolean }[] = [{ node: value, path: '' }]
|
|
54
|
-
let visited = 0
|
|
55
|
-
while (stack.length > 0) {
|
|
56
|
-
const task = stack.pop()
|
|
57
|
-
if (task === undefined) break
|
|
58
|
-
if (task.leave === true) {
|
|
59
|
-
ancestors.delete(task.node as object)
|
|
60
|
-
continue
|
|
61
|
-
}
|
|
62
|
-
if (visited >= limit) return { path: task.path, reason: 'cycle', actual: `node-limit(${limit})` }
|
|
63
|
-
visited += 1
|
|
64
|
-
const node = task.node
|
|
65
|
-
if (node === undefined) return { path: task.path, reason: 'undefined', actual: 'undefined' }
|
|
66
|
-
if (node === null) continue
|
|
67
|
-
switch (typeof node) {
|
|
68
|
-
case 'boolean':
|
|
69
|
-
case 'string':
|
|
70
|
-
continue
|
|
71
|
-
case 'number':
|
|
72
|
-
if (Number.isNaN(node)) return { path: task.path, reason: 'NaN', actual: 'NaN' }
|
|
73
|
-
if (node === Infinity) return { path: task.path, reason: 'Infinity', actual: 'Infinity' }
|
|
74
|
-
if (node === -Infinity) return { path: task.path, reason: '-Infinity', actual: '-Infinity' }
|
|
75
|
-
if (Object.is(node, -0)) return { path: task.path, reason: '-0', actual: '-0' }
|
|
76
|
-
continue
|
|
77
|
-
case 'function':
|
|
78
|
-
return { path: task.path, reason: 'function', actual: 'function' }
|
|
79
|
-
case 'symbol':
|
|
80
|
-
return { path: task.path, reason: 'symbol', actual: 'symbol' }
|
|
81
|
-
case 'bigint':
|
|
82
|
-
return { path: task.path, reason: 'bigint', actual: 'bigint' }
|
|
83
|
-
default:
|
|
84
|
-
break
|
|
85
|
-
}
|
|
86
|
-
const object = node as object
|
|
87
|
-
if (ancestors.has(object)) return { path: task.path, reason: 'cycle', actual: 'object-cycle' }
|
|
88
|
-
if (Array.isArray(object)) {
|
|
89
|
-
if (!isPlainArray(object)) return { path: task.path, reason: 'non-plain-prototype', actual: 'array-subclass' }
|
|
90
|
-
// 先按升序找出最低位的洞(报告口径确定),再倒序入栈以获得左→右的访问顺序。
|
|
91
|
-
for (let index = 0; index < object.length; index += 1) {
|
|
92
|
-
if (!Object.prototype.hasOwnProperty.call(object, index)) {
|
|
93
|
-
return { path: `${task.path}[${index}]`, reason: 'sparse-array-hole', actual: 'hole' }
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
ancestors.add(object)
|
|
97
|
-
stack.push({ node: object, path: task.path, leave: true })
|
|
98
|
-
for (let index = object.length - 1; index >= 0; index -= 1) {
|
|
99
|
-
stack.push({ node: object[index], path: `${task.path}[${index}]` })
|
|
100
|
-
}
|
|
101
|
-
continue
|
|
102
|
-
}
|
|
103
|
-
if (!isPlainObjectPrototype(object)) {
|
|
104
|
-
const name = (object as { constructor?: { name?: string } }).constructor?.name ?? 'unknown'
|
|
105
|
-
return { path: task.path, reason: 'non-plain-prototype', actual: name }
|
|
106
|
-
}
|
|
107
|
-
const keys = Reflect.ownKeys(object)
|
|
108
|
-
for (const key of keys) {
|
|
109
|
-
if (typeof key !== 'string' || !Object.prototype.propertyIsEnumerable.call(object, key)) {
|
|
110
|
-
return { path: `${task.path}.${String(key)}`, reason: 'non-enumerable-or-symbol-key', actual: typeof key }
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
ancestors.add(object)
|
|
114
|
-
stack.push({ node: object, path: task.path, leave: true })
|
|
115
|
-
for (let index = keys.length - 1; index >= 0; index -= 1) {
|
|
116
|
-
const key = keys[index]
|
|
117
|
-
if (key === undefined || typeof key !== 'string') continue
|
|
118
|
-
stack.push({ node: (object as Record<string, unknown>)[key], path: `${task.path}.${key}` })
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return undefined
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** 数组是否为普通数组(排除子类)。 */
|
|
125
|
-
function isPlainArray(value: readonly unknown[]): boolean {
|
|
126
|
-
const prototype = Object.getPrototypeOf(value) as object | null
|
|
127
|
-
return prototype === null || prototype === Array.prototype
|
|
128
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 无损 JSON 判定 —— 本次事故的核心 gate 判据。
|
|
3
|
+
*
|
|
4
|
+
* 判定口径与宿主 `@deepseek-ai/dsh-util-values` 的 `isJsonValue` 一致:
|
|
5
|
+
* 普通(或 null)原型的对象、可枚举的字符串键、有限数(含拒绝 -0)、非稀疏数组、
|
|
6
|
+
* 无环;undefined / NaN / ±Infinity / 函数 / Symbol / BigInt 一律视为不可无损序列化。
|
|
7
|
+
*
|
|
8
|
+
* 通过宿主 `assertJsonArgs` / 投影缓存写入的数据都会先过这道口径,所以投影**状态**
|
|
9
|
+
* 一旦含上述值,`api-session/added` 推送与检查点写入都会失败(症状错位的根因)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** 首个违规点位。 */
|
|
13
|
+
export interface LosslessViolation {
|
|
14
|
+
/** 到违规值的属性路径,例如 `.activeStep.lastSettled.actualTokens`。 */
|
|
15
|
+
path: string
|
|
16
|
+
/** 违规原因的可读描述。 */
|
|
17
|
+
reason:
|
|
18
|
+
| 'undefined'
|
|
19
|
+
| 'NaN'
|
|
20
|
+
| 'Infinity'
|
|
21
|
+
| '-Infinity'
|
|
22
|
+
| '-0'
|
|
23
|
+
| 'function'
|
|
24
|
+
| 'symbol'
|
|
25
|
+
| 'bigint'
|
|
26
|
+
| 'sparse-array-hole'
|
|
27
|
+
| 'non-plain-prototype'
|
|
28
|
+
| 'non-enumerable-or-symbol-key'
|
|
29
|
+
| 'cycle'
|
|
30
|
+
/** 违规处的实际类型(便于人工核对)。 */
|
|
31
|
+
actual: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 单个值是否可无损 JSON 序列化。 */
|
|
35
|
+
export function isLossless(value: unknown): boolean {
|
|
36
|
+
return firstLosslessViolation(value) === undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 判定一个普通对象是否「普通原型」(Object.prototype 或 null)。 */
|
|
40
|
+
function isPlainObjectPrototype(value: object): boolean {
|
|
41
|
+
const prototype = Object.getPrototypeOf(value) as object | null
|
|
42
|
+
return prototype === null || prototype === Object.prototype
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 找出第一个破坏无损 JSON 的值;全部合规时返回 undefined。
|
|
47
|
+
* @param value - 待判定的值(通常是投影状态)。
|
|
48
|
+
* @param limit - 最多访问的节点数(防御超大对象)。
|
|
49
|
+
* @returns 首个违规点位或 undefined。
|
|
50
|
+
*/
|
|
51
|
+
export function firstLosslessViolation(value: unknown, limit = 200_000): LosslessViolation | undefined {
|
|
52
|
+
const ancestors = new Set<object>()
|
|
53
|
+
const stack: { node: unknown; path: string; leave?: boolean }[] = [{ node: value, path: '' }]
|
|
54
|
+
let visited = 0
|
|
55
|
+
while (stack.length > 0) {
|
|
56
|
+
const task = stack.pop()
|
|
57
|
+
if (task === undefined) break
|
|
58
|
+
if (task.leave === true) {
|
|
59
|
+
ancestors.delete(task.node as object)
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
if (visited >= limit) return { path: task.path, reason: 'cycle', actual: `node-limit(${limit})` }
|
|
63
|
+
visited += 1
|
|
64
|
+
const node = task.node
|
|
65
|
+
if (node === undefined) return { path: task.path, reason: 'undefined', actual: 'undefined' }
|
|
66
|
+
if (node === null) continue
|
|
67
|
+
switch (typeof node) {
|
|
68
|
+
case 'boolean':
|
|
69
|
+
case 'string':
|
|
70
|
+
continue
|
|
71
|
+
case 'number':
|
|
72
|
+
if (Number.isNaN(node)) return { path: task.path, reason: 'NaN', actual: 'NaN' }
|
|
73
|
+
if (node === Infinity) return { path: task.path, reason: 'Infinity', actual: 'Infinity' }
|
|
74
|
+
if (node === -Infinity) return { path: task.path, reason: '-Infinity', actual: '-Infinity' }
|
|
75
|
+
if (Object.is(node, -0)) return { path: task.path, reason: '-0', actual: '-0' }
|
|
76
|
+
continue
|
|
77
|
+
case 'function':
|
|
78
|
+
return { path: task.path, reason: 'function', actual: 'function' }
|
|
79
|
+
case 'symbol':
|
|
80
|
+
return { path: task.path, reason: 'symbol', actual: 'symbol' }
|
|
81
|
+
case 'bigint':
|
|
82
|
+
return { path: task.path, reason: 'bigint', actual: 'bigint' }
|
|
83
|
+
default:
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
const object = node as object
|
|
87
|
+
if (ancestors.has(object)) return { path: task.path, reason: 'cycle', actual: 'object-cycle' }
|
|
88
|
+
if (Array.isArray(object)) {
|
|
89
|
+
if (!isPlainArray(object)) return { path: task.path, reason: 'non-plain-prototype', actual: 'array-subclass' }
|
|
90
|
+
// 先按升序找出最低位的洞(报告口径确定),再倒序入栈以获得左→右的访问顺序。
|
|
91
|
+
for (let index = 0; index < object.length; index += 1) {
|
|
92
|
+
if (!Object.prototype.hasOwnProperty.call(object, index)) {
|
|
93
|
+
return { path: `${task.path}[${index}]`, reason: 'sparse-array-hole', actual: 'hole' }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
ancestors.add(object)
|
|
97
|
+
stack.push({ node: object, path: task.path, leave: true })
|
|
98
|
+
for (let index = object.length - 1; index >= 0; index -= 1) {
|
|
99
|
+
stack.push({ node: object[index], path: `${task.path}[${index}]` })
|
|
100
|
+
}
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
if (!isPlainObjectPrototype(object)) {
|
|
104
|
+
const name = (object as { constructor?: { name?: string } }).constructor?.name ?? 'unknown'
|
|
105
|
+
return { path: task.path, reason: 'non-plain-prototype', actual: name }
|
|
106
|
+
}
|
|
107
|
+
const keys = Reflect.ownKeys(object)
|
|
108
|
+
for (const key of keys) {
|
|
109
|
+
if (typeof key !== 'string' || !Object.prototype.propertyIsEnumerable.call(object, key)) {
|
|
110
|
+
return { path: `${task.path}.${String(key)}`, reason: 'non-enumerable-or-symbol-key', actual: typeof key }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
ancestors.add(object)
|
|
114
|
+
stack.push({ node: object, path: task.path, leave: true })
|
|
115
|
+
for (let index = keys.length - 1; index >= 0; index -= 1) {
|
|
116
|
+
const key = keys[index]
|
|
117
|
+
if (key === undefined || typeof key !== 'string') continue
|
|
118
|
+
stack.push({ node: (object as Record<string, unknown>)[key], path: `${task.path}.${key}` })
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 数组是否为普通数组(排除子类)。 */
|
|
125
|
+
function isPlainArray(value: readonly unknown[]): boolean {
|
|
126
|
+
const prototype = Object.getPrototypeOf(value) as object | null
|
|
127
|
+
return prototype === null || prototype === Array.prototype
|
|
128
|
+
}
|