dsh-model-groups 0.3.4 → 0.3.6
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 +148 -66
- package/lib/client.js +294 -4
- package/native/pi-ai.patch.json +507 -4
- package/native/settings-models.patch.json +408 -1
- package/package.json +6 -3
- package/scripts/build.mjs +6 -1
- package/scripts/doctor.mjs +22 -23
- package/scripts/native-patches.mjs +78 -13
- package/src/auto-patch.js +8 -3
- package/src/client.js +8 -4
- package/src/compatibility.js +22 -0
- package/src/index.js +26 -22
- package/src/maintenance-fallback.js +160 -0
- package/src/maintenance-host.js +250 -0
- package/src/maintenance-state.js +186 -0
- package/src/maintenance.js +188 -0
- package/src/settings-page.css +21 -0
- package/src/settings-page.js +64 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-model-groups",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "DSH
|
|
3
|
+
"version": "0.3.6",
|
|
4
|
+
"description": "DSH 网页模型选择器:供应商与密钥配置分层折叠、多密钥独立协议与安全直升维护。支持 DSH 0.2.2-rc.1。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
"lib",
|
|
29
29
|
"native/*.patch.json",
|
|
30
30
|
"scripts",
|
|
31
|
+
"!scripts/settings-models-collapse.mjs",
|
|
32
|
+
"!scripts/pi-ai-protocol.mjs",
|
|
31
33
|
"cordis.patch.yml",
|
|
32
34
|
"README.md",
|
|
33
35
|
"LICENSE"
|
|
@@ -45,7 +47,8 @@
|
|
|
45
47
|
"inject": [
|
|
46
48
|
"@deepseek-ai/dsh-client-ui-model-selection",
|
|
47
49
|
"@deepseek-ai/dsh-client-ui-renderer",
|
|
48
|
-
"@deepseek-ai/dsh-client-ui-settings-models"
|
|
50
|
+
"@deepseek-ai/dsh-client-ui-settings-models",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
49
52
|
],
|
|
50
53
|
"external": [
|
|
51
54
|
"react",
|
package/scripts/build.mjs
CHANGED
|
@@ -5,10 +5,15 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
6
6
|
const read = name => fs.readFileSync(path.join(root, name), 'utf8');
|
|
7
7
|
const withoutExports = source => source.replace(/^export /gm, '');
|
|
8
|
+
const clientVersion = JSON.parse(read('package.json')).version;
|
|
8
9
|
// Styles belong to the mounted plugin, not to module materialization: a
|
|
9
10
|
// disposal removes them and a re-applied (memoized) module must restore them.
|
|
10
11
|
const styles = `\nconst MODEL_GROUPS_CSS = ${JSON.stringify(read('src/groups.css'))};\nfunction installModelGroupsStyles() {\n const tagId = 'dsh-model-groups/groups.css';\n if (document.querySelector('style[data-plugin-css="' + tagId + '"]') !== null) return;\n const style = document.createElement('style');\n style.dataset.plugin = 'dsh-model-groups';\n style.dataset.pluginCss = tagId;\n style.textContent = MODEL_GROUPS_CSS;\n document.head.appendChild(style);\n}\n`;
|
|
11
|
-
const
|
|
12
|
+
const settingsStyles = styleInstaller('SETTINGS_PAGE_CSS', 'installSettingsPageStyles', 'src/settings-page.css', 'dsh-model-groups/settings-page.css');
|
|
13
|
+
function styleInstaller(constant, installer, file, tagId) {
|
|
14
|
+
return `\nconst ${constant} = ${JSON.stringify(read(file))};\nfunction ${installer}() {\n const tagId = ${JSON.stringify(tagId)};\n if (document.querySelector('style[data-plugin-css="' + tagId + '"]') !== null) return;\n const style = document.createElement('style');\n style.dataset.plugin = 'dsh-model-groups';\n style.dataset.pluginCss = tagId;\n style.textContent = ${constant};\n document.head.appendChild(style);\n}\n`;
|
|
15
|
+
}
|
|
16
|
+
const source = `// Generated by scripts/build.mjs; edit src instead.\nwindow.__ModuleLoader__.load({\n id: 'dsh-model-groups',\n factory(require) {\n const react = require('react');\n const react_dom = require('react-dom');\n const react_jsx_runtime = require('react/jsx-runtime');\n const _deepseek_ai_dsh_client_ui_primitives = require('@deepseek-ai/dsh-client-ui-primitives');\n${withoutExports(read('src/group-state.js'))}\n${withoutExports(read('src/hierarchy.js')).replace(/^import .*;\n/gm, '')}\n${read('src/model-select.js')}\n${withoutExports(read('src/fallback.js'))}\n${styles}\n${settingsStyles}\nconst MODEL_GROUPS_CLIENT_VERSION = ${JSON.stringify(clientVersion)};\n${withoutExports(read('src/compatibility.js'))}\n${withoutExports(read('src/maintenance-state.js')).replace(/^import .*;\n/gm, '')}\n${withoutExports(read('src/settings-page.js')).replace(/^import .*;\n/gm, '')}\n${withoutExports(read('src/client.js'))}\n return { apply, inject };\n },\n});\n`;
|
|
12
17
|
const target = path.join(root, 'lib/client.js');
|
|
13
18
|
// Compile the classic-script bundle before writing anything.
|
|
14
19
|
new Function(source);
|
package/scripts/doctor.mjs
CHANGED
|
@@ -1,39 +1,38 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import {
|
|
3
|
+
import { inspectPatches } from './native-patches.mjs';
|
|
4
|
+
import { compatibilityGuidance } from '../src/compatibility.js';
|
|
4
5
|
|
|
5
6
|
const args = process.argv.slice(2);
|
|
6
7
|
if (args.includes('--help')) {
|
|
7
|
-
console.log('Usage: node scripts/doctor.mjs --dsh-root <DSH installation directory> [--json]\nRead-only
|
|
8
|
+
console.log('Usage: node scripts/doctor.mjs --dsh-root <DSH installation directory> [--json]\nRead-only program compatibility check. No credentials, paths, or source text in diagnostics.');
|
|
8
9
|
} else {
|
|
9
10
|
try {
|
|
10
11
|
const i = args.indexOf('--dsh-root');
|
|
11
|
-
if (i < 0 || !args[i + 1] || args.some((arg, index) => index !== i + 1 && !['--dsh-root', '--json'].includes(arg)))
|
|
12
|
-
throw new Error('请用 --dsh-root 指定正在使用的 DSH 安装目录。');
|
|
13
|
-
}
|
|
12
|
+
if (i < 0 || !args[i + 1] || args.some((arg, index) => index !== i + 1 && !['--dsh-root', '--json'].includes(arg))) throw new Error('arguments');
|
|
14
13
|
const pkg = JSON.parse(fs.readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8'));
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
? '多密钥安装尚未完成:需要按 README 应用尚未安装的兼容补丁,再重启原 DSH 服务并刷新网页。'
|
|
26
|
-
: '磁盘补丁齐全。请确认原 DSH 服务已在应用补丁后重启,并刷新网页;本检查不能确认运行中的模块或浏览器缓存。',
|
|
27
|
-
};
|
|
14
|
+
const compatibility = inspectPatches(args[i + 1]);
|
|
15
|
+
const conflict = compatibility.files.some(file => file.state === 'conflict');
|
|
16
|
+
const ready = compatibility.files.every(file => file.code === 'ready');
|
|
17
|
+
const report = { plugin: pkg.name, version: pkg.version, supportedDsh: pkg.engines.dsh,
|
|
18
|
+
diskPatchesReady: ready, runtimeVerified: false, compatibility,
|
|
19
|
+
files: compatibility.files.map(file => ({ ...file, updateAvailable: file.code === 'update_available',
|
|
20
|
+
applySupported: ['ready', 'update_available', 'not_applied'].includes(file.code) })),
|
|
21
|
+
next: compatibilityGuidance(compatibility) || (ready
|
|
22
|
+
? '磁盘补丁齐全。请确认原 DSH 服务已在应用补丁后重启,并刷新网页;本检查不能证明运行中的模块已刷新。'
|
|
23
|
+
: '请在设置 → 模型分组中确认启用,再重启原 DSH 服务并刷新网页。') };
|
|
28
24
|
if (args.includes('--json')) console.log(JSON.stringify(report, null, 2));
|
|
29
25
|
else {
|
|
30
|
-
console.log(`${report.plugin} ${report.version} · DSH ${
|
|
31
|
-
for (const item of report.files) console.log(`${item.
|
|
26
|
+
console.log(`${report.plugin} ${report.version} · DSH ${compatibility.hostVersion ?? 'unknown'}`);
|
|
27
|
+
for (const item of report.files) console.log(`${item.id}: ${item.state} / ${item.code} · SHA-256 ${item.observedSha256 ?? 'unavailable'}`);
|
|
32
28
|
console.log(report.next);
|
|
33
29
|
}
|
|
34
|
-
|
|
35
|
-
} catch
|
|
36
|
-
|
|
30
|
+
process.exitCode = conflict ? 1 : ready ? 0 : 2;
|
|
31
|
+
} catch {
|
|
32
|
+
const report = { diskPatchesReady: false, runtimeVerified: false, error: 'diagnostic_unavailable',
|
|
33
|
+
next: '请用 --dsh-root 指定正在使用的 DSH 安装目录;诊断失败不代表补丁已就绪,未修改任何文件。' };
|
|
34
|
+
if (args.includes('--json')) console.log(JSON.stringify(report, null, 2));
|
|
35
|
+
else console.error(report.next);
|
|
37
36
|
process.exitCode = 1;
|
|
38
37
|
}
|
|
39
38
|
}
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { compatibilitySnapshot } from '../src/compatibility.js';
|
|
5
6
|
|
|
6
7
|
export const ARTIFACTS = ['pi-ai.patch.json', 'settings-models.patch.json', 'llm-typert.host.patch.json', 'llm-typert.remote-client.patch.json', 'api-remotes.patch.json'];
|
|
7
8
|
const pluginRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
@@ -25,13 +26,16 @@ export function transformSource(source, artifact, reverse = false) {
|
|
|
25
26
|
|
|
26
27
|
/** Validate ALL files before any modification. Check mode is read-only. */
|
|
27
28
|
export function planPatches(dshRoot, mode = 'check', artifactDirectory = path.join(pluginRoot, 'native')) {
|
|
29
|
+
return planPatchFiles(dshRoot, mode, artifactDirectory, ARTIFACTS);
|
|
30
|
+
}
|
|
31
|
+
function planPatchFiles(dshRoot, mode, artifactDirectory, names) {
|
|
28
32
|
if (!['check', 'apply', 'revert'].includes(mode)) throw new Error('Unknown patch mode');
|
|
29
33
|
const root = fs.realpathSync(dshRoot);
|
|
30
34
|
const inner = path.join(root, 'node_modules/@deepseek-ai/dsh/package.json');
|
|
31
35
|
const dsh = JSON.parse(fs.readFileSync(fs.existsSync(inner) ? inner : path.join(root, 'package.json'), 'utf8'));
|
|
32
36
|
if (dsh.name !== '@deepseek-ai/dsh' || dsh.version !== '0.2.2-rc.1') throw new Error('This patch supports only @deepseek-ai/dsh 0.2.2-rc.1');
|
|
33
37
|
const seen = new Set();
|
|
34
|
-
return
|
|
38
|
+
return names.map(name => {
|
|
35
39
|
let artifact = JSON.parse(fs.readFileSync(path.join(artifactDirectory, name), 'utf8'));
|
|
36
40
|
if (artifact.version !== '0.2.2-rc.1' || !/^@deepseek-ai\/dsh-[a-z-]+$/.test(artifact.packageName)) throw new Error('Unsupported patch package/version');
|
|
37
41
|
if (!Array.isArray(artifact.replacements) || artifact.replacements.length === 0 || !/^[0-9a-f]{64}$/.test(artifact.baseSha256) || !/^[0-9a-f]{64}$/.test(artifact.resultSha256)) throw new Error('Invalid patch manifest');
|
|
@@ -44,21 +48,82 @@ export function planPatches(dshRoot, mode = 'check', artifactDirectory = path.jo
|
|
|
44
48
|
if (pkg.name !== artifact.packageName || pkg.version !== artifact.version) throw new Error('Installed native package version mismatch');
|
|
45
49
|
const source = fs.readFileSync(file, 'utf8');
|
|
46
50
|
const hash = sha256(source);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const primary = artifact;
|
|
52
|
+
if (artifact.variants !== undefined && (!Array.isArray(artifact.variants) || artifact.variants.some(variant => !/^[0-9a-f]{64}$/.test(variant.baseSha256) || !/^[0-9a-f]{64}$/.test(variant.resultSha256) || (variant.replacements !== undefined && !Array.isArray(variant.replacements))))) throw new Error('Invalid patch variants');
|
|
53
|
+
if (primary.current !== undefined && typeof primary.current !== 'boolean' || primary.variants?.some(variant => variant.current !== undefined && typeof variant.current !== 'boolean')) throw new Error('Invalid current patch marker');
|
|
54
|
+
if (primary.requiresCurrent !== undefined && typeof primary.requiresCurrent !== 'boolean') throw new Error('Invalid required-current patch marker');
|
|
55
|
+
// current belongs to a revision, not the shared package metadata. Do not
|
|
56
|
+
// inherit primary.current onto legacy revisions when merging their fields.
|
|
57
|
+
const candidates = [primary, ...(primary.variants || []).map(variant => ({ ...primary, ...variant, current: variant.current === true }))];
|
|
58
|
+
const currentByBase = new Map();
|
|
59
|
+
for (const candidate of candidates) {
|
|
60
|
+
if (candidate.current !== true) continue;
|
|
61
|
+
if (currentByBase.has(candidate.baseSha256)) throw new Error('Ambiguous current patch variant');
|
|
62
|
+
currentByBase.set(candidate.baseSha256, candidate);
|
|
53
63
|
}
|
|
64
|
+
// A shared pristine base is intentional for legacy revisions. Prefer current
|
|
65
|
+
// output, but never guess between two different recognized patched sources.
|
|
66
|
+
const patchedMatches = candidates.filter(candidate => hash === candidate.resultSha256);
|
|
67
|
+
if (patchedMatches.length > 1) throw new Error('Ambiguous patch variant');
|
|
68
|
+
artifact = patchedMatches[0] || candidates.find(candidate => hash === candidate.baseSha256) || primary;
|
|
54
69
|
const state = hash === artifact.baseSha256 ? 'pristine' : hash === artifact.resultSha256 ? 'patched' : 'modified';
|
|
55
70
|
if (state === 'modified') throw new Error(`Refusing unrecognized content: ${artifact.target}`);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
const original = state === 'patched' ? transformSource(source, artifact, true) : source;
|
|
72
|
+
// Verify the observed revision bidirectionally, including check/no-op plans.
|
|
73
|
+
const observedPatched = transformSource(original, artifact);
|
|
74
|
+
if (state === 'patched' && observedPatched !== source) throw new Error('Patch roundtrip mismatch');
|
|
75
|
+
const applySupported = primary.requiresCurrent !== true || currentByBase.has(artifact.baseSha256);
|
|
76
|
+
// A recognized old revision can still be checked/restored. It cannot be
|
|
77
|
+
// silently left behind while dependent UI/schema files upgrade around it.
|
|
78
|
+
if (mode === 'apply' && !applySupported) throw new Error('No verified current patch for this native build; no files written');
|
|
79
|
+
const targetArtifact = currentByBase.get(artifact.baseSha256) ?? (artifact.baseSha256 === primary.baseSha256 ? primary : artifact);
|
|
80
|
+
const latest = transformSource(original, targetArtifact);
|
|
81
|
+
const updateAvailable = state === 'patched' && sha256(latest) !== hash;
|
|
82
|
+
const result = mode === 'apply' ? latest : mode === 'revert' ? original : source;
|
|
83
|
+
return { name, artifact, file, source, result, state, updateAvailable, applySupported, observedSha256: hash, baseSha256: artifact.baseSha256, targetSha256: targetArtifact.resultSha256, mode: fs.statSync(file).mode & 0o777 };
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
/** Diagnose each program independently, without weakening the all-or-nothing planner.
|
|
89
|
+
* Output is a closed schema: program hashes/versions only, never paths or errors. */
|
|
90
|
+
export function inspectPatches(dshRoot, artifactDirectory = path.join(pluginRoot, 'native')) {
|
|
91
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(pluginRoot, 'package.json'), 'utf8'));
|
|
92
|
+
let root, hostVersion = null;
|
|
93
|
+
try {
|
|
94
|
+
root = fs.realpathSync(dshRoot);
|
|
95
|
+
const inner = path.join(root, 'node_modules/@deepseek-ai/dsh/package.json');
|
|
96
|
+
const host = JSON.parse(fs.readFileSync(fs.existsSync(inner) ? inner : path.join(root, 'package.json'), 'utf8'));
|
|
97
|
+
if (host.name === '@deepseek-ai/dsh') hostVersion = host.version;
|
|
98
|
+
} catch { /* Do not expose filesystem or parser error messages. */ }
|
|
99
|
+
const files = ARTIFACTS.map(id => {
|
|
100
|
+
const item = { id, state: 'conflict', code: 'target_unavailable', installedVersion: null, observedSha256: null, baseSha256: null, targetSha256: null };
|
|
101
|
+
if (!root) return item;
|
|
102
|
+
// Collect only metadata from the exact manifest-owned, regular program path.
|
|
103
|
+
try {
|
|
104
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(artifactDirectory, id), 'utf8'));
|
|
105
|
+
const prefix = `node_modules/${manifest.packageName}/`;
|
|
106
|
+
if (!/^@deepseek-ai\/dsh-[a-z-]+$/.test(manifest.packageName) || typeof manifest.target !== 'string' || !manifest.target.startsWith(prefix) || manifest.target.includes('..') || manifest.target.includes('\\')) throw new Error();
|
|
107
|
+
const file = path.resolve(root, manifest.target);
|
|
108
|
+
if (!inside(root, file) || fs.realpathSync(file) !== file || !fs.statSync(file).isFile()) throw new Error();
|
|
109
|
+
const installed = JSON.parse(fs.readFileSync(path.join(root, 'node_modules', manifest.packageName, 'package.json'), 'utf8'));
|
|
110
|
+
if (installed.name === manifest.packageName) item.installedVersion = installed.version;
|
|
111
|
+
item.observedSha256 = sha256(fs.readFileSync(file));
|
|
112
|
+
} catch { /* Safe incomplete metadata; planner still refuses unknown targets. */ }
|
|
113
|
+
try {
|
|
114
|
+
const [plan] = planPatchFiles(root, 'check', artifactDirectory, [id]);
|
|
115
|
+
Object.assign(item, { state: plan.state, code: !plan.applySupported ? 'unsupported_patch_build' : plan.updateAvailable ? 'update_available' : plan.state === 'pristine' ? 'not_applied' : 'ready',
|
|
116
|
+
baseSha256: plan.baseSha256, targetSha256: plan.targetSha256 });
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const message = error?.message ?? '';
|
|
119
|
+
item.code = message === 'This patch supports only @deepseek-ai/dsh 0.2.2-rc.1' ? 'host_version'
|
|
120
|
+
: message === 'Installed native package version mismatch' ? 'package_version'
|
|
121
|
+
: message.startsWith('Refusing unrecognized content: ') ? 'unrecognized_content'
|
|
122
|
+
: error?.code === 'ENOENT' || error?.code === 'EACCES' || message === 'Unexpected target path or duplicate patch' ? 'target_unavailable' : 'patch_manifest';
|
|
123
|
+
}
|
|
124
|
+
return item;
|
|
61
125
|
});
|
|
126
|
+
return compatibilitySnapshot({ schema: 1, pluginVersion: pkg.version, hostVersion, supportedDsh: pkg.engines.dsh, files });
|
|
62
127
|
}
|
|
63
128
|
|
|
64
129
|
function stageReplacement(file, content, mode) {
|
|
@@ -133,7 +198,7 @@ function main(args) {
|
|
|
133
198
|
if (args.some(arg => !allowed.has(arg))) throw new Error('Unknown argument');
|
|
134
199
|
const mode = (modes[0] ?? '--check').slice(2);
|
|
135
200
|
const plans = planPatches(args[i + 1], mode);
|
|
136
|
-
for (const plan of plans) console.log(`${plan.artifact.target}: ${plan.state}`);
|
|
201
|
+
for (const plan of plans) console.log(`${plan.artifact.target}: ${plan.state}${plan.applySupported ? plan.updateAvailable ? ' (update available)' : '' : ' (recognized legacy build; no verified upgrade, apply disabled)'}`);
|
|
137
202
|
if (mode !== 'check') console.log(`${applyPlan(plans)} native files changed. Restart the existing DSH service and refresh its page; the live process is not reloaded by this script.`);
|
|
138
203
|
}
|
|
139
204
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
package/src/auto-patch.js
CHANGED
|
@@ -4,6 +4,7 @@ import os from 'node:os';
|
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { planPatches, applyPlan, sha256 } from '../scripts/native-patches.mjs';
|
|
7
|
+
import { restorationPending } from './maintenance.js';
|
|
7
8
|
|
|
8
9
|
export function findDshRoot(entry = process.argv[1]) {
|
|
9
10
|
if (!entry) throw new Error('无法确定 DSH 进程入口。');
|
|
@@ -14,9 +15,11 @@ export function findDshRoot(entry = process.argv[1]) {
|
|
|
14
15
|
while (true) {
|
|
15
16
|
const candidate = path.join(dir, 'node_modules/@deepseek-ai/dsh-llm-pi-ai/package.json');
|
|
16
17
|
if (fs.existsSync(candidate) && fs.realpathSync(candidate) === resolved) {
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// Discovery must survive unknown program hashes so maintenance can explain
|
|
19
|
+
// the conflict. The planner still validates every target before any write.
|
|
20
|
+
const inner = path.join(dir, 'node_modules/@deepseek-ai/dsh/package.json');
|
|
21
|
+
const host = JSON.parse(fs.readFileSync(fs.existsSync(inner) ? inner : path.join(dir, 'package.json'), 'utf8'));
|
|
22
|
+
if (host.name === '@deepseek-ai/dsh') return dir;
|
|
20
23
|
}
|
|
21
24
|
const parent = path.dirname(dir);
|
|
22
25
|
if (parent === dir) break;
|
|
@@ -26,6 +29,7 @@ export function findDshRoot(entry = process.argv[1]) {
|
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
export function autoPatch(root, home = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')) {
|
|
32
|
+
if (restorationPending(root, home)) return { state: 'restore-pending', changed: 0, runtimeVerified: false };
|
|
29
33
|
const plans = planPatches(root, 'apply');
|
|
30
34
|
const pending = plans.some(plan => plan.source !== plan.result);
|
|
31
35
|
if (!pending) return { state: 'disk-ready', changed: 0, runtimeVerified: false };
|
|
@@ -35,6 +39,7 @@ export function autoPatch(root, home = process.env.DSH_HOME || path.join(os.home
|
|
|
35
39
|
try { fs.mkdirSync(lock, { mode: 0o700 }); }
|
|
36
40
|
catch { throw new Error('补丁安装已被锁定;可能有另一进程正在安装。确认没有安装任务后再处理锁目录,勿并发重试。'); }
|
|
37
41
|
try {
|
|
42
|
+
if (restorationPending(root, home)) return { state: 'restore-pending', changed: 0, runtimeVerified: false };
|
|
38
43
|
const changed = applyPlan(planPatches(root, 'apply'), path.join(storage, 'backups'));
|
|
39
44
|
if (!planPatches(root, 'check').every(plan => plan.state === 'patched')) throw new Error('自动补丁复查失败。');
|
|
40
45
|
return { state: changed ? 'restart-required' : 'disk-ready', changed, runtimeVerified: false };
|
package/src/client.js
CHANGED
|
@@ -5,6 +5,10 @@ const inject = ['slots', 'modelDirectories', 'sessions', 'locale', 'remote', 're
|
|
|
5
5
|
export function apply(ctx) {
|
|
6
6
|
installModelSelectStyles();
|
|
7
7
|
installModelGroupsStyles();
|
|
8
|
+
installSettingsPageStyles();
|
|
9
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
10
|
+
name: 'settings.section', id: 'dsh-model-groups', order: 35, label: '模型分组',
|
|
11
|
+
}, ModelGroupsSettingsPage));
|
|
8
12
|
const providersForm = ctx.configForms?.get('llm-pi-ai');
|
|
9
13
|
let statusDisposed = false;
|
|
10
14
|
let statusNotice;
|
|
@@ -14,11 +18,11 @@ export function apply(ctx) {
|
|
|
14
18
|
if (statusDisposed || (!status.restartRequired && !status.failed)) return;
|
|
15
19
|
statusNotice = document.createElement('div');
|
|
16
20
|
statusNotice.dataset.plugin = 'dsh-model-groups';
|
|
17
|
-
statusNotice.setAttribute('role', '
|
|
18
|
-
statusNotice.style.cssText = 'position:fixed;
|
|
21
|
+
statusNotice.setAttribute('role', 'status');
|
|
22
|
+
statusNotice.style.cssText = 'position:fixed;bottom:16px;right:16px;max-width:min(360px,80vw);z-index:1000;background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#222);padding:12px;border:1px solid currentColor;border-radius:8px;box-sizing:border-box;pointer-events:none;';
|
|
19
23
|
statusNotice.textContent = status.failed
|
|
20
|
-
? '多密钥自动补丁未完成。请回到 App
|
|
21
|
-
: '多密钥补丁已自动安装。请先保存其他页面的草稿,再到 App
|
|
24
|
+
? '多密钥自动补丁未完成。请回到 App 对话查看引擎日志或运行安装诊断,勿强制覆盖。请暂勿编辑模型配置;可到「设置 → 模型分组」检查并安全恢复。'
|
|
25
|
+
: '多密钥补丁已自动安装。请先保存其他页面的草稿,再到 App 重启原引擎并刷新此网页。请暂勿编辑模型配置,避免新旧模块混用;维护入口仍可使用。';
|
|
22
26
|
document.body.appendChild(statusNotice);
|
|
23
27
|
}).catch(() => {});
|
|
24
28
|
ctx.on('dispose', () => { statusDisposed = true; statusNotice?.remove(); });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Public program metadata only. Never include paths, source text, configuration,
|
|
2
|
+
// credentials, environment variables, or arbitrary exception messages.
|
|
3
|
+
export const PATCH_IDS = ['pi-ai.patch.json', 'settings-models.patch.json', 'llm-typert.host.patch.json', 'llm-typert.remote-client.patch.json', 'api-remotes.patch.json'];
|
|
4
|
+
export const COMPATIBILITY_CODES = ['ready', 'update_available', 'not_applied', 'unsupported_patch_build', 'host_version', 'package_version', 'unrecognized_content', 'target_unavailable', 'patch_manifest'];
|
|
5
|
+
export const publicVersion = value => typeof value === 'string' && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]{1,40})?$/.test(value) ? value : null;
|
|
6
|
+
export const publicHash = value => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value) ? value : null;
|
|
7
|
+
export function compatibilitySnapshot(value) {
|
|
8
|
+
if (!value || value.schema !== 1 || !Array.isArray(value.files) || value.files.length > PATCH_IDS.length) return null;
|
|
9
|
+
return { schema: 1, pluginVersion: publicVersion(value.pluginVersion), hostVersion: publicVersion(value.hostVersion), supportedDsh: publicVersion(value.supportedDsh),
|
|
10
|
+
files: value.files.filter(item => item && PATCH_IDS.includes(item.id)).map(item => ({ id: item.id,
|
|
11
|
+
state: ['patched', 'pristine', 'conflict'].includes(item.state) ? item.state : 'conflict',
|
|
12
|
+
code: COMPATIBILITY_CODES.includes(item.code) ? item.code : 'patch_manifest',
|
|
13
|
+
installedVersion: publicVersion(item.installedVersion), observedSha256: publicHash(item.observedSha256),
|
|
14
|
+
baseSha256: publicHash(item.baseSha256), targetSha256: publicHash(item.targetSha256) })) };
|
|
15
|
+
}
|
|
16
|
+
export function compatibilityGuidance(snapshot) {
|
|
17
|
+
const codes = snapshot?.files.map(item => item.code) ?? [];
|
|
18
|
+
if (codes.includes('host_version') || codes.includes('package_version')) return '宿主程序版本不在本版支持范围。先核对插件与 DSH 的兼容版本;不要强行降级、覆盖文件或删除配置。';
|
|
19
|
+
if (codes.includes('unrecognized_content') || codes.includes('target_unavailable') || codes.includes('patch_manifest') || codes.includes('unsupported_patch_build')) return '已停止整批更新,未覆盖未知文件。先在原 profile 更新最新版插件,重启原引擎并刷新网页后复查;仍不支持时,复制脱敏诊断反馈。不要先卸载、还原补丁或删除配置;如需进一步适配,只提供被标记的程序文件,不提供密钥、配置或聊天记录。';
|
|
20
|
+
if (codes.includes('update_available')) return '已识别历史补丁,可直接更新到最新版,不需要逐版安装或先关闭多密钥。请保存草稿、停止任务,确认更新后重启原引擎并刷新网页。';
|
|
21
|
+
return '';
|
|
22
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
|
-
import
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { findDshRoot } from './auto-patch.js';
|
|
4
|
+
import { createMaintenance, createUnavailableMaintenance, maintenanceHandler } from './maintenance.js';
|
|
5
|
+
import { createHostInspection } from './maintenance-host.js';
|
|
2
6
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
7
|
+
// Neither disposal nor ordinary package removal restores patches: HMR and updates
|
|
8
|
+
// also dispose plugins. Only the explicitly confirmed maintenance transaction does.
|
|
5
9
|
export function apply(ctx) {
|
|
6
10
|
const key = Symbol.for('dsh-model-groups.patch-state');
|
|
7
11
|
const state = globalThis[key] ??= { restartRequired: false, error: null };
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
kind: 'exact', path: '/dsh-model-groups/status',
|
|
11
|
-
handler(req, res) {
|
|
12
|
-
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
13
|
-
res.end(JSON.stringify({ restartRequired: state.restartRequired, failed: state.error !== null }));
|
|
14
|
-
},
|
|
15
|
-
}));
|
|
16
|
-
});
|
|
12
|
+
const home = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
13
|
+
let root;
|
|
17
14
|
try {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} catch (error) {
|
|
26
|
-
state.error = error.message;
|
|
27
|
-
ctx.logger.error(`多密钥自动安装未完成:${error.message}。未强制覆盖未知文件,请运行安装诊断;不要把模型分组出现视为多密钥已就绪。`);
|
|
15
|
+
root = findDshRoot();
|
|
16
|
+
state.error = null;
|
|
17
|
+
state.restartRequired = false;
|
|
18
|
+
ctx.logger.info('多密钥补丁由设置页手动管理;插件启动不会修改宿主文件。');
|
|
19
|
+
} catch {
|
|
20
|
+
state.error = 'patch-validation-failed';
|
|
21
|
+
ctx.logger.error('多密钥安装校验失败,未强制覆盖未知文件。请查看维护页。');
|
|
28
22
|
}
|
|
23
|
+
ctx.inject(['webServer', 'connection', 'pluginManager'], child => {
|
|
24
|
+
const controller = root ? createMaintenance({ root, home, inspect: createHostInspection(child, root, home), manager: child.pluginManager }) : createUnavailableMaintenance();
|
|
25
|
+
child.effect(() => child.webServer.register({ kind: 'exact', path: '/dsh-model-groups/maintenance', handler: maintenanceHandler(controller, child.connection) }));
|
|
26
|
+
child.effect(() => child.webServer.register({ kind: 'exact', path: '/dsh-model-groups/status', handler(req, res) {
|
|
27
|
+
const admission = child.connection.admit(req);
|
|
28
|
+
if (!admission?.peer || admission.peer.principal?.kind !== 'owner') { res.writeHead(admission?.rejection || 403); res.end(); return; }
|
|
29
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
30
|
+
res.end(JSON.stringify({ restartRequired: state.restartRequired, failed: state.error !== null }));
|
|
31
|
+
} }));
|
|
32
|
+
});
|
|
29
33
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const alias = value => typeof value === 'string' && value.startsWith('@dsh-key/');
|
|
4
|
+
const validId = value => typeof value === 'string' && value.length > 0 && value.length <= 512;
|
|
5
|
+
const selection = value => {
|
|
6
|
+
if (!value || !validId(value.provider) || !validId(value.model) ||
|
|
7
|
+
(value.reasoningEffort !== undefined && !validId(value.reasoningEffort))) throw new Error('INVALID_SELECTION');
|
|
8
|
+
return { provider: value.provider, model: value.model,
|
|
9
|
+
...(value.reasoningEffort === undefined ? {} : { reasoningEffort: value.reasoningEffort }) };
|
|
10
|
+
};
|
|
11
|
+
const equal = (a, b) => !!a && !!b && a.provider === b.provider && a.model === b.model && a.reasoningEffort === b.reasoningEffort;
|
|
12
|
+
const fail = code => Object.assign(new Error(code), { code });
|
|
13
|
+
const clone = value => JSON.parse(JSON.stringify(value));
|
|
14
|
+
const unsupported = () => [
|
|
15
|
+
{ code: 'COLD_SESSIONS_UNSUPPORTED', message: 'Cold sessions cannot be safely changed by this adapter.' },
|
|
16
|
+
{ code: 'HISTORICAL_REFERENCES_UNSUPPORTED', message: 'Historical request headers and replay/fork references are not rewritten.' },
|
|
17
|
+
{ code: 'OTHER_REFERENCES_UNSUPPORTED', message: 'Agent options, subagent descriptors, presets, other profiles and processes are not migrated.' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/** Verified installed service spellings. Descriptive only: this module imports no host code. */
|
|
21
|
+
export const FALLBACK_HOST_APIS = Object.freeze({
|
|
22
|
+
live: 'ctx.agents.get(sessionId); agent.model.current(); agent.model.select(selection)',
|
|
23
|
+
durability: 'await ctx.sessions.flush(agent.session); await ctx.sessionQuery.readSession(sessionId)',
|
|
24
|
+
defaults: 'ctx.agentDefaultModel.currentSelection(); ctx.agentDefaultModel.remembersLastSelection(); await ctx.agentDefaultModel.saveSelection(next, { ifCurrent: previous })',
|
|
25
|
+
catalog: 'session.modelCatalog() uses ctx.llm.listProviders/listModels/resolveModelInfo; NOT called here because discovery may perform network I/O',
|
|
26
|
+
limitation: 'No host session-selection CAS or cross-service transaction exists; synchronous local check+select is not a process-wide maintenance lock.',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* UNWIRED, local-only injected adapter. No discovery, inference, credential access, file
|
|
31
|
+
* writes, cold-agent creation, automatic default update or native patch operations.
|
|
32
|
+
*
|
|
33
|
+
* `catalog` is a detached ALREADY OBTAINED ordinary catalog: { groups: [{ id, models:
|
|
34
|
+
* [{ id, reasoning?: { efforts: [{id}] } }] }] }. `ordinaryProviders` is an explicit
|
|
35
|
+
* allowlist verified by the future caller from local configuration: the catalog alone
|
|
36
|
+
* cannot establish that a provider has no multi-key routing. Never derive it by merely
|
|
37
|
+
* excluding alias IDs. No provider discovery or settings/secret reads occur here.
|
|
38
|
+
*
|
|
39
|
+
* Inject formal services `agents`, `sessions`, `defaultModel`, and optional
|
|
40
|
+
* `readPersistedSelection(id)` (read-only; return the effective current selection).
|
|
41
|
+
* The future reader must fold model/selection and request/header events, not old
|
|
42
|
+
* request headers alone. `sessions.flush` must return true (a durability listener).
|
|
43
|
+
*
|
|
44
|
+
* Session CAS is a synchronous identity + full selection + seq + idle check immediately
|
|
45
|
+
* followed by the synchronous AgentModel.select. No await is permitted between them.
|
|
46
|
+
* This is same-process CAS only; it does NOT lock out a later prompt or other process.
|
|
47
|
+
* Defaults use the host's lock-held ifCurrent CAS and reject fixed-default mode.
|
|
48
|
+
* Each ticket changes exactly ONE explicitly chosen target. Partial failures are
|
|
49
|
+
* reported, never rolled back or represented as successful migration/removal safety.
|
|
50
|
+
*/
|
|
51
|
+
export function createFallbackSelection({ catalog, ordinaryProviders = [], agents, sessions,
|
|
52
|
+
defaultModel, readPersistedSelection, now = Date.now, token = () => randomBytes(24).toString('hex'), ttlMs = 120000 } = {}) {
|
|
53
|
+
const allowed = new Set(ordinaryProviders);
|
|
54
|
+
const choices = [];
|
|
55
|
+
for (const group of catalog?.groups ?? []) {
|
|
56
|
+
if (!validId(group.id) || alias(group.id) || !allowed.has(group.id)) continue;
|
|
57
|
+
for (const model of group.models ?? []) {
|
|
58
|
+
if (!validId(model.id) || alias(model.id)) continue;
|
|
59
|
+
const efforts = (model.reasoning?.efforts ?? []).map(effort => effort.id).filter(validId);
|
|
60
|
+
if (choices.some(item => item.provider === group.id && item.model === model.id)) throw fail('AMBIGUOUS_CATALOG');
|
|
61
|
+
choices.push({ provider: group.id, model: model.id, efforts });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const tickets = new Map();
|
|
65
|
+
let busy = false;
|
|
66
|
+
const choice = input => {
|
|
67
|
+
const next = selection(input);
|
|
68
|
+
if (Object.keys(input).some(key => !['provider', 'model', 'reasoningEffort'].includes(key))) throw fail('INVALID_SELECTION');
|
|
69
|
+
const item = choices.find(item => item.provider === next.provider && item.model === next.model);
|
|
70
|
+
if (!item || alias(next.provider) || alias(next.model) ||
|
|
71
|
+
(next.reasoningEffort !== undefined && !item.efforts.includes(next.reasoningEffort))) throw fail('NOT_ORDINARY_CATALOG_SELECTION');
|
|
72
|
+
return next;
|
|
73
|
+
};
|
|
74
|
+
const readLive = id => {
|
|
75
|
+
const agent = agents?.get?.(id);
|
|
76
|
+
if (!agent || agent.id !== id || !agent.session || typeof agent.model?.current !== 'function' || typeof agent.model?.select !== 'function') throw fail('LIVE_SESSION_UNSUPPORTED');
|
|
77
|
+
if (agent.status !== 'idle' || (agent.phase && agent.phase.kind !== 'idle')) throw fail('SESSION_BUSY');
|
|
78
|
+
if (!Number.isSafeInteger(agent.session.seq) || agent.session.seq < 0) throw fail('SESSION_REVISION_UNAVAILABLE');
|
|
79
|
+
return { agent, session: agent.session, seq: agent.session.seq, current: selection(agent.model.current()) };
|
|
80
|
+
};
|
|
81
|
+
const readDefault = () => {
|
|
82
|
+
if (typeof defaultModel?.currentSelection !== 'function' || typeof defaultModel?.remembersLastSelection !== 'function' || typeof defaultModel?.saveSelection !== 'function') throw fail('DEFAULT_API_UNAVAILABLE');
|
|
83
|
+
if (defaultModel.remembersLastSelection() !== true) throw fail('FIXED_DEFAULT_UNSUPPORTED');
|
|
84
|
+
return selection(defaultModel.currentSelection());
|
|
85
|
+
};
|
|
86
|
+
function preview({ target, next } = {}) {
|
|
87
|
+
const blockers = [];
|
|
88
|
+
let selected, previous, observation, cleanTarget;
|
|
89
|
+
try {
|
|
90
|
+
selected = choice(next);
|
|
91
|
+
if (target?.kind === 'session' && validId(target.sessionId) && Object.keys(target).every(key => ['kind', 'sessionId'].includes(key))) {
|
|
92
|
+
cleanTarget = { kind: 'session', sessionId: target.sessionId };
|
|
93
|
+
if (typeof sessions?.flush !== 'function' || typeof readPersistedSelection !== 'function') throw fail('DURABILITY_VERIFICATION_UNAVAILABLE');
|
|
94
|
+
observation = readLive(target.sessionId);
|
|
95
|
+
previous = observation.current;
|
|
96
|
+
} else if (target?.kind === 'default' && Object.keys(target).length === 1) {
|
|
97
|
+
cleanTarget = { kind: 'default' };
|
|
98
|
+
previous = readDefault();
|
|
99
|
+
} else throw fail('TARGET_UNSUPPORTED');
|
|
100
|
+
if (!alias(previous.model) && !alias(previous.provider)) throw fail('NOT_ALIAS_SELECTION');
|
|
101
|
+
} catch (error) { blockers.push({ code: error.code || 'INVALID_SELECTION' }); }
|
|
102
|
+
let ticket = null;
|
|
103
|
+
if (!blockers.length) {
|
|
104
|
+
// Bound memory and invalidate expired confirmations without exposing observations.
|
|
105
|
+
for (const [key, value] of tickets) if (value.expires <= now()) tickets.delete(key);
|
|
106
|
+
if (tickets.size >= 128) tickets.delete(tickets.keys().next().value);
|
|
107
|
+
ticket = token();
|
|
108
|
+
if (typeof ticket !== 'string' || !ticket || tickets.has(ticket)) throw fail('TOKEN_UNAVAILABLE');
|
|
109
|
+
tickets.set(ticket, { target: cleanTarget, next: selected, previous, observation, expires: now() + ttlMs });
|
|
110
|
+
}
|
|
111
|
+
return { canApply: blockers.length === 0 && !busy, token: ticket,
|
|
112
|
+
target: cleanTarget ? { ...cleanTarget } : null, previous: previous ? { ...previous } : null, next: selected ? { ...selected } : null,
|
|
113
|
+
blockers, removalBlockers: unsupported(), safeToRemovePatches: false,
|
|
114
|
+
scope: 'one-current-selection-only', catalogFreshness: 'caller-supplied-snapshot' };
|
|
115
|
+
}
|
|
116
|
+
async function apply(input) {
|
|
117
|
+
if (busy) throw fail('BUSY');
|
|
118
|
+
const ticket = tickets.get(input?.token);
|
|
119
|
+
if (!ticket || ticket.expires <= now() || input?.confirmation !== 'CONFIRM FALLBACK SELECTION' ||
|
|
120
|
+
Object.keys(input).some(key => !['token', 'confirmation'].includes(key))) throw fail('CONFIRMATION_REQUIRED');
|
|
121
|
+
tickets.delete(input.token);
|
|
122
|
+
busy = true;
|
|
123
|
+
let changed = false;
|
|
124
|
+
try {
|
|
125
|
+
if (ticket.target.kind === 'session') {
|
|
126
|
+
const live = readLive(ticket.target.sessionId);
|
|
127
|
+
const old = ticket.observation;
|
|
128
|
+
if (live.agent !== old.agent || live.session !== old.session || live.seq !== old.seq || !equal(live.current, ticket.previous)) throw fail('STATE_CHANGED');
|
|
129
|
+
// The only live write: synchronous check+select with no intervening await.
|
|
130
|
+
changed = true; // A throwing host operation may already have appended.
|
|
131
|
+
const result = live.agent.model.select({ ...ticket.next });
|
|
132
|
+
if (result && typeof result.then === 'function') throw fail('ASYNC_SELECTION_UNSUPPORTED');
|
|
133
|
+
if (await sessions.flush(live.session) !== true) throw fail('DURABILITY_UNCONFIRMED');
|
|
134
|
+
const persisted = selection(await readPersistedSelection(ticket.target.sessionId));
|
|
135
|
+
const after = readLive(ticket.target.sessionId);
|
|
136
|
+
if (after.agent !== live.agent || after.session !== live.session || !equal(after.current, ticket.next) || !equal(persisted, ticket.next)) throw fail('POSTCHECK_FAILED');
|
|
137
|
+
} else {
|
|
138
|
+
if (!equal(readDefault(), ticket.previous)) throw fail('STATE_CHANGED');
|
|
139
|
+
const receipt = await defaultModel.saveSelection({ ...ticket.next }, { ifCurrent: { ...ticket.previous } });
|
|
140
|
+
if (receipt?.status !== 'saved') throw fail('DEFAULT_CAS_REJECTED');
|
|
141
|
+
changed = true;
|
|
142
|
+
if (!equal(readDefault(), ticket.next)) throw fail('POSTCHECK_FAILED');
|
|
143
|
+
}
|
|
144
|
+
return { status: 'applied', target: clone(ticket.target), selected: { ...ticket.next },
|
|
145
|
+
safeToRemovePatches: false, removalBlockers: unsupported() };
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// Host errors can include secrets: do not propagate messages or causes.
|
|
148
|
+
return { status: changed ? 'changed-unverified' : 'not-applied', code: knownCode(error),
|
|
149
|
+
mayHaveChanged: changed || ticket.target.kind === 'default', safeToRemovePatches: false,
|
|
150
|
+
removalBlockers: unsupported() };
|
|
151
|
+
} finally { busy = false; }
|
|
152
|
+
}
|
|
153
|
+
return { preview, apply, catalog: () => choices.map(({ provider, model, efforts }) => ({ provider, model, efforts: [...efforts] })) };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function knownCode(error) {
|
|
157
|
+
return ['LIVE_SESSION_UNSUPPORTED', 'SESSION_BUSY', 'SESSION_REVISION_UNAVAILABLE', 'STATE_CHANGED',
|
|
158
|
+
'FIXED_DEFAULT_UNSUPPORTED', 'DEFAULT_API_UNAVAILABLE', 'DEFAULT_CAS_REJECTED', 'ASYNC_SELECTION_UNSUPPORTED',
|
|
159
|
+
'DURABILITY_UNCONFIRMED', 'POSTCHECK_FAILED'].includes(error?.code) ? error.code : 'OPERATION_FAILED';
|
|
160
|
+
}
|