@dsh-enhanced/owner-console 0.0.0-stage → 0.1.67
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/LICENSE +21 -0
- package/README.md +320 -2
- package/cordis.patch.yml +3 -0
- package/lib/art.d.ts +7 -0
- package/lib/art.d.ts.map +1 -0
- package/lib/art.js +22 -0
- package/lib/art.js.map +1 -0
- package/lib/artwork.d.ts +15 -0
- package/lib/artwork.d.ts.map +1 -0
- package/lib/artwork.js +15 -0
- package/lib/artwork.js.map +1 -0
- package/lib/build-approvals.d.ts +11 -0
- package/lib/build-approvals.d.ts.map +1 -0
- package/lib/build-approvals.js +64 -0
- package/lib/build-approvals.js.map +1 -0
- package/lib/chat-view.d.ts +50 -0
- package/lib/chat-view.d.ts.map +1 -0
- package/lib/chat-view.js +545 -0
- package/lib/chat-view.js.map +1 -0
- package/lib/chat.d.ts +345 -0
- package/lib/chat.d.ts.map +1 -0
- package/lib/chat.js +418 -0
- package/lib/chat.js.map +1 -0
- package/lib/client.d.ts +131 -0
- package/lib/client.d.ts.map +1 -0
- package/lib/client.js +17214 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +18 -0
- package/lib/index.js.map +1 -0
- package/lib/service.d.ts +78 -0
- package/lib/service.d.ts.map +1 -0
- package/lib/service.js +683 -0
- package/lib/service.js.map +1 -0
- package/lib/settings.d.ts +136 -0
- package/lib/settings.d.ts.map +1 -0
- package/lib/settings.js +31 -0
- package/lib/settings.js.map +1 -0
- package/lib/state.d.ts +35 -0
- package/lib/state.d.ts.map +1 -0
- package/lib/state.js +84 -0
- package/lib/state.js.map +1 -0
- package/lib/typert.d.ts +1307 -0
- package/lib/typert.d.ts.map +1 -0
- package/lib/typert.js +271 -0
- package/lib/typert.js.map +1 -0
- package/lib/ui.d.ts +142 -0
- package/lib/ui.d.ts.map +1 -0
- package/lib/ui.js +87 -0
- package/lib/ui.js.map +1 -0
- package/lib/version.d.ts +2 -0
- package/lib/version.d.ts.map +1 -0
- package/lib/version.js +2 -0
- package/lib/version.js.map +1 -0
- package/package.json +99 -3
package/lib/service.js
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
2
|
+
import { boundedConfig, configFacts, findEntry, isAppId, isEntryId, isEnvName, isProjectBundle, isProjectPackage, isSecretRef, } from './settings.js';
|
|
3
|
+
import { INTAKE_AGREEMENT_LEVELS, INTAKE_AGREEMENT_OUTCOMES, LARK_CONNECTIONS, LARK_ERROR_CODES, OwnerConsoleIntakeAgreementSchema, OwnerConsoleTrendSnapshotsSchema, TREND_SNAPSHOT_PAGE_MAX, } from './typert.js';
|
|
4
|
+
import { componentState, missingRemedy, repairPatch } from './state.js';
|
|
5
|
+
import { persistAllowedBuilds } from './build-approvals.js';
|
|
6
|
+
/** Companions worth surfacing; a missing one is a setup hint, never a hard failure. */
|
|
7
|
+
const DEPENDENCIES = ['assistantPolicy', 'assistantDelivery', 'assistantSkills'];
|
|
8
|
+
const LARK = /(^|[-_/])lark(-channel)?($|[-_/])/u;
|
|
9
|
+
const TRAEX = /traex/iu;
|
|
10
|
+
const APP_ID_ENV = ['LARK_APP_ID', 'FEISHU_APP_ID'];
|
|
11
|
+
const ACCOUNT = /^[A-Za-z0-9][A-Za-z0-9._@/-]{0,255}$/u;
|
|
12
|
+
/** Read-only status plus owner-clicked, one-step writes through the Host settings service.
|
|
13
|
+
* Only `.volatile()` config fields are editable by that service; every write addresses one
|
|
14
|
+
* entry, uses CAS on its revision, and returns a business reason instead of throwing. */
|
|
15
|
+
export class OwnerConsoleService extends TypertRemoteService {
|
|
16
|
+
config;
|
|
17
|
+
host;
|
|
18
|
+
constructor(ctx, config) {
|
|
19
|
+
super(ctx, 'ownerConsole');
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.host = ctx;
|
|
22
|
+
}
|
|
23
|
+
settings() {
|
|
24
|
+
const settings = this.host.get('settings');
|
|
25
|
+
if (settings === undefined)
|
|
26
|
+
throw new Error('Host settings service is unavailable; this console cannot edit configuration');
|
|
27
|
+
return settings;
|
|
28
|
+
}
|
|
29
|
+
/** Host-supplied profile directory; only the `allowBuilds` fallback writes there. */
|
|
30
|
+
profileDirectory() {
|
|
31
|
+
const profile = this.host.get('profileContext');
|
|
32
|
+
return typeof profile?.dir === 'string' && profile.dir !== '' ? profile.dir : undefined;
|
|
33
|
+
}
|
|
34
|
+
descriptors() {
|
|
35
|
+
const settings = this.host.get('settings');
|
|
36
|
+
return settings?.describe({ redactSecrets: true }) ?? [];
|
|
37
|
+
}
|
|
38
|
+
/** Volatile edits update the running fiber references but never re-run apply.
|
|
39
|
+
* A one-step write therefore restarts just this entry with its persisted raw
|
|
40
|
+
* config. `fiber.update()` is synchronous while the restart is not, so the note
|
|
41
|
+
* only claims a reload when the entry is active again in a fresh `describe()`;
|
|
42
|
+
* anything else says a restart is needed. This never throws. */
|
|
43
|
+
async reload(entryId) {
|
|
44
|
+
const loader = this.host.get('loader');
|
|
45
|
+
if (loader === undefined)
|
|
46
|
+
return ';已保存,但 loader 不可用,重启 profile 后生效';
|
|
47
|
+
try {
|
|
48
|
+
const entry = [...loader.entries()].find(candidate => candidate.options.id === entryId || candidate.id === entryId);
|
|
49
|
+
if (entry === undefined)
|
|
50
|
+
return ';已保存,但未找到该 profile 条目,重启 profile 后生效';
|
|
51
|
+
if (entry.fiber === undefined)
|
|
52
|
+
await entry.refresh();
|
|
53
|
+
else
|
|
54
|
+
entry.fiber.update(entry.options.config, true);
|
|
55
|
+
return this.descriptors().some(descriptor => descriptor.ns === entryId)
|
|
56
|
+
? ';条目已重新加载'
|
|
57
|
+
: ';已保存,但未能确认条目已恢复加载;如未生效请重启 profile';
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return ';已保存,但自动重载失败,请重启 profile 后生效';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Why a card's write target is not active, in the panel's own terms. It reports the observed
|
|
64
|
+
* fiber phase/diagnostic, the missing companion services and a minimal patch repair instead of
|
|
65
|
+
* the generic "not installed" refusal that hides a failed or pending load. */
|
|
66
|
+
async targetRefusal(entry, packageName, label) {
|
|
67
|
+
const state = componentState(await this.status(), packageName);
|
|
68
|
+
const diagnostic = state.entry?.diagnostic === undefined ? '' : `:${boundedText(state.entry.diagnostic, 300)}`;
|
|
69
|
+
const bundleError = state.error === undefined ? '' : `;Host 记录错误 ${state.error.code}`;
|
|
70
|
+
const missing = missingRemedy(state);
|
|
71
|
+
const repair = state.stalled
|
|
72
|
+
? ` 可用以下最小 patch 临时停用该行:${repairPatch(state, packageName)}` : '';
|
|
73
|
+
const note = state.kind === 'absent'
|
|
74
|
+
? `${label} 未安装;请先在面板安装 ${packageName},再配置`
|
|
75
|
+
: state.kind === 'active'
|
|
76
|
+
? `${label} 已加载,但没有声明任何可实时修改的配置字段`
|
|
77
|
+
: state.kind === 'disabled'
|
|
78
|
+
? `${label} 已安装但未启用;请先点“启用”,等条目加载后再配置`
|
|
79
|
+
: state.kind === 'unavailable' || state.kind === 'unreadable'
|
|
80
|
+
? `${label} 状态不可读:${state.label}${diagnostic}`
|
|
81
|
+
: `${label} 已启用但未加载(${state.entry?.fiberPhase ?? '无 Loader 条目'}${bundleError})${diagnostic}。`
|
|
82
|
+
+ `${missing === '' ? '' : `${missing}。`}修复后重启 profile 再配置。${repair}`;
|
|
83
|
+
return refuse(entry, note);
|
|
84
|
+
}
|
|
85
|
+
/** Read the keychain handles only when the provider exposes them; a provider that cannot list
|
|
86
|
+
* (or throws) fails soft, and the write proceeds with a note that existence was not verified. */
|
|
87
|
+
keychainHandles() {
|
|
88
|
+
const keychain = this.host.get('credentialsKeychain');
|
|
89
|
+
try {
|
|
90
|
+
if (typeof keychain?.listHandles !== 'function')
|
|
91
|
+
return undefined;
|
|
92
|
+
const handles = keychain.listHandles();
|
|
93
|
+
return Array.isArray(handles)
|
|
94
|
+
? handles.flatMap(handle => typeof handle?.id === 'string' ? [handle.id] : [])
|
|
95
|
+
: undefined;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
manager() {
|
|
102
|
+
const manager = this.host.get('pluginManager');
|
|
103
|
+
if (manager === undefined)
|
|
104
|
+
throw new Error('Host plugin manager is unavailable; this console cannot manage bundles');
|
|
105
|
+
return manager;
|
|
106
|
+
}
|
|
107
|
+
/** One inventory read, with each list isolated: a rejecting list is reported instead of making the
|
|
108
|
+
* whole status (or the other list) fail, and an absent service is not the same fact as "nothing installed". */
|
|
109
|
+
async inventory() {
|
|
110
|
+
const manager = this.host.get('pluginManager');
|
|
111
|
+
if (manager === undefined) {
|
|
112
|
+
return Object.freeze({ bundles: Object.freeze([]), plugins: Object.freeze([]),
|
|
113
|
+
phases: Object.freeze({ bundles: 'unavailable', plugins: 'unavailable' }) });
|
|
114
|
+
}
|
|
115
|
+
const [listedBundles, listedPlugins] = await Promise.allSettled([manager.listBundles(), manager.listPlugins()]);
|
|
116
|
+
const phases = Object.freeze({
|
|
117
|
+
bundles: listedBundles.status === 'fulfilled' ? 'ok' : 'failed',
|
|
118
|
+
plugins: listedPlugins.status === 'fulfilled' ? 'ok' : 'failed',
|
|
119
|
+
});
|
|
120
|
+
const rejected = [listedBundles, listedPlugins].find(result => result.status === 'rejected');
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
// A dependency row without a bundle patch (`not-bundle`) is not manageable here; every other
|
|
123
|
+
// ManagementError stays visible so an incompatible installed bundle is never reported as absent.
|
|
124
|
+
bundles: Object.freeze(listedBundles.status === 'fulfilled'
|
|
125
|
+
? listedBundles.value.filter(bundle => isProjectPackage(bundle.name) && bundle.error?.code !== 'not-bundle')
|
|
126
|
+
: []),
|
|
127
|
+
plugins: Object.freeze(listedPlugins.status === 'fulfilled'
|
|
128
|
+
? listedPlugins.value.filter(plugin => isProjectPackage(plugin.moduleName))
|
|
129
|
+
: []),
|
|
130
|
+
phases,
|
|
131
|
+
...(rejected === undefined
|
|
132
|
+
? {}
|
|
133
|
+
: { diagnostic: boundedText(rejected.reason instanceof Error ? rejected.reason.message : String(rejected.reason), 1_024) }),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async status() {
|
|
137
|
+
const settings = this.host.get('settings');
|
|
138
|
+
const entries = settings?.describe({ redactSecrets: true }) ?? [];
|
|
139
|
+
const inventory = await this.inventory();
|
|
140
|
+
const lark = findEntry(entries, ns => LARK.test(ns));
|
|
141
|
+
const traex = findEntry(entries, ns => TRAEX.test(ns));
|
|
142
|
+
const larkValue = configFacts(lark?.value);
|
|
143
|
+
const traexValue = configFacts(traex?.value);
|
|
144
|
+
const installed = (name) => inventory.phases.bundles === 'ok'
|
|
145
|
+
&& inventory.bundles.some(bundle => bundle.name === name && bundle.installed);
|
|
146
|
+
const larkHealth = readLarkHealth(this.host.get('larkChannel'));
|
|
147
|
+
return Object.freeze({
|
|
148
|
+
feishu: Object.freeze({
|
|
149
|
+
...(lark === undefined ? {} : { entry: lark.ns }),
|
|
150
|
+
mode: larkValue['secretSource'] === 'handle' || (larkValue['secretSource'] === undefined && typeof larkValue['credentialHandle'] === 'string') ? 'handle' : 'env',
|
|
151
|
+
installed: installed('@dsh-enhanced/lark-channel'),
|
|
152
|
+
enabled: larkValue['enabled'] === true,
|
|
153
|
+
appIdConfigured: isAppId(larkValue['appId']),
|
|
154
|
+
...larkHealth,
|
|
155
|
+
}),
|
|
156
|
+
traex: Object.freeze({
|
|
157
|
+
...(traex === undefined ? {} : { entry: traex.ns }),
|
|
158
|
+
installed: installed('@dsh-enhanced/traex-acp-provider'),
|
|
159
|
+
enabled: traexValue['enabled'] === true,
|
|
160
|
+
command: typeof traexValue['command'] === 'string' ? traexValue['command'] : '',
|
|
161
|
+
}),
|
|
162
|
+
host: Object.freeze({
|
|
163
|
+
settings: settings !== undefined,
|
|
164
|
+
pluginManager: this.host.get('pluginManager') !== undefined,
|
|
165
|
+
bundles: inventory.phases.bundles,
|
|
166
|
+
plugins: inventory.phases.plugins,
|
|
167
|
+
...(inventory.diagnostic === undefined ? {} : { diagnostic: inventory.diagnostic }),
|
|
168
|
+
}),
|
|
169
|
+
dependencies: Object.freeze(DEPENDENCIES.map(name => Object.freeze({
|
|
170
|
+
name, available: this.host.get(name) !== undefined,
|
|
171
|
+
}))),
|
|
172
|
+
evaluation: Object.freeze({
|
|
173
|
+
intakeAgreement: readIntakeAgreementAvailability(this.host.get('assistantEvaluation'), this.host.get('loader')),
|
|
174
|
+
}),
|
|
175
|
+
bundles: Object.freeze(inventory.bundles.map(bundle => Object.freeze({
|
|
176
|
+
name: bundle.name,
|
|
177
|
+
...(bundle.version === undefined ? {} : { version: bundle.version }),
|
|
178
|
+
enabled: bundle.enabled,
|
|
179
|
+
installed: bundle.installed,
|
|
180
|
+
removable: bundle.removable,
|
|
181
|
+
...(bundle.error === undefined ? {} : { error: Object.freeze({
|
|
182
|
+
code: bundle.error.code ?? 'operation-error',
|
|
183
|
+
...(bundle.error.diagnostic === undefined ? {} : { diagnostic: boundedText(bundle.error.diagnostic, 2_048) }),
|
|
184
|
+
}) }),
|
|
185
|
+
rows: Object.freeze((bundle.rows ?? []).slice(0, 32).map(row => Object.freeze({
|
|
186
|
+
rowId: row.rowId,
|
|
187
|
+
moduleName: row.moduleName,
|
|
188
|
+
...(row.entryId === undefined ? {} : { entryId: row.entryId }),
|
|
189
|
+
}))),
|
|
190
|
+
}))),
|
|
191
|
+
plugins: Object.freeze(inventory.plugins.map(plugin => Object.freeze({
|
|
192
|
+
entryId: plugin.entryId,
|
|
193
|
+
moduleName: plugin.moduleName,
|
|
194
|
+
enabled: plugin.enabled,
|
|
195
|
+
fiberPhase: plugin.fiberPhase,
|
|
196
|
+
...(plugin.diagnostic === undefined ? {} : { diagnostic: boundedText(plugin.diagnostic, 4_096) }),
|
|
197
|
+
}))),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/** Read-only page of sealed ordinary-use trend snapshots from the control plane. It never seals,
|
|
201
|
+
* so viewing cannot change the ledger; `planId: ''` lists every plan and `beforeSequence: 0` starts
|
|
202
|
+
* at the newest row. Any refusal or tampered row is reported as `unverifiable`, never repaired. */
|
|
203
|
+
async trendSnapshots(planId, beforeSequence, limit) {
|
|
204
|
+
return readTrendSnapshots(this.host.get('pluginControlPlane'), planId, beforeSequence, limit);
|
|
205
|
+
}
|
|
206
|
+
/** Read-only agreement between intake judgments and owner notification outcomes for one exact
|
|
207
|
+
* Evaluation scope (workspace + preset), over Evaluation's default window. It never writes, adopts
|
|
208
|
+
* or sends; a disabled switch is reported as `disabled`, any refusal or odd shape as `unverifiable`. */
|
|
209
|
+
async intakeAgreement(workspace, preset) {
|
|
210
|
+
return readIntakeAgreement(this.host.get('assistantEvaluation'), workspace, preset);
|
|
211
|
+
}
|
|
212
|
+
/** Select or remove one installed project bundle layer; dependencies stay installed. */
|
|
213
|
+
async setBundleEnabled(name, enabled) {
|
|
214
|
+
if (!isProjectPackage(name))
|
|
215
|
+
return refuse('', '只允许管理本项目的 @dsh-enhanced/* bundle');
|
|
216
|
+
const manager = this.manager();
|
|
217
|
+
return applyChange(await manager.setBundleEnabled(name, enabled));
|
|
218
|
+
}
|
|
219
|
+
/** Enable or disable one loaded entry of a project bundle. */
|
|
220
|
+
async setPluginEnabled(entryId, enabled) {
|
|
221
|
+
const manager = this.manager();
|
|
222
|
+
if (entryId.trim() === '')
|
|
223
|
+
return refuse('', 'entryId 不能为空');
|
|
224
|
+
const inventory = await this.inventory();
|
|
225
|
+
const entry = inventory.plugins.find(plugin => plugin.entryId === entryId);
|
|
226
|
+
if (entry === undefined)
|
|
227
|
+
return refuse(entryId, '该条目不属于本项目的插件,或尚未安装');
|
|
228
|
+
return applyChange(await manager.setPluginEnabled(entryId, enabled));
|
|
229
|
+
}
|
|
230
|
+
/** Remove one project bundle layer after the owner confirms the destructive step. */
|
|
231
|
+
async removeProjectBundle(name) {
|
|
232
|
+
if (!isProjectPackage(name))
|
|
233
|
+
return refuse('', '只允许移除本项目的 @dsh-enhanced/* bundle');
|
|
234
|
+
const manager = this.manager();
|
|
235
|
+
return applyChange(await manager.removeBundle(name));
|
|
236
|
+
}
|
|
237
|
+
/** Install one project bundle with its declared dependencies, then activate it.
|
|
238
|
+
* The Host's pnpm run can stop at its dependency build-script policy. The names the console
|
|
239
|
+
* parsed from that failure are written to the profile's `allowBuilds` before the retry, then
|
|
240
|
+
* the install runs once more without an approval flag. Writing first works even when the
|
|
241
|
+
* Host's own ANSI parsing lost the names: pnpm sees a real allow decision instead of a
|
|
242
|
+
* pending placeholder, so the Host never has to record or re-approve them. */
|
|
243
|
+
async installProjectBundle(spec, approvedBuilds = []) {
|
|
244
|
+
const trimmed = spec.trim();
|
|
245
|
+
if (!isProjectPackage(trimmed)) {
|
|
246
|
+
return refuse('', '只允许安装本项目的 @dsh-enhanced/<name> 包;本地路径和 git 地址不在此面板处理');
|
|
247
|
+
}
|
|
248
|
+
const approvals = [...new Set(approvedBuilds.map(name => name.trim()))];
|
|
249
|
+
if (approvedBuilds.length > MAX_BUILD_NAMES
|
|
250
|
+
|| approvals.some(name => name === '' || name.length > 200 || /[*?\r\n]/u.test(name))) {
|
|
251
|
+
return refuse(trimmed, '批准的构建脚本名称无效');
|
|
252
|
+
}
|
|
253
|
+
const manager = this.manager();
|
|
254
|
+
if (approvals.length === 0) {
|
|
255
|
+
return applyInstall(await manager.installBundle(trimmed, { enabled: true, saveExact: true }));
|
|
256
|
+
}
|
|
257
|
+
const dir = this.profileDirectory();
|
|
258
|
+
if (dir === undefined)
|
|
259
|
+
return rejectStaleApproval(trimmed, approvals);
|
|
260
|
+
try {
|
|
261
|
+
await persistAllowedBuilds(dir, approvals);
|
|
262
|
+
}
|
|
263
|
+
catch (cause) {
|
|
264
|
+
return refuse(trimmed, `写入 allowBuilds 失败:${cause instanceof Error ? cause.message : String(cause)}`);
|
|
265
|
+
}
|
|
266
|
+
return applyInstall(await manager.installBundle(trimmed, { enabled: true, saveExact: true }));
|
|
267
|
+
}
|
|
268
|
+
/** Feishu: one owner click, two ways to pair the bot. No plaintext secret is ever accepted. */
|
|
269
|
+
async configureFeishu(appId, appSecretEnv, credentialHandle, account, tenant, discoverAppId) {
|
|
270
|
+
const settings = this.settings();
|
|
271
|
+
const lark = findEntry(this.descriptors(), ns => LARK.test(ns));
|
|
272
|
+
if (lark === undefined) {
|
|
273
|
+
return await this.targetRefusal('dsh-enhanced-lark-channel', '@dsh-enhanced/lark-channel', 'lark-channel');
|
|
274
|
+
}
|
|
275
|
+
const current = configFacts(lark.value);
|
|
276
|
+
const configured = isAppId(this.config.feishuAppId) ? this.config.feishuAppId : undefined;
|
|
277
|
+
const discovered = appId.trim() === '' && discoverAppId
|
|
278
|
+
? APP_ID_ENV.map(name => process.env[name]).find(value => isAppId(value)) ?? configured
|
|
279
|
+
: undefined;
|
|
280
|
+
const resolvedAppId = appId.trim() === '' ? discovered : appId.trim();
|
|
281
|
+
if (!isAppId(resolvedAppId)) {
|
|
282
|
+
return refuse(lark.ns, discoverAppId
|
|
283
|
+
? '一键模式需要 Host 环境中的 LARK_APP_ID 或 FEISHU_APP_ID;否则请填写 appId'
|
|
284
|
+
: 'appId 必须形如 cli_0123456789abcdef');
|
|
285
|
+
}
|
|
286
|
+
const envName = appSecretEnv.trim();
|
|
287
|
+
const handle = credentialHandle.trim();
|
|
288
|
+
if ((envName === '') === (handle === '')) {
|
|
289
|
+
return refuse(lark.ns, 'secret 引用必须二选一:环境变量名或 keychain handle');
|
|
290
|
+
}
|
|
291
|
+
if (envName !== '' && !isEnvName(envName))
|
|
292
|
+
return refuse(lark.ns, 'appSecretEnv 必须是环境变量名');
|
|
293
|
+
if (handle !== '' && !isSecretRef(handle))
|
|
294
|
+
return refuse(lark.ns, 'credentialHandle 格式无效');
|
|
295
|
+
if (envName !== '' && process.env[envName] === undefined) {
|
|
296
|
+
return refuse(lark.ns, `Host 进程中不存在环境变量 ${envName};请先设置它,或改用 keychain handle`);
|
|
297
|
+
}
|
|
298
|
+
let keychainVerified = true;
|
|
299
|
+
if (handle !== '') {
|
|
300
|
+
if (this.host.get('credentialsKeychain') === undefined) {
|
|
301
|
+
return refuse(lark.ns, 'credentials-keychain 未加载;无法引用 keychain secret');
|
|
302
|
+
}
|
|
303
|
+
const handles = this.keychainHandles();
|
|
304
|
+
keychainVerified = handles !== undefined;
|
|
305
|
+
if (handles !== undefined && !handles.includes(handle)) {
|
|
306
|
+
const known = handles.length === 0 ? '当前没有任何 handle' : `现有:${handles.slice(0, 8).join('、')}`;
|
|
307
|
+
return refuse(lark.ns, `credentials keychain 中没有 handle "${handle}"(${known});请先在 keychain 创建它,或改用环境变量引用`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const resolvedAccount = account.trim() === '' ? current['account'] : account.trim();
|
|
311
|
+
const resolvedTenant = tenant.trim() === '' ? current['tenant'] : tenant.trim();
|
|
312
|
+
if (typeof resolvedAccount !== 'string' || !ACCOUNT.test(resolvedAccount)) {
|
|
313
|
+
return refuse(lark.ns, 'account 缺失或格式无效;请填写当前 owner 的 account,避免更换身份');
|
|
314
|
+
}
|
|
315
|
+
if (typeof resolvedTenant !== 'string' || !ACCOUNT.test(resolvedTenant)) {
|
|
316
|
+
return refuse(lark.ns, 'tenant 缺失或格式无效;请填写当前 owner 的 tenant,避免更换身份');
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
await settings.mutate(lark.ns, [
|
|
320
|
+
{ op: 'set', path: ['enabled'], value: true },
|
|
321
|
+
{ op: 'set', path: ['appId'], value: resolvedAppId },
|
|
322
|
+
{ op: 'set', path: ['account'], value: resolvedAccount },
|
|
323
|
+
{ op: 'set', path: ['tenant'], value: resolvedTenant },
|
|
324
|
+
envName === '' ? { op: 'unset', path: ['appSecretEnv'] } : { op: 'set', path: ['appSecretEnv'], value: envName },
|
|
325
|
+
{ op: 'set', path: ['secretSource'], value: handle === '' ? 'env' : 'handle' },
|
|
326
|
+
handle === '' ? { op: 'unset', path: ['credentialHandle'] } : { op: 'set', path: ['credentialHandle'], value: handle },
|
|
327
|
+
], lark.revision);
|
|
328
|
+
}
|
|
329
|
+
catch (cause) {
|
|
330
|
+
return writeFailure(lark.ns, cause);
|
|
331
|
+
}
|
|
332
|
+
const reloaded = await this.reload(lark.ns);
|
|
333
|
+
const unchecked = handle !== '' && !keychainVerified ? '(未能校验 keychain handle 是否存在)' : '';
|
|
334
|
+
return Object.freeze({ applied: true, entry: lark.ns,
|
|
335
|
+
note: (handle === '' ? `secret 来自环境变量 ${envName}` : 'secret 保留在 credentials keychain') + unchecked + reloaded });
|
|
336
|
+
}
|
|
337
|
+
/** TraeX: one owner click toggles the local provider row; command is optional. */
|
|
338
|
+
async configureTraex(enabled, command) {
|
|
339
|
+
const settings = this.settings();
|
|
340
|
+
const traex = findEntry(this.descriptors(), ns => TRAEX.test(ns));
|
|
341
|
+
if (traex === undefined) {
|
|
342
|
+
return await this.targetRefusal('dsh-enhanced-traex-acp-provider', '@dsh-enhanced/traex-acp-provider', 'traex-acp-provider');
|
|
343
|
+
}
|
|
344
|
+
const value = command.trim();
|
|
345
|
+
if (value !== '' && (value.length > 1_024 || /[\r\n]/u.test(value))) {
|
|
346
|
+
return refuse(traex.ns, 'command 必须是单行且不超过 1024 字符');
|
|
347
|
+
}
|
|
348
|
+
const ops = [{ op: 'set', path: ['enabled'], value: enabled }];
|
|
349
|
+
if (value !== '')
|
|
350
|
+
ops.push({ op: 'set', path: ['command'], value });
|
|
351
|
+
try {
|
|
352
|
+
await settings.mutate(traex.ns, ops, traex.revision);
|
|
353
|
+
}
|
|
354
|
+
catch (cause) {
|
|
355
|
+
return writeFailure(traex.ns, cause);
|
|
356
|
+
}
|
|
357
|
+
const reloaded = await this.reload(traex.ns);
|
|
358
|
+
return Object.freeze({ applied: true, entry: traex.ns, note: (enabled ? 'TraeX 行已启用' : 'TraeX 行已停用') + reloaded });
|
|
359
|
+
}
|
|
360
|
+
/** Project bundles only: merge one bounded JSON object into fields the entry exposes live. */
|
|
361
|
+
async configureBundle(entry, config) {
|
|
362
|
+
const settings = this.settings();
|
|
363
|
+
if (!isEntryId(entry) || !isProjectBundle(entry)) {
|
|
364
|
+
return refuse('', '只允许修改本项目的 dsh-enhanced 条目');
|
|
365
|
+
}
|
|
366
|
+
const target = findEntry(this.descriptors(), ns => ns === entry);
|
|
367
|
+
if (target === undefined) {
|
|
368
|
+
const status = await this.status();
|
|
369
|
+
const packageName = status.bundles.find(bundle => bundle.rows.some(row => row.rowId === entry))?.name
|
|
370
|
+
?? entry.replace(/^dsh-enhanced-/u, '@dsh-enhanced/');
|
|
371
|
+
return await this.targetRefusal(entry, packageName, entry);
|
|
372
|
+
}
|
|
373
|
+
let patch;
|
|
374
|
+
try {
|
|
375
|
+
patch = boundedConfig(config);
|
|
376
|
+
}
|
|
377
|
+
catch (cause) {
|
|
378
|
+
return refuse(entry, cause instanceof Error ? cause.message : '配置无效');
|
|
379
|
+
}
|
|
380
|
+
const ops = Object.entries(patch).map(([key, value]) => ({ op: 'set', path: [key], value }));
|
|
381
|
+
if (ops.length === 0)
|
|
382
|
+
return refuse(entry, '配置为空,没有可写入的字段');
|
|
383
|
+
try {
|
|
384
|
+
await settings.mutate(entry, ops, target.revision);
|
|
385
|
+
}
|
|
386
|
+
catch (cause) {
|
|
387
|
+
return writeFailure(entry, cause);
|
|
388
|
+
}
|
|
389
|
+
const reloaded = await this.reload(entry);
|
|
390
|
+
return Object.freeze({ applied: true, entry, note: '已合并写入' + reloaded });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const ANSI_ESCAPE = String.fromCodePoint(0x1b);
|
|
394
|
+
const ANSI_SEQUENCE = new RegExp(`${ANSI_ESCAPE}\\[[0-9;]*m`, 'gu');
|
|
395
|
+
/** pnpm names the scripts it skipped on one line, coloured by default (pnpm 11 makes that a failure). */
|
|
396
|
+
const IGNORED_BUILDS = /\bIgnored build scripts: ([^\n]+)/u;
|
|
397
|
+
const MAX_BUILD_NAMES = 64;
|
|
398
|
+
const MAX_DIAGNOSTIC_CHARS = 600;
|
|
399
|
+
/** Read the build-script names pnpm reported from a raw run: ANSI escapes are stripped first, since
|
|
400
|
+
* pnpm 11 decorates the line and the Host's own parser (`dsh-plugin-manager`) can miss the boundary. */
|
|
401
|
+
export function blockedBuildNames(output) {
|
|
402
|
+
const line = IGNORED_BUILDS.exec(output.replace(ANSI_SEQUENCE, ''))?.[1];
|
|
403
|
+
if (line === undefined)
|
|
404
|
+
return Object.freeze([]);
|
|
405
|
+
const names = new Set();
|
|
406
|
+
for (const raw of line.split(',')) {
|
|
407
|
+
const name = raw.trim().replace(/\.$/u, '');
|
|
408
|
+
if (name === '' || name.length > 200 || /[*?]/u.test(name))
|
|
409
|
+
continue;
|
|
410
|
+
names.add(name);
|
|
411
|
+
if (names.size >= MAX_BUILD_NAMES)
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
return Object.freeze([...names]);
|
|
415
|
+
}
|
|
416
|
+
/** A failure diagnostic is shown as one bounded line; the full pnpm log stays in the Host's log file. */
|
|
417
|
+
function diagnosticTail(output) {
|
|
418
|
+
const text = (output ?? '').replace(ANSI_SEQUENCE, '').replace(/\s+/gu, ' ').trim();
|
|
419
|
+
return text.length > MAX_DIAGNOSTIC_CHARS ? `…${text.slice(-MAX_DIAGNOSTIC_CHARS)}` : text;
|
|
420
|
+
}
|
|
421
|
+
function requirementsLabel(requirements) {
|
|
422
|
+
const entries = Object.entries(requirements);
|
|
423
|
+
return entries.length === 0 ? '未知要求' : entries.map(([name, version]) => `${name} ${version}`).join('、');
|
|
424
|
+
}
|
|
425
|
+
/** Only names the write-result wire schema accepts reach the client; a longer Host list is
|
|
426
|
+
* approved in bounded retries instead of failing the whole result validation. */
|
|
427
|
+
function normalizeBuildNames(names) {
|
|
428
|
+
return Object.freeze((names ?? [])
|
|
429
|
+
.filter(name => name !== '' && name.length <= 200 && !/[\r\n]/u.test(name))
|
|
430
|
+
.slice(0, MAX_BUILD_NAMES));
|
|
431
|
+
}
|
|
432
|
+
/** Install-specific refusal mapping: surface every build name pnpm reported (the Host's own record
|
|
433
|
+
* may be empty when its parser misses pnpm's coloured line), and turn a raw error box or a
|
|
434
|
+
* compatibility denial into the concrete owner-facing next step. */
|
|
435
|
+
export function applyInstall(result) {
|
|
436
|
+
const pending = normalizeBuildNames(result.pendingBuilds);
|
|
437
|
+
const log = result.packageResult?.logPath === undefined
|
|
438
|
+
? '' : `;pnpm 日志:${boundedText(result.packageResult.logPath, 300)}`;
|
|
439
|
+
const error = result.error;
|
|
440
|
+
if (error === undefined)
|
|
441
|
+
return applyChange(result);
|
|
442
|
+
const incompatible = error.incompatible ?? [];
|
|
443
|
+
if (incompatible.length > 0) {
|
|
444
|
+
const packages = incompatible.map(plugin => `${plugin.name}@${plugin.version}(要求 ${requirementsLabel(plugin.requirements)},当前 DSH ${plugin.runtimeVersion})`);
|
|
445
|
+
return Object.freeze({
|
|
446
|
+
applied: false,
|
|
447
|
+
entry: result.target,
|
|
448
|
+
...(pending.length === 0 ? {} : { pendingBuilds: pending }),
|
|
449
|
+
note: `Host 拒绝了不兼容的插件版本:${packages.join(';')}。请安装兼容版本,或由主人为这个精确版本授予兼容豁免后重试。${log}`,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
const buildBlocked = result.packageResult?.kind === 'build-blocked'
|
|
453
|
+
|| /ERR_PNPM_IGNORED_BUILDS/u.test(error.diagnostic ?? '');
|
|
454
|
+
if (buildBlocked) {
|
|
455
|
+
const names = pending.length > 0 ? pending
|
|
456
|
+
: normalizeBuildNames(blockedBuildNames(error.diagnostic ?? result.packageResult?.output ?? ''));
|
|
457
|
+
const listed = names.length === 0 ? 'Host 未给出名称(完整输出在 profile 的 pnpm 日志中)' : names.join('、');
|
|
458
|
+
const next = names.length === 0
|
|
459
|
+
? 'Host 未能自动记录这次拒绝;请在 profile 的 pnpm-workspace.yaml 中写入 allowBuilds(依赖名: true),或运行 pnpm approve-builds 后重试。'
|
|
460
|
+
: pending.length > 0
|
|
461
|
+
? '确认允许这些依赖运行构建脚本后重试。'
|
|
462
|
+
: '确认允许后,面板会把这些名称写入 profile 的 allowBuilds 并重试一次。';
|
|
463
|
+
return Object.freeze({
|
|
464
|
+
applied: false,
|
|
465
|
+
entry: result.target,
|
|
466
|
+
...(names.length === 0 ? {} : { pendingBuilds: names }),
|
|
467
|
+
note: `安装被 pnpm 的依赖构建脚本策略拒绝:${listed}。${next}${log}`,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
const tail = diagnosticTail(error.diagnostic ?? result.packageResult?.output);
|
|
471
|
+
return Object.freeze({
|
|
472
|
+
applied: false,
|
|
473
|
+
entry: result.target,
|
|
474
|
+
...(pending.length === 0 ? {} : { pendingBuilds: pending }),
|
|
475
|
+
note: `${error.code}(${result.application})${tail === '' ? '' : `:${tail}`}${log}`,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
function applyChange(result) {
|
|
479
|
+
const reason = result.error === undefined
|
|
480
|
+
? result.application === 'restart-required'
|
|
481
|
+
? '需要重启 profile 后生效'
|
|
482
|
+
: result.application === 'overridden' ? '被更高优先级的覆盖项接管' : ''
|
|
483
|
+
: `${result.error.code}${result.error.diagnostic === undefined ? '' : `:${diagnosticTail(result.error.diagnostic)}`}`;
|
|
484
|
+
const warnings = (result.warnings ?? []).length === 0 ? '' : `;警告:${(result.warnings ?? []).join(';')}`;
|
|
485
|
+
const builds = (result.pendingBuilds ?? []).length === 0 ? ''
|
|
486
|
+
: `;需要在面板确认允许构建脚本:${normalizeBuildNames(result.pendingBuilds).join('、')}`;
|
|
487
|
+
return Object.freeze({
|
|
488
|
+
// A persisted change that only needs a profile restart is still applied;
|
|
489
|
+
// failed/cancelled results and validation refusals stay not applied.
|
|
490
|
+
applied: result.application === 'applied' || result.application === 'restart-required',
|
|
491
|
+
entry: result.target,
|
|
492
|
+
note: `${result.changed ? '已变更' : '无变化'}(${result.application})${reason === '' ? '' : `;${reason}`}${warnings}${builds}`,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
/** Keep a Host message or diagnostic short enough for a panel banner. */
|
|
496
|
+
function boundedText(text, limit) {
|
|
497
|
+
const normalized = text.replace(/\s+/gu, ' ').trim();
|
|
498
|
+
return normalized.length > limit ? `${normalized.slice(0, limit)}…` : normalized;
|
|
499
|
+
}
|
|
500
|
+
/** Map a rejected `settings.mutate` to a business note instead of throwing through the RPC. */
|
|
501
|
+
function writeFailure(entry, cause) {
|
|
502
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
503
|
+
const note = /not volatile/iu.test(message)
|
|
504
|
+
? `字段不能实时修改:${boundedText(message, 300)}`
|
|
505
|
+
: /changed since it was read|conflict/iu.test(message)
|
|
506
|
+
? '配置已被其他操作修改(CAS 冲突);请刷新面板后重试'
|
|
507
|
+
: /No configurable plugin entry|no volatile fields/iu.test(message)
|
|
508
|
+
? `该条目当前不可配置(未加载或没有 live 字段):${boundedText(message, 300)}。条目未加载时请重启 profile 或用 profile patch 修复`
|
|
509
|
+
: `写入失败:${boundedText(message, 400)}`;
|
|
510
|
+
return refuse(entry, note);
|
|
511
|
+
}
|
|
512
|
+
/** The Host holds no pending record for a name the owner approved, so no retry is possible. */
|
|
513
|
+
function rejectStaleApproval(entry, names) {
|
|
514
|
+
return refuse(entry, `Host 未记录这些待批准的构建脚本:${names.join('、')};且未提供 profile 目录,`
|
|
515
|
+
+ '无法写入 allowBuilds。请在 profile 的 pnpm-workspace.yaml 中写入对应条目后重试。');
|
|
516
|
+
}
|
|
517
|
+
function refuse(entry, note) {
|
|
518
|
+
return Object.freeze({ applied: false, entry, note });
|
|
519
|
+
}
|
|
520
|
+
const includes = (values, value) => typeof value === 'string' && values.includes(value);
|
|
521
|
+
/** Read-only: `larkChannel.health()` is a synchronous snapshot with no I/O. Only the
|
|
522
|
+
* enumerated state and error code pass through; anything else becomes `unknown`. */
|
|
523
|
+
function readLarkHealth(provider) {
|
|
524
|
+
if (provider === undefined || provider === null)
|
|
525
|
+
return { connected: false, connection: 'absent' };
|
|
526
|
+
let state;
|
|
527
|
+
let code;
|
|
528
|
+
try {
|
|
529
|
+
const read = provider.health;
|
|
530
|
+
const health = typeof read === 'function' ? read.call(provider) : undefined;
|
|
531
|
+
state = health?.state;
|
|
532
|
+
code = health?.lastErrorCode;
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
// An inactive or failing provider is reported, never thrown into the panel.
|
|
536
|
+
state = undefined;
|
|
537
|
+
}
|
|
538
|
+
if (!includes(LARK_CONNECTIONS, state) || state === 'absent' || state === 'unknown') {
|
|
539
|
+
return { connected: false, connection: 'unknown' };
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
connected: state === 'connected' || state === 'connected-with-gap',
|
|
543
|
+
connection: state,
|
|
544
|
+
...(includes(LARK_ERROR_CODES, code) ? { lastErrorCode: code } : {}),
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
const UNVERIFIABLE = Object.freeze({ state: 'unverifiable', causal: false, items: Object.freeze([]) });
|
|
548
|
+
const record = (value) => typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {};
|
|
549
|
+
const list = (value) => Array.isArray(value) ? value : [];
|
|
550
|
+
/** Project one control-plane snapshot page onto the bounded panel summary; content-free digests and counts only. */
|
|
551
|
+
export function readTrendSnapshots(provider, planId, beforeSequence, limit) {
|
|
552
|
+
if (provider === undefined || provider === null)
|
|
553
|
+
return Object.freeze({ state: 'absent', causal: false, items: Object.freeze([]) });
|
|
554
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > TREND_SNAPSHOT_PAGE_MAX
|
|
555
|
+
|| !Number.isSafeInteger(beforeSequence) || beforeSequence < 0)
|
|
556
|
+
return UNVERIFIABLE;
|
|
557
|
+
try {
|
|
558
|
+
const read = provider.inspectCreatedCapabilityOrdinaryUseTrendSnapshots;
|
|
559
|
+
if (typeof read !== 'function')
|
|
560
|
+
return UNVERIFIABLE;
|
|
561
|
+
const page = record(read.call(provider, {
|
|
562
|
+
...(planId.trim() === '' ? {} : { planId: planId.trim() }),
|
|
563
|
+
...(beforeSequence === 0 ? {} : { beforeSequence }),
|
|
564
|
+
limit,
|
|
565
|
+
}));
|
|
566
|
+
if (page['causal'] !== false || !Array.isArray(page['items']))
|
|
567
|
+
return UNVERIFIABLE;
|
|
568
|
+
const items = page['items'].map(item => {
|
|
569
|
+
const row = record(item);
|
|
570
|
+
const snapshot = record(row['snapshot']);
|
|
571
|
+
const trend = record(snapshot['trend']);
|
|
572
|
+
if (snapshot['causal'] !== false || trend['causal'] !== false)
|
|
573
|
+
throw new Error('causal snapshot');
|
|
574
|
+
return {
|
|
575
|
+
sequence: row['sequence'], sealedAt: row['sealedAt'], planId: snapshot['planId'], digest: snapshot['digest'],
|
|
576
|
+
inputsDigest: snapshot['inputsDigest'], trendDigest: snapshot['trendDigest'], window: snapshot['window'],
|
|
577
|
+
cohorts: trend['cohorts'], flags: snapshot['flags'],
|
|
578
|
+
strata: list(trend['strata']).map(value => {
|
|
579
|
+
const stratum = record(value);
|
|
580
|
+
const did = record(stratum['differenceInDifferences']);
|
|
581
|
+
const interval = record(did['interval95']);
|
|
582
|
+
return {
|
|
583
|
+
model: stratum['model'],
|
|
584
|
+
differenceInDifferences: did['status'] === 'estimated'
|
|
585
|
+
? { status: 'estimated', estimate: did['estimate'], low: interval['low'], high: interval['high'] }
|
|
586
|
+
: { status: 'insufficient' },
|
|
587
|
+
parallelTrend: record(stratum['parallelTrend'])['status'],
|
|
588
|
+
};
|
|
589
|
+
}),
|
|
590
|
+
};
|
|
591
|
+
});
|
|
592
|
+
const parsed = OwnerConsoleTrendSnapshotsSchema.safeParse({ state: 'ok', causal: false, items,
|
|
593
|
+
...(page['nextBeforeSequence'] === undefined ? {} : { nextBeforeSequence: page['nextBeforeSequence'] }) });
|
|
594
|
+
return parsed.success ? Object.freeze(parsed.data) : UNVERIFIABLE;
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return UNVERIFIABLE;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const INTAKE_UNVERIFIABLE = Object.freeze({ state: 'unverifiable', causal: false });
|
|
601
|
+
function intakeAgreementCell(value) {
|
|
602
|
+
const cell = record(value);
|
|
603
|
+
const outcomes = record(cell['outcomes']);
|
|
604
|
+
return {
|
|
605
|
+
notifications: cell['notifications'], items: cell['items'], agreed: cell['agreed'], disagreed: cell['disagreed'],
|
|
606
|
+
agreementPpm: cell['agreementPpm'],
|
|
607
|
+
outcomes: Object.fromEntries(INTAKE_AGREEMENT_OUTCOMES.map(outcome => [outcome, outcomes[outcome]])),
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
/** Project Evaluation's intake attention agreement onto the panel summary; counts only, no ids or content. */
|
|
611
|
+
export function readIntakeAgreement(provider, workspace, preset) {
|
|
612
|
+
if (provider === undefined || provider === null)
|
|
613
|
+
return Object.freeze({ state: 'absent', causal: false });
|
|
614
|
+
const scope = { workspace: workspace.trim(), preset: preset.trim() };
|
|
615
|
+
if (scope.workspace === '' || scope.preset === '')
|
|
616
|
+
return INTAKE_UNVERIFIABLE;
|
|
617
|
+
try {
|
|
618
|
+
const read = provider.inspectIntakeAttentionAgreement;
|
|
619
|
+
if (typeof read !== 'function')
|
|
620
|
+
return INTAKE_UNVERIFIABLE;
|
|
621
|
+
const result = read.call(provider, { scope });
|
|
622
|
+
if (result === undefined)
|
|
623
|
+
return Object.freeze({ state: 'disabled', causal: false });
|
|
624
|
+
const agreement = record(result);
|
|
625
|
+
if (agreement['protocol'] !== 'assistant-evaluation/intake-attention-agreement/v1' || agreement['causal'] !== false)
|
|
626
|
+
return INTAKE_UNVERIFIABLE;
|
|
627
|
+
const byLevel = record(agreement['byLevel']);
|
|
628
|
+
const parsed = OwnerConsoleIntakeAgreementSchema.safeParse({ state: 'ok', causal: false, summary: {
|
|
629
|
+
fromNotifiedAt: agreement['fromNotifiedAt'], toNotifiedAt: agreement['toNotifiedAt'], scanned: agreement['scanned'],
|
|
630
|
+
truncated: agreement['truncated'], provenanceAvailable: agreement['provenanceAvailable'], notIntake: agreement['notIntake'],
|
|
631
|
+
unresolved: agreement['unresolved'], foreign: agreement['foreign'], intake: intakeAgreementCell(agreement['intake']),
|
|
632
|
+
byLevel: INTAKE_AGREEMENT_LEVELS.filter(level => byLevel[level] !== undefined)
|
|
633
|
+
.map(level => ({ level, cell: intakeAgreementCell(byLevel[level]) })),
|
|
634
|
+
} });
|
|
635
|
+
return parsed.success && intakeAgreementConsistent(parsed.data.summary) ? Object.freeze(parsed.data) : INTAKE_UNVERIFIABLE;
|
|
636
|
+
}
|
|
637
|
+
catch {
|
|
638
|
+
return INTAKE_UNVERIFIABLE;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const EVALUATION_MODULE = '@dsh-enhanced/assistant-evaluation';
|
|
642
|
+
/** Scope-free and read-only: whether `intakeAgreement` can answer at all. Nothing on Evaluation is
|
|
643
|
+
* called, so no notification is scanned; the switch comes from the one enabled Evaluation loader
|
|
644
|
+
* entry's persisted config (Evaluation defaults it off). No scope, count or other config passes through. */
|
|
645
|
+
export function readIntakeAgreementAvailability(provider, loader) {
|
|
646
|
+
if (provider === undefined || provider === null)
|
|
647
|
+
return 'absent';
|
|
648
|
+
try {
|
|
649
|
+
if (typeof provider.inspectIntakeAttentionAgreement !== 'function')
|
|
650
|
+
return 'unknown';
|
|
651
|
+
const read = loader?.entries;
|
|
652
|
+
if (typeof read !== 'function')
|
|
653
|
+
return 'unknown';
|
|
654
|
+
const entries = [...read.call(loader)].filter(entry => entry.options.name === EVALUATION_MODULE && entry.options.disabled !== true);
|
|
655
|
+
// Zero or several candidates: the panel cannot tell which one provides the service.
|
|
656
|
+
if (entries.length !== 1)
|
|
657
|
+
return 'unknown';
|
|
658
|
+
const enabled = record(entries[0].options.config)['intakeAttentionAgreementEnabled'];
|
|
659
|
+
if (enabled === true)
|
|
660
|
+
return 'available';
|
|
661
|
+
return enabled === undefined || enabled === false ? 'disabled' : 'unknown';
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
return 'unknown';
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/** Evaluation's own arithmetic, re-checked so a self-contradicting answer is never shown as `ok`. */
|
|
668
|
+
function intakeAgreementConsistent(summary) {
|
|
669
|
+
const cellConsistent = (cell) => {
|
|
670
|
+
const o = cell.outcomes;
|
|
671
|
+
const total = INTAKE_AGREEMENT_OUTCOMES.reduce((sum, outcome) => sum + o[outcome], 0);
|
|
672
|
+
const decided = cell.agreed + cell.disagreed;
|
|
673
|
+
return total === cell.notifications && cell.items <= cell.notifications
|
|
674
|
+
&& cell.agreed === o.accepted + o.reacted && cell.disagreed === o.rejected + o['expired-unhandled']
|
|
675
|
+
&& cell.agreementPpm === (decided === 0 ? null : Math.round(cell.agreed * 1_000_000 / decided));
|
|
676
|
+
};
|
|
677
|
+
const levels = summary.byLevel.map(entry => entry.cell);
|
|
678
|
+
return summary.fromNotifiedAt < summary.toNotifiedAt
|
|
679
|
+
&& summary.intake.notifications + summary.notIntake + summary.unresolved + summary.foreign <= summary.scanned
|
|
680
|
+
&& cellConsistent(summary.intake) && levels.every(cellConsistent)
|
|
681
|
+
&& levels.reduce((sum, cell) => sum + cell.notifications, 0) === summary.intake.notifications;
|
|
682
|
+
}
|
|
683
|
+
//# sourceMappingURL=service.js.map
|