@klarkxy/dsh-plugin-kit 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/README.zh-CN.md +1 -1
- package/lib/auxiliary-model-client.d.ts +4 -1
- package/lib/auxiliary-model-client.js +72 -33
- package/lib/auxiliary-model-client.js.map +1 -1
- package/lib/auxiliary-model.js +26 -2
- package/lib/auxiliary-model.js.map +1 -1
- package/lib/cli-media.js +1 -1
- package/lib/cli-process.d.ts +2 -0
- package/lib/cli-process.js +36 -2
- package/lib/cli-process.js.map +1 -1
- package/lib/client-logger.d.ts +19 -0
- package/lib/client-logger.js +153 -0
- package/lib/client-logger.js.map +1 -0
- package/lib/client-utils.d.ts +2 -1
- package/lib/client-utils.js +9 -3
- package/lib/client-utils.js.map +1 -1
- package/lib/domain.d.ts +12 -4
- package/lib/domain.js +32 -6
- package/lib/domain.js.map +1 -1
- package/lib/host-rpc.d.ts +2 -0
- package/lib/host-rpc.js +71 -2
- package/lib/host-rpc.js.map +1 -1
- package/lib/index.d.ts +3 -1
- package/lib/index.js +2 -1
- package/lib/llm-call.d.ts +5 -1
- package/lib/llm-call.js +79 -46
- package/lib/llm-call.js.map +1 -1
- package/lib/log-shared.d.ts +2 -0
- package/lib/log-shared.js +169 -0
- package/lib/log-shared.js.map +1 -0
- package/lib/log-shared2.d.ts +72 -0
- package/lib/logger.d.ts +87 -0
- package/lib/logger.js +572 -0
- package/lib/logger.js.map +1 -0
- package/lib/model-picker.d.ts +34 -0
- package/lib/model-picker.js +96 -0
- package/lib/model-picker.js.map +1 -0
- package/lib/official-ui.d.ts +1 -1
- package/lib/official-ui.js +3 -1
- package/lib/official-ui.js.map +1 -1
- package/lib/zod.js +79 -79
- package/lib/zod.js.map +1 -1
- package/package.json +34 -14
package/lib/cli-process.js
CHANGED
|
@@ -58,10 +58,16 @@ var CliProcessError = class extends Error {
|
|
|
58
58
|
/** Own one process tree until it exits, including caller cancellation and protocol failure. */
|
|
59
59
|
async function runCli(executable, args, options) {
|
|
60
60
|
options.signal.throwIfAborted();
|
|
61
|
+
const started = Date.now();
|
|
62
|
+
const log = options.logger;
|
|
61
63
|
const timed = AbortSignal.timeout(options.timeoutMs ?? 9e5);
|
|
62
64
|
const signal = AbortSignal.any([options.signal, timed]);
|
|
63
65
|
const maxBytes = options.maxOutputBytes ?? 16777216;
|
|
64
66
|
const maxLine = options.maxLineBytes ?? 8388608;
|
|
67
|
+
log?.debug("cli.started", "CLI process starting.", {
|
|
68
|
+
argCount: args.length,
|
|
69
|
+
timeoutMs: options.timeoutMs ?? 9e5
|
|
70
|
+
});
|
|
65
71
|
let child;
|
|
66
72
|
try {
|
|
67
73
|
child = (options.spawnChild ?? spawn)(executable.command, [...executable.args ?? [], ...args], {
|
|
@@ -76,7 +82,8 @@ async function runCli(executable, args, options) {
|
|
|
76
82
|
"pipe"
|
|
77
83
|
]
|
|
78
84
|
});
|
|
79
|
-
} catch {
|
|
85
|
+
} catch (spawnErr) {
|
|
86
|
+
log?.error("cli.unavailable", "Unable to start CLI process.", spawnErr, { argCount: args.length });
|
|
80
87
|
throw new CliProcessError("unavailable", "Unable to start the official CLI.");
|
|
81
88
|
}
|
|
82
89
|
let failure, total = 0, stderr = "", stdout = "", pending = Buffer.alloc(0);
|
|
@@ -194,8 +201,35 @@ async function runCli(executable, args, options) {
|
|
|
194
201
|
stderr += stderrDecoder.end();
|
|
195
202
|
if (pending.length && !failure) acceptLine(pending.toString("utf8"));
|
|
196
203
|
await lineWork;
|
|
197
|
-
if (failure)
|
|
204
|
+
if (failure) {
|
|
205
|
+
const durationMs = Date.now() - started;
|
|
206
|
+
if (failure instanceof CliProcessError && failure.code === "cancelled") log?.debug("cli.cancelled", "CLI process cancelled.", {
|
|
207
|
+
durationMs,
|
|
208
|
+
outputBytes: total
|
|
209
|
+
});
|
|
210
|
+
else if (failure instanceof CliProcessError && failure.code === "timeout") log?.warn("cli.timeout", "CLI process timed out.", failure, {
|
|
211
|
+
durationMs,
|
|
212
|
+
outputBytes: total
|
|
213
|
+
});
|
|
214
|
+
else log?.error("cli.failed", "CLI process failed.", failure, {
|
|
215
|
+
code: failure instanceof CliProcessError ? failure.code : null,
|
|
216
|
+
durationMs,
|
|
217
|
+
outputBytes: total
|
|
218
|
+
});
|
|
219
|
+
throw failure;
|
|
220
|
+
}
|
|
198
221
|
signal.throwIfAborted();
|
|
222
|
+
const durationMs = Date.now() - started;
|
|
223
|
+
if (exitCode === 0) log?.debug("cli.completed", "CLI process exited cleanly.", {
|
|
224
|
+
exitCode,
|
|
225
|
+
durationMs,
|
|
226
|
+
outputBytes: total
|
|
227
|
+
});
|
|
228
|
+
else log?.warn("cli.nonzero-exit", "CLI process exited with a non-zero code.", void 0, {
|
|
229
|
+
exitCode,
|
|
230
|
+
durationMs,
|
|
231
|
+
outputBytes: total
|
|
232
|
+
});
|
|
199
233
|
return {
|
|
200
234
|
stdout,
|
|
201
235
|
stderr,
|
package/lib/cli-process.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-process.js","names":[],"sources":["../src/cli-process.ts"],"sourcesContent":["import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'\nimport { StringDecoder } from 'node:string_decoder'\n\n/** Only OS/runtime discovery facts cross the default CLI boundary. CLI-specific login configuration must opt in. */\nexport function cliEnvironment(parent: NodeJS.ProcessEnv = process.env, extra: readonly string[] = []): NodeJS.ProcessEnv {\n const allowed = new Set(['PATH', 'PATHEXT', 'SYSTEMROOT', 'WINDIR', 'COMSPEC', 'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA', 'PROGRAMDATA', 'PROGRAMFILES', 'PROGRAMFILES(X86)', 'TEMP', 'TMP', 'TMPDIR', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TZ', 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'SSL_CERT_FILE', 'SSL_CERT_DIR', ...extra.map(key => key.toUpperCase())])\n return Object.fromEntries(Object.entries(parent).filter(([key, value]) => value !== undefined && !key.toUpperCase().startsWith('DSH_') && allowed.has(key.toUpperCase())))\n}\n\n/** Run a JavaScript CLI with process.execPath even when the host is Electron.\n * Set this explicitly: desktop hosts may already have removed it from their own env.\n * Native executables continue to use cliEnvironment without this Node-mode override.\n */\nexport function cliNodeEnvironment(parent: NodeJS.ProcessEnv = process.env, extra: readonly string[] = []): NodeJS.ProcessEnv {\n const env = cliEnvironment(parent, extra)\n // Windows environment names are case-insensitive; avoid conflicting spellings.\n for (const key of Object.keys(env)) if (key.toUpperCase() === 'ELECTRON_RUN_AS_NODE') delete env[key]\n return { ...env, ELECTRON_RUN_AS_NODE: '1' }\n}\n\n/** A resolved executable, never a shell command or an argument string. */\nexport type CliExecutable = { command: string; args?: string[]; env?: NodeJS.ProcessEnv; displayPath: string }\nexport type CliOutput = { stdout: string; stderr: string; exitCode: number }\nexport class CliProcessError extends Error {\n constructor(public readonly code: string, message: string) { super(message); this.name = 'CliProcessError' }\n}\nexport type CliOptions = {\n cwd: string; signal: AbortSignal; input?: string; timeoutMs?: number;\n maxOutputBytes?: number; maxLineBytes?: number;\n onLine?: (line: string) => void | Promise<void>;\n spawnChild?: typeof spawn;\n}\n\n/** Own one process tree until it exits, including caller cancellation and protocol failure. */\nexport async function runCli(executable: CliExecutable, args: string[], options: CliOptions): Promise<CliOutput> {\n options.signal.throwIfAborted()\n const timed = AbortSignal.timeout(options.timeoutMs ?? 900_000)\n const signal = AbortSignal.any([options.signal, timed])\n const maxBytes = options.maxOutputBytes ?? 16 * 1024 * 1024\n const maxLine = options.maxLineBytes ?? 8 * 1024 * 1024\n let child: ChildProcessWithoutNullStreams\n try {\n child = (options.spawnChild ?? spawn)(executable.command, [...(executable.args ?? []), ...args], {\n cwd: options.cwd, env: executable.env ?? cliEnvironment(), shell: false,\n windowsHide: true, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'],\n }) as ChildProcessWithoutNullStreams\n } catch { throw new CliProcessError('unavailable', 'Unable to start the official CLI.') }\n let failure: unknown, total = 0, stderr = '', stdout = '', pending = Buffer.alloc(0)\n const stdoutDecoder = new StringDecoder('utf8'), stderrDecoder = new StringDecoder('utf8')\n let exited = false, exitCode = -1, stopping: Promise<void> | undefined\n let lineWork = Promise.resolve()\n const closed = new Promise<void>(resolve => {\n child.once('close', code => { exited = true; exitCode = code ?? -1; resolve() })\n })\n const stop = () => stopping ??= (async () => {\n if (exited) return\n const pid = child.pid\n if (pid && process.platform === 'win32') {\n await new Promise<void>(resolve => {\n const killer = spawn('taskkill.exe', ['/PID', String(pid), '/T', '/F'], { env: cliEnvironment(), windowsHide: true, shell: false, stdio: 'ignore' })\n let timer: ReturnType<typeof setTimeout>\n const done = () => { clearTimeout(timer); resolve() }\n killer.once('error', () => { child.kill(); done() })\n killer.once('close', done)\n timer = setTimeout(() => { killer.kill(); child.kill(); resolve() }, 5_000)\n })\n } else if (pid) {\n try { process.kill(-pid, 'SIGTERM') } catch { child.kill() }\n } else child.kill()\n let timer: ReturnType<typeof setTimeout> | undefined\n await Promise.race([closed, new Promise<void>(resolve => { timer = setTimeout(resolve, 2_000) })])\n if (timer) clearTimeout(timer)\n if (!exited) {\n try { pid && process.platform !== 'win32' ? process.kill(-pid, 'SIGKILL') : child.kill('SIGKILL') } catch { /* already exited */ }\n }\n await closed\n })()\n const fail = (error: unknown) => { failure ??= error; void stop() }\n const abort = () => fail(new CliProcessError(timed.aborted && !options.signal.aborted ? 'timeout' : 'cancelled', timed.aborted && !options.signal.aborted ? 'The CLI request timed out.' : 'The CLI request was cancelled.'))\n signal.addEventListener('abort', abort, { once: true })\n child.on('error', () => fail(new CliProcessError('unavailable', 'Unable to start the official CLI.')))\n child.stdin.on('error', () => { if (!exited) fail(new CliProcessError('process-io', 'The CLI input stream closed unexpectedly.')) })\n child.stdout.on('error', () => fail(new CliProcessError('process-io', 'The CLI output stream failed.')))\n child.stderr.on('error', () => fail(new CliProcessError('process-io', 'The CLI error stream failed.')))\n const count = (chunk: Buffer) => {\n total += chunk.length\n if (total > maxBytes) { fail(new CliProcessError('output-limit', 'The CLI output exceeded its limit.')); return false }\n return true\n }\n const acceptLine = (line: string) => {\n if (!options.onLine || !line.trim()) return\n child.stdout.pause()\n lineWork = lineWork.then(async () => { if (!failure) await options.onLine!(line) }).catch(fail).finally(() => child.stdout.resume())\n }\n child.stdout.on('data', (chunk: Buffer) => {\n if (failure || !count(chunk)) return\n if (!options.onLine) { stdout += stdoutDecoder.write(chunk); return }\n pending = Buffer.concat([pending, chunk])\n for (;;) {\n const end = pending.indexOf(10)\n if (end < 0) break\n if (end > maxLine) { fail(new CliProcessError('output-limit', 'A CLI protocol frame exceeded its limit.')); return }\n acceptLine(pending.subarray(0, end).toString('utf8'))\n pending = pending.subarray(end + 1)\n }\n if (pending.length > maxLine) fail(new CliProcessError('output-limit', 'A CLI protocol frame exceeded its limit.'))\n })\n child.stderr.on('data', (chunk: Buffer) => { if (count(chunk)) stderr = (stderr + stderrDecoder.write(chunk)).slice(-16_384) })\n try {\n if (signal.aborted) abort()\n if (!failure) child.stdin.end(options.input ?? '')\n await closed\n stdout += stdoutDecoder.end(); stderr += stderrDecoder.end()\n if (pending.length && !failure) acceptLine(pending.toString('utf8'))\n await lineWork\n if (failure) throw failure\n signal.throwIfAborted()\n return { stdout, stderr, exitCode }\n } finally {\n signal.removeEventListener('abort', abort)\n if (!exited) await stop()\n if (stopping) await stopping\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli-process.js","names":[],"sources":["../src/cli-process.ts"],"sourcesContent":["import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'\nimport { StringDecoder } from 'node:string_decoder'\nimport type { PluginLogger } from './log-shared.ts'\n\n/** Only OS/runtime discovery facts cross the default CLI boundary. CLI-specific login configuration must opt in. */\nexport function cliEnvironment(parent: NodeJS.ProcessEnv = process.env, extra: readonly string[] = []): NodeJS.ProcessEnv {\n const allowed = new Set(['PATH', 'PATHEXT', 'SYSTEMROOT', 'WINDIR', 'COMSPEC', 'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA', 'PROGRAMDATA', 'PROGRAMFILES', 'PROGRAMFILES(X86)', 'TEMP', 'TMP', 'TMPDIR', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TZ', 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'SSL_CERT_FILE', 'SSL_CERT_DIR', ...extra.map(key => key.toUpperCase())])\n return Object.fromEntries(Object.entries(parent).filter(([key, value]) => value !== undefined && !key.toUpperCase().startsWith('DSH_') && allowed.has(key.toUpperCase())))\n}\n\n/** Run a JavaScript CLI with process.execPath even when the host is Electron.\n * Set this explicitly: desktop hosts may already have removed it from their own env.\n * Native executables continue to use cliEnvironment without this Node-mode override.\n */\nexport function cliNodeEnvironment(parent: NodeJS.ProcessEnv = process.env, extra: readonly string[] = []): NodeJS.ProcessEnv {\n const env = cliEnvironment(parent, extra)\n // Windows environment names are case-insensitive; avoid conflicting spellings.\n for (const key of Object.keys(env)) if (key.toUpperCase() === 'ELECTRON_RUN_AS_NODE') delete env[key]\n return { ...env, ELECTRON_RUN_AS_NODE: '1' }\n}\n\n/** A resolved executable, never a shell command or an argument string. */\nexport type CliExecutable = { command: string; args?: string[]; env?: NodeJS.ProcessEnv; displayPath: string }\nexport type CliOutput = { stdout: string; stderr: string; exitCode: number }\nexport class CliProcessError extends Error {\n constructor(public readonly code: string, message: string) { super(message); this.name = 'CliProcessError' }\n}\nexport type CliOptions = {\n cwd: string; signal: AbortSignal; input?: string; timeoutMs?: number;\n maxOutputBytes?: number; maxLineBytes?: number;\n onLine?: (line: string) => void | Promise<void>;\n spawnChild?: typeof spawn;\n logger?: PluginLogger;\n}\n\n/** Own one process tree until it exits, including caller cancellation and protocol failure. */\nexport async function runCli(executable: CliExecutable, args: string[], options: CliOptions): Promise<CliOutput> {\n options.signal.throwIfAborted()\n const started = Date.now()\n const log = options.logger\n const timed = AbortSignal.timeout(options.timeoutMs ?? 900_000)\n const signal = AbortSignal.any([options.signal, timed])\n const maxBytes = options.maxOutputBytes ?? 16 * 1024 * 1024\n const maxLine = options.maxLineBytes ?? 8 * 1024 * 1024\n log?.debug('cli.started', 'CLI process starting.', {\n argCount: args.length,\n timeoutMs: options.timeoutMs ?? 900_000,\n })\n let child: ChildProcessWithoutNullStreams\n try {\n child = (options.spawnChild ?? spawn)(executable.command, [...(executable.args ?? []), ...args], {\n cwd: options.cwd, env: executable.env ?? cliEnvironment(), shell: false,\n windowsHide: true, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'],\n }) as ChildProcessWithoutNullStreams\n } catch (spawnErr) {\n log?.error('cli.unavailable', 'Unable to start CLI process.', spawnErr, { argCount: args.length })\n throw new CliProcessError('unavailable', 'Unable to start the official CLI.')\n }\n let failure: unknown, total = 0, stderr = '', stdout = '', pending = Buffer.alloc(0)\n const stdoutDecoder = new StringDecoder('utf8'), stderrDecoder = new StringDecoder('utf8')\n let exited = false, exitCode = -1, stopping: Promise<void> | undefined\n let lineWork = Promise.resolve()\n const closed = new Promise<void>(resolve => {\n child.once('close', code => { exited = true; exitCode = code ?? -1; resolve() })\n })\n const stop = () => stopping ??= (async () => {\n if (exited) return\n const pid = child.pid\n if (pid && process.platform === 'win32') {\n await new Promise<void>(resolve => {\n const killer = spawn('taskkill.exe', ['/PID', String(pid), '/T', '/F'], { env: cliEnvironment(), windowsHide: true, shell: false, stdio: 'ignore' })\n let timer: ReturnType<typeof setTimeout>\n const done = () => { clearTimeout(timer); resolve() }\n killer.once('error', () => { child.kill(); done() })\n killer.once('close', done)\n timer = setTimeout(() => { killer.kill(); child.kill(); resolve() }, 5_000)\n })\n } else if (pid) {\n try { process.kill(-pid, 'SIGTERM') } catch { child.kill() }\n } else child.kill()\n let timer: ReturnType<typeof setTimeout> | undefined\n await Promise.race([closed, new Promise<void>(resolve => { timer = setTimeout(resolve, 2_000) })])\n if (timer) clearTimeout(timer)\n if (!exited) {\n try { pid && process.platform !== 'win32' ? process.kill(-pid, 'SIGKILL') : child.kill('SIGKILL') } catch { /* already exited */ }\n }\n await closed\n })()\n const fail = (error: unknown) => { failure ??= error; void stop() }\n const abort = () => fail(new CliProcessError(timed.aborted && !options.signal.aborted ? 'timeout' : 'cancelled', timed.aborted && !options.signal.aborted ? 'The CLI request timed out.' : 'The CLI request was cancelled.'))\n signal.addEventListener('abort', abort, { once: true })\n child.on('error', () => fail(new CliProcessError('unavailable', 'Unable to start the official CLI.')))\n child.stdin.on('error', () => { if (!exited) fail(new CliProcessError('process-io', 'The CLI input stream closed unexpectedly.')) })\n child.stdout.on('error', () => fail(new CliProcessError('process-io', 'The CLI output stream failed.')))\n child.stderr.on('error', () => fail(new CliProcessError('process-io', 'The CLI error stream failed.')))\n const count = (chunk: Buffer) => {\n total += chunk.length\n if (total > maxBytes) { fail(new CliProcessError('output-limit', 'The CLI output exceeded its limit.')); return false }\n return true\n }\n const acceptLine = (line: string) => {\n if (!options.onLine || !line.trim()) return\n child.stdout.pause()\n lineWork = lineWork.then(async () => { if (!failure) await options.onLine!(line) }).catch(fail).finally(() => child.stdout.resume())\n }\n child.stdout.on('data', (chunk: Buffer) => {\n if (failure || !count(chunk)) return\n if (!options.onLine) { stdout += stdoutDecoder.write(chunk); return }\n pending = Buffer.concat([pending, chunk])\n for (;;) {\n const end = pending.indexOf(10)\n if (end < 0) break\n if (end > maxLine) { fail(new CliProcessError('output-limit', 'A CLI protocol frame exceeded its limit.')); return }\n acceptLine(pending.subarray(0, end).toString('utf8'))\n pending = pending.subarray(end + 1)\n }\n if (pending.length > maxLine) fail(new CliProcessError('output-limit', 'A CLI protocol frame exceeded its limit.'))\n })\n child.stderr.on('data', (chunk: Buffer) => { if (count(chunk)) stderr = (stderr + stderrDecoder.write(chunk)).slice(-16_384) })\n try {\n if (signal.aborted) abort()\n if (!failure) child.stdin.end(options.input ?? '')\n await closed\n stdout += stdoutDecoder.end(); stderr += stderrDecoder.end()\n if (pending.length && !failure) acceptLine(pending.toString('utf8'))\n await lineWork\n if (failure) {\n const durationMs = Date.now() - started\n if (failure instanceof CliProcessError && failure.code === 'cancelled') {\n log?.debug('cli.cancelled', 'CLI process cancelled.', { durationMs, outputBytes: total })\n } else if (failure instanceof CliProcessError && failure.code === 'timeout') {\n log?.warn('cli.timeout', 'CLI process timed out.', failure, { durationMs, outputBytes: total })\n } else {\n log?.error('cli.failed', 'CLI process failed.', failure, {\n code: failure instanceof CliProcessError ? failure.code : null,\n durationMs,\n outputBytes: total,\n })\n }\n throw failure\n }\n signal.throwIfAborted()\n const durationMs = Date.now() - started\n if (exitCode === 0) {\n log?.debug('cli.completed', 'CLI process exited cleanly.', { exitCode, durationMs, outputBytes: total })\n } else {\n log?.warn('cli.nonzero-exit', 'CLI process exited with a non-zero code.', undefined, { exitCode, durationMs, outputBytes: total })\n }\n return { stdout, stderr, exitCode }\n } finally {\n signal.removeEventListener('abort', abort)\n if (!exited) await stop()\n if (stopping) await stopping\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { C as truncateLogText, S as sanitizePluginLogText, _ as normalizePluginLogSource, a as PluginLogContext, b as sanitizePluginLogContext, c as PluginLogInput, d as PluginLogger, f as buildPluginLogEntry, g as normalizePluginLogEvent, h as mergePluginLogContext, i as PLUGIN_LOG_MAX_MESSAGE_CHARS, l as PluginLogLevel, m as isPluginLogLevel, n as PLUGIN_LOG_LEVELS, o as PluginLogEntry, p as createChildPluginLogger, r as PLUGIN_LOG_MAX_CONTEXT_CHARS, s as PluginLogError, t as DEFAULT_PLUGIN_LOG_LEVEL, u as PluginLogScalar, v as parsePluginLogLevel, x as sanitizePluginLogError, y as pluginLogLevelEnabled } from "./log-shared2.js";
|
|
2
|
+
//#region src/client-logger.d.ts
|
|
3
|
+
declare const CLIENT_PLUGIN_LOG_GLOBAL = "__KLARKXY_PLUGIN_LOGS__";
|
|
4
|
+
declare const CLIENT_PLUGIN_LOG_LIMIT = 500;
|
|
5
|
+
interface ClientPluginLoggerOptions {
|
|
6
|
+
minimumLevel?: PluginLogLevel;
|
|
7
|
+
now?: () => Date;
|
|
8
|
+
bufferLimit?: number;
|
|
9
|
+
forwardToHostLogger?: boolean;
|
|
10
|
+
loadedContext?: PluginLogContext;
|
|
11
|
+
}
|
|
12
|
+
declare function readClientPluginLogs(source?: string): PluginLogEntry[];
|
|
13
|
+
declare function clearClientPluginLogs(): void;
|
|
14
|
+
declare function createClientPluginLogger(source: string, ctx?: unknown, options?: ClientPluginLoggerOptions): PluginLogger;
|
|
15
|
+
declare function installClientPluginLogger(ctx: unknown, source: string, options?: ClientPluginLoggerOptions): PluginLogger;
|
|
16
|
+
declare function clientLogStep<T>(logger: PluginLogger, step: string, context: PluginLogContext | undefined, action: () => Promise<T>, completedLevel?: PluginLogLevel): Promise<T>;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { CLIENT_PLUGIN_LOG_GLOBAL, CLIENT_PLUGIN_LOG_LIMIT, ClientPluginLoggerOptions, DEFAULT_PLUGIN_LOG_LEVEL, PLUGIN_LOG_LEVELS, PLUGIN_LOG_MAX_CONTEXT_CHARS, PLUGIN_LOG_MAX_MESSAGE_CHARS, PluginLogContext, PluginLogEntry, PluginLogError, PluginLogInput, PluginLogLevel, PluginLogScalar, PluginLogger, buildPluginLogEntry, clearClientPluginLogs, clientLogStep, createChildPluginLogger, createClientPluginLogger, installClientPluginLogger, isPluginLogLevel, mergePluginLogContext, normalizePluginLogEvent, normalizePluginLogSource, parsePluginLogLevel, pluginLogLevelEnabled, readClientPluginLogs, sanitizePluginLogContext, sanitizePluginLogError, sanitizePluginLogText, truncateLogText };
|
|
19
|
+
//# sourceMappingURL=client-logger.d.ts.map
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { DEFAULT_PLUGIN_LOG_LEVEL, PLUGIN_LOG_LEVELS, PLUGIN_LOG_MAX_CONTEXT_CHARS, PLUGIN_LOG_MAX_MESSAGE_CHARS, buildPluginLogEntry, createChildPluginLogger, isPluginLogLevel, mergePluginLogContext, normalizePluginLogEvent, normalizePluginLogSource, parsePluginLogLevel, pluginLogLevelEnabled, sanitizePluginLogContext, sanitizePluginLogError, sanitizePluginLogText, truncateLogText } from "./log-shared.js";
|
|
2
|
+
//#region src/client-logger.ts
|
|
3
|
+
const CLIENT_PLUGIN_LOG_GLOBAL = "__KLARKXY_PLUGIN_LOGS__";
|
|
4
|
+
const CLIENT_PLUGIN_LOG_LIMIT = 500;
|
|
5
|
+
function readStorageFlag(key) {
|
|
6
|
+
try {
|
|
7
|
+
const value = (globalThis.window?.localStorage)?.getItem?.(key);
|
|
8
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
9
|
+
} catch {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function sharedClientBuffer(limit) {
|
|
14
|
+
const carrier = globalThis;
|
|
15
|
+
if (!Array.isArray(carrier["__KLARKXY_PLUGIN_LOGS__"])) try {
|
|
16
|
+
carrier[CLIENT_PLUGIN_LOG_GLOBAL] = [];
|
|
17
|
+
} catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const list = carrier[CLIENT_PLUGIN_LOG_GLOBAL];
|
|
21
|
+
const overflow = list.length - limit;
|
|
22
|
+
if (overflow > 0) list.splice(0, overflow);
|
|
23
|
+
return list;
|
|
24
|
+
}
|
|
25
|
+
function readClientPluginLogs(source) {
|
|
26
|
+
const carrier = globalThis;
|
|
27
|
+
const list = Array.isArray(carrier["__KLARKXY_PLUGIN_LOGS__"]) ? carrier[CLIENT_PLUGIN_LOG_GLOBAL] : [];
|
|
28
|
+
if (!source) return list.slice();
|
|
29
|
+
const normalized = normalizePluginLogSource(source);
|
|
30
|
+
return list.filter((entry) => entry.source === normalized);
|
|
31
|
+
}
|
|
32
|
+
function clearClientPluginLogs() {
|
|
33
|
+
const carrier = globalThis;
|
|
34
|
+
if (Array.isArray(carrier["__KLARKXY_PLUGIN_LOGS__"])) carrier[CLIENT_PLUGIN_LOG_GLOBAL].length = 0;
|
|
35
|
+
}
|
|
36
|
+
function createClientPluginLogger(source, ctx, options = {}) {
|
|
37
|
+
const normalized = normalizePluginLogSource(source);
|
|
38
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
39
|
+
const limit = Math.max(10, options.bufferLimit ?? 500);
|
|
40
|
+
const browserCtx = ctx && typeof ctx === "object" ? ctx : void 0;
|
|
41
|
+
const resolveMinimum = () => {
|
|
42
|
+
if (options.minimumLevel) return options.minimumLevel;
|
|
43
|
+
return parsePluginLogLevel(readStorageFlag("dsh-plugin-log-level"), normalized, DEFAULT_PLUGIN_LOG_LEVEL);
|
|
44
|
+
};
|
|
45
|
+
const logger = {
|
|
46
|
+
source: normalized,
|
|
47
|
+
file: null,
|
|
48
|
+
lastWriteError: null,
|
|
49
|
+
debug: (event, message, context) => logger.write({
|
|
50
|
+
level: "debug",
|
|
51
|
+
event,
|
|
52
|
+
message,
|
|
53
|
+
...context ? { context } : {}
|
|
54
|
+
}),
|
|
55
|
+
info: (event, message, context) => logger.write({
|
|
56
|
+
level: "info",
|
|
57
|
+
event,
|
|
58
|
+
message,
|
|
59
|
+
...context ? { context } : {}
|
|
60
|
+
}),
|
|
61
|
+
warn: (event, message, error, context) => logger.write({
|
|
62
|
+
level: "warn",
|
|
63
|
+
event,
|
|
64
|
+
message,
|
|
65
|
+
...error !== void 0 ? { error } : {},
|
|
66
|
+
...context ? { context } : {}
|
|
67
|
+
}),
|
|
68
|
+
error: (event, message, error, context) => logger.write({
|
|
69
|
+
level: "error",
|
|
70
|
+
event,
|
|
71
|
+
message,
|
|
72
|
+
...error !== void 0 ? { error } : {},
|
|
73
|
+
...context ? { context } : {}
|
|
74
|
+
}),
|
|
75
|
+
fault: (event, message, error, context) => logger.write({
|
|
76
|
+
level: "fault",
|
|
77
|
+
event,
|
|
78
|
+
message,
|
|
79
|
+
...error !== void 0 ? { error } : {},
|
|
80
|
+
...context ? { context } : {}
|
|
81
|
+
}),
|
|
82
|
+
child: (context) => createChildPluginLogger(logger, context),
|
|
83
|
+
write(input) {
|
|
84
|
+
try {
|
|
85
|
+
if (!pluginLogLevelEnabled(input.level, resolveMinimum())) return false;
|
|
86
|
+
const entry = buildPluginLogEntry(input, {
|
|
87
|
+
at: now(),
|
|
88
|
+
source: normalized,
|
|
89
|
+
surface: "client"
|
|
90
|
+
});
|
|
91
|
+
const buffer = sharedClientBuffer(limit);
|
|
92
|
+
buffer.push(entry);
|
|
93
|
+
const overflow = buffer.length - limit;
|
|
94
|
+
if (overflow > 0) buffer.splice(0, overflow);
|
|
95
|
+
if (options.forwardToHostLogger !== false && browserCtx?.logger) {
|
|
96
|
+
const summary = sanitizePluginLogText(`${normalized} [${entry.event}]: ${entry.message}`, 500);
|
|
97
|
+
if (entry.level === "warn" && typeof browserCtx.logger.warn === "function") browserCtx.logger.warn(summary);
|
|
98
|
+
else if ((entry.level === "error" || entry.level === "fault") && typeof browserCtx.logger.error === "function") browserCtx.logger.error(summary);
|
|
99
|
+
}
|
|
100
|
+
if (readStorageFlag("dsh-plugin-log-console") === "1") {
|
|
101
|
+
const con = globalThis.console;
|
|
102
|
+
(entry.level === "debug" ? con?.debug : entry.level === "info" ? con?.info : entry.level === "warn" ? con?.warn : con?.error)?.call(con, `[${normalized}] ${entry.event}: ${entry.message}`, entry);
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
return logger;
|
|
111
|
+
}
|
|
112
|
+
function installClientPluginLogger(ctx, source, options = {}) {
|
|
113
|
+
const logger = createClientPluginLogger(source, ctx, options);
|
|
114
|
+
logger.info("client.loaded", "Client plugin initialized.", options.loadedContext);
|
|
115
|
+
const browserCtx = ctx && typeof ctx === "object" ? ctx : void 0;
|
|
116
|
+
if (typeof browserCtx?.effect === "function") try {
|
|
117
|
+
browserCtx.effect(() => () => {
|
|
118
|
+
logger.debug("client.disposed", "Client plugin disposed.");
|
|
119
|
+
}, `${logger.source}.client-logger`);
|
|
120
|
+
} catch {}
|
|
121
|
+
return logger;
|
|
122
|
+
}
|
|
123
|
+
async function clientLogStep(logger, step, context, action, completedLevel = "debug") {
|
|
124
|
+
const started = Date.now();
|
|
125
|
+
const base = {
|
|
126
|
+
...context,
|
|
127
|
+
step
|
|
128
|
+
};
|
|
129
|
+
logger.debug("step.started", "Client step started.", base);
|
|
130
|
+
try {
|
|
131
|
+
const result = await action();
|
|
132
|
+
logger.write({
|
|
133
|
+
level: completedLevel,
|
|
134
|
+
event: "step.completed",
|
|
135
|
+
message: "Client step completed.",
|
|
136
|
+
context: {
|
|
137
|
+
...base,
|
|
138
|
+
durationMs: Date.now() - started
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return result;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
logger.error("step.failed", "Client step failed.", error, {
|
|
144
|
+
...base,
|
|
145
|
+
durationMs: Date.now() - started
|
|
146
|
+
});
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
export { CLIENT_PLUGIN_LOG_GLOBAL, CLIENT_PLUGIN_LOG_LIMIT, DEFAULT_PLUGIN_LOG_LEVEL, PLUGIN_LOG_LEVELS, PLUGIN_LOG_MAX_CONTEXT_CHARS, PLUGIN_LOG_MAX_MESSAGE_CHARS, buildPluginLogEntry, clearClientPluginLogs, clientLogStep, createChildPluginLogger, createClientPluginLogger, installClientPluginLogger, isPluginLogLevel, mergePluginLogContext, normalizePluginLogEvent, normalizePluginLogSource, parsePluginLogLevel, pluginLogLevelEnabled, readClientPluginLogs, sanitizePluginLogContext, sanitizePluginLogError, sanitizePluginLogText, truncateLogText };
|
|
152
|
+
|
|
153
|
+
//# sourceMappingURL=client-logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-logger.js","names":[],"sources":["../src/client-logger.ts"],"sourcesContent":["import {\n DEFAULT_PLUGIN_LOG_LEVEL, buildPluginLogEntry, createChildPluginLogger,\n normalizePluginLogSource, parsePluginLogLevel, pluginLogLevelEnabled,\n sanitizePluginLogText,\n type PluginLogContext, type PluginLogEntry, type PluginLogInput,\n type PluginLogLevel, type PluginLogger,\n} from './log-shared.ts'\n\nexport * from './log-shared.ts'\n\nexport const CLIENT_PLUGIN_LOG_GLOBAL = '__KLARKXY_PLUGIN_LOGS__'\nexport const CLIENT_PLUGIN_LOG_LIMIT = 500\n\nexport interface ClientPluginLoggerOptions {\n minimumLevel?: PluginLogLevel\n now?: () => Date\n bufferLimit?: number\n forwardToHostLogger?: boolean\n loadedContext?: PluginLogContext\n}\n\ntype BrowserContextLike = {\n effect?: (callback: () => () => void, label?: string) => unknown\n logger?: {\n warn?: (...args: unknown[]) => void\n error?: (...args: unknown[]) => void\n }\n}\n\ntype GlobalLogCarrier = {\n [CLIENT_PLUGIN_LOG_GLOBAL]?: PluginLogEntry[]\n window?: { localStorage?: { getItem?: (key: string) => string | null } }\n console?: {\n debug?: (...args: unknown[]) => void\n info?: (...args: unknown[]) => void\n warn?: (...args: unknown[]) => void\n error?: (...args: unknown[]) => void\n }\n}\n\nfunction readStorageFlag(key: string): string | undefined {\n try {\n const win = (globalThis as unknown as GlobalLogCarrier).window\n const storage = win?.localStorage\n const value = storage?.getItem?.(key)\n return typeof value === 'string' && value.trim() ? value.trim() : undefined\n } catch {\n return undefined\n }\n}\n\nfunction sharedClientBuffer(limit: number): PluginLogEntry[] {\n const carrier = globalThis as unknown as GlobalLogCarrier\n if (!Array.isArray(carrier[CLIENT_PLUGIN_LOG_GLOBAL])) {\n try {\n carrier[CLIENT_PLUGIN_LOG_GLOBAL] = []\n } catch {\n return []\n }\n }\n const list = carrier[CLIENT_PLUGIN_LOG_GLOBAL]!\n const overflow = list.length - limit\n if (overflow > 0) list.splice(0, overflow)\n return list\n}\n\nexport function readClientPluginLogs(source?: string): PluginLogEntry[] {\n const carrier = globalThis as unknown as GlobalLogCarrier\n const list = Array.isArray(carrier[CLIENT_PLUGIN_LOG_GLOBAL]) ? carrier[CLIENT_PLUGIN_LOG_GLOBAL]! : []\n if (!source) return list.slice()\n const normalized = normalizePluginLogSource(source)\n return list.filter(entry => entry.source === normalized)\n}\n\nexport function clearClientPluginLogs(): void {\n const carrier = globalThis as unknown as GlobalLogCarrier\n if (Array.isArray(carrier[CLIENT_PLUGIN_LOG_GLOBAL])) {\n carrier[CLIENT_PLUGIN_LOG_GLOBAL]!.length = 0\n }\n}\n\nexport function createClientPluginLogger(\n source: string,\n ctx?: unknown,\n options: ClientPluginLoggerOptions = {},\n): PluginLogger {\n const normalized = normalizePluginLogSource(source)\n const now = options.now ?? (() => new Date())\n const limit = Math.max(10, options.bufferLimit ?? CLIENT_PLUGIN_LOG_LIMIT)\n const browserCtx = ctx && typeof ctx === 'object' ? ctx as BrowserContextLike : undefined\n\n const resolveMinimum = (): PluginLogLevel => {\n if (options.minimumLevel) return options.minimumLevel\n return parsePluginLogLevel(readStorageFlag('dsh-plugin-log-level'), normalized, DEFAULT_PLUGIN_LOG_LEVEL)\n }\n\n const logger: PluginLogger = {\n source: normalized,\n file: null,\n lastWriteError: null,\n debug: (event, message, context) => logger.write({ level: 'debug', event, message, ...(context ? { context } : {}) }),\n info: (event, message, context) => logger.write({ level: 'info', event, message, ...(context ? { context } : {}) }),\n warn: (event, message, error, context) => logger.write({\n level: 'warn', event, message,\n ...(error !== undefined ? { error } : {}),\n ...(context ? { context } : {}),\n }),\n error: (event, message, error, context) => logger.write({\n level: 'error', event, message,\n ...(error !== undefined ? { error } : {}),\n ...(context ? { context } : {}),\n }),\n fault: (event, message, error, context) => logger.write({\n level: 'fault', event, message,\n ...(error !== undefined ? { error } : {}),\n ...(context ? { context } : {}),\n }),\n child: context => createChildPluginLogger(logger, context),\n write(input: PluginLogInput): boolean {\n try {\n if (!pluginLogLevelEnabled(input.level, resolveMinimum())) return false\n const entry = buildPluginLogEntry(input, {\n at: now(),\n source: normalized,\n surface: 'client',\n })\n const buffer = sharedClientBuffer(limit)\n buffer.push(entry)\n const overflow = buffer.length - limit\n if (overflow > 0) buffer.splice(0, overflow)\n\n if (options.forwardToHostLogger !== false && browserCtx?.logger) {\n const summary = sanitizePluginLogText(`${normalized} [${entry.event}]: ${entry.message}`, 500)\n if (entry.level === 'warn' && typeof browserCtx.logger.warn === 'function') {\n browserCtx.logger.warn(summary)\n } else if ((entry.level === 'error' || entry.level === 'fault') && typeof browserCtx.logger.error === 'function') {\n browserCtx.logger.error(summary)\n }\n }\n if (readStorageFlag('dsh-plugin-log-console') === '1') {\n const con = (globalThis as unknown as GlobalLogCarrier).console\n const fn = entry.level === 'debug' ? con?.debug\n : entry.level === 'info' ? con?.info\n : entry.level === 'warn' ? con?.warn\n : con?.error\n fn?.call(con, `[${normalized}] ${entry.event}: ${entry.message}`, entry)\n }\n return true\n } catch {\n return false\n }\n },\n }\n return logger\n}\n\nexport function installClientPluginLogger(\n ctx: unknown,\n source: string,\n options: ClientPluginLoggerOptions = {},\n): PluginLogger {\n const logger = createClientPluginLogger(source, ctx, options)\n logger.info('client.loaded', 'Client plugin initialized.', options.loadedContext)\n const browserCtx = ctx && typeof ctx === 'object' ? ctx as BrowserContextLike : undefined\n if (typeof browserCtx?.effect === 'function') {\n try {\n browserCtx.effect(() => () => {\n logger.debug('client.disposed', 'Client plugin disposed.')\n }, `${logger.source}.client-logger`)\n } catch {\n // Non-Cordis mock context.\n }\n }\n return logger\n}\n\nexport async function clientLogStep<T>(\n logger: PluginLogger,\n step: string,\n context: PluginLogContext | undefined,\n action: () => Promise<T>,\n completedLevel: PluginLogLevel = 'debug',\n): Promise<T> {\n const started = Date.now()\n const base = { ...context, step }\n logger.debug('step.started', 'Client step started.', base)\n try {\n const result = await action()\n logger.write({\n level: completedLevel,\n event: 'step.completed',\n message: 'Client step completed.',\n context: { ...base, durationMs: Date.now() - started },\n })\n return result\n } catch (error) {\n logger.error('step.failed', 'Client step failed.', error, {\n ...base,\n durationMs: Date.now() - started,\n })\n throw error\n }\n}\n"],"mappings":"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"}
|
package/lib/client-utils.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { d as PluginLogger } from "./log-shared2.js";
|
|
1
2
|
import { ReactNode } from "react";
|
|
2
3
|
import { Context } from "@deepseek-ai/cordis";
|
|
3
4
|
//#region src/client-utils.d.ts
|
|
4
5
|
/** Fixed pause for failing background reads; manual work is never replayed. */
|
|
5
|
-
declare function createPluginReadGate(now?: () => number): {
|
|
6
|
+
declare function createPluginReadGate(now?: () => number, logger?: PluginLogger): {
|
|
6
7
|
canRead: () => boolean;
|
|
7
8
|
reset: () => void;
|
|
8
9
|
run<T>(read: () => Promise<T>): Promise<T>;
|
package/lib/client-utils.js
CHANGED
|
@@ -2,7 +2,7 @@ import { SESSION_SETTINGS_ITEM_SLOT, SESSION_SETTINGS_SECTION_ID, SETTINGS_SECTI
|
|
|
2
2
|
import { createElement, useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
3
3
|
//#region src/client-utils.ts
|
|
4
4
|
/** Fixed pause for failing background reads; manual work is never replayed. */
|
|
5
|
-
function createPluginReadGate(now = Date.now) {
|
|
5
|
+
function createPluginReadGate(now = Date.now, logger) {
|
|
6
6
|
let pausedUntil = 0;
|
|
7
7
|
let epoch = 0;
|
|
8
8
|
return {
|
|
@@ -15,11 +15,17 @@ function createPluginReadGate(now = Date.now) {
|
|
|
15
15
|
const current = epoch;
|
|
16
16
|
try {
|
|
17
17
|
const value = await read();
|
|
18
|
-
if (current === epoch)
|
|
18
|
+
if (current === epoch) {
|
|
19
|
+
if (pausedUntil > 0) logger?.debug("read-gate.resumed", "Background read gate recovered.");
|
|
20
|
+
pausedUntil = 0;
|
|
21
|
+
}
|
|
19
22
|
return value;
|
|
20
23
|
} catch (error) {
|
|
21
24
|
const message = error instanceof Error ? error.message : "";
|
|
22
|
-
if (current === epoch && (/(?:^|\s)transport failure for .+: HTTP (?:404|405|503)\b/.test(message) || error instanceof TypeError && /^(?:Failed to fetch|fetch failed|NetworkError when attempting to fetch resource\.?)$/i.test(message)))
|
|
25
|
+
if (current === epoch && (/(?:^|\s)transport failure for .+: HTTP (?:404|405|503)\b/.test(message) || error instanceof TypeError && /^(?:Failed to fetch|fetch failed|NetworkError when attempting to fetch resource\.?)$/i.test(message))) {
|
|
26
|
+
pausedUntil = now() + 3e4;
|
|
27
|
+
logger?.warn("read-gate.paused", "Background reads paused for 30s after transport failure.", error);
|
|
28
|
+
}
|
|
23
29
|
throw error;
|
|
24
30
|
}
|
|
25
31
|
}
|
package/lib/client-utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-utils.js","names":[],"sources":["../src/client-utils.ts"],"sourcesContent":["import type { Context } from '@deepseek-ai/cordis'\nimport { createElement, useCallback, useEffect, useRef, useSyncExternalStore, type ReactNode } from 'react'\nimport { SESSION_SETTINGS_ITEM_SLOT, SESSION_SETTINGS_SECTION_ID, SETTINGS_SECTION_ICON_SLOT } from './contracts.ts'\n\n/** Fixed pause for failing background reads; manual work is never replayed. */\nexport function createPluginReadGate(now: () => number = Date.now) {\n let pausedUntil = 0\n let epoch = 0\n return {\n canRead: () => now() >= pausedUntil,\n reset: () => { epoch++; pausedUntil = 0 },\n async run<T>(read: () => Promise<T>): Promise<T> {\n const current = epoch\n try {\n const value = await read()\n if (current === epoch) pausedUntil = 0\n return value\n } catch (error) {\n // The native Connection error is an Error with this exact HTTP text.\n // Authentication, application failures, and cancellation stay distinct.\n const message = error instanceof Error ? error.message : ''\n if (current === epoch && (/(?:^|\\s)transport failure for .+: HTTP (?:404|405|503)\\b/.test(message)\n || (error instanceof TypeError && /^(?:Failed to fetch|fetch failed|NetworkError when attempting to fetch resource\\.?)$/i.test(message)))) {\n pausedUntil = now() + 30_000\n }\n throw error\n }\n },\n }\n}\n\nexport interface NativeSurfaceClient {\n sessions: {\n binding?(sessionId: string): { eventSource: { subscribe(listener: () => void): () => void } } | undefined\n }\n uiWorkspace: { current?: { getSnapshot(): { sessionId: string } | undefined; subscribe(listener: () => void): () => void } }\n uiSession: { adapter: { current: { getSnapshot(): { key?: string }; subscribe(listener: () => void): () => void } } }\n locale: { getSnapshot(): { active: string }; subscribe(listener: () => void): () => void }\n connection?: { generation?: { subscribe(listener: () => void): () => void } }\n}\n\n/** The Editor retains its manuscript session separately; native DSH exposes the main-view binding. */\nexport function selectedSessionId(client: Pick<NativeSurfaceClient, 'uiWorkspace' | 'uiSession'>): string {\n const workspace = client.uiWorkspace?.current\n return workspace\n ? workspace.getSnapshot()?.sessionId ?? ''\n : client.uiSession.adapter.current.getSnapshot().key ?? ''\n}\n\n/**\n * Owner props are data the host composed, never a Cordis context: reading an\n * undeclared property off one throws instead of returning undefined, which\n * would crash the slot entry and abdicate its cell for good. Tolerate a\n * throwing read so a mistaken prop degrades to \"not supplied\".\n */\nfunction read(source: unknown, key: string): unknown {\n if (source === null || typeof source !== 'object') return undefined\n try {\n return (source as Record<string, unknown>)[key]\n } catch {\n return undefined\n }\n}\n\nfunction readString(source: unknown, key: string): string | undefined {\n const value = read(source, key)\n return typeof value === 'string' ? value : undefined\n}\n\n/** Owner props win; native settings only supplies close, so resolve its selected session. */\nexport function useNativeSeat(client: NativeSurfaceClient, props?: unknown) {\n const current = useSyncExternalStore(\n useCallback((fn: () => void) => (client.uiWorkspace?.current ?? client.uiSession.adapter.current).subscribe(fn), [client]),\n useCallback(() => selectedSessionId(client), [client]),\n () => '',\n )\n const language = useSyncExternalStore(\n useCallback((fn: () => void) => client.locale.subscribe(fn), [client]),\n useCallback(() => client.locale.getSnapshot().active, [client]),\n () => 'en',\n )\n const sessionId = readString(props, 'sessionId') ?? readString(read(props, 'owner'), 'sessionId') ?? current\n const requestedLocale = read(props, 'locale') ?? read(read(props, 'owner'), 'locale') ?? language\n return { sessionId, locale: (String(requestedLocale).startsWith('zh') ? 'zh' : 'en') as 'zh' | 'en',\n hidden: read(props, 'hidden') === true || read(read(props, 'owner'), 'hidden') === true }\n}\n\n/** Refresh on native log boundaries/reconnect/focus. Read-only settling is bounded to an observed job. */\nexport function useFeatureRefresh(\n client: NativeSurfaceClient, sessionId: string, refresh: () => void,\n working = false, enabled = true,\n): void {\n const refreshRef = useRef(refresh)\n refreshRef.current = refresh\n useEffect(() => {\n if (!enabled) return\n let timer: ReturnType<typeof setTimeout> | undefined\n const queue = () => {\n if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return\n clearTimeout(timer)\n timer = setTimeout(() => refreshRef.current(), 150)\n }\n const source = sessionId ? client.sessions.binding?.(sessionId)?.eventSource : undefined\n const stopEvents = source?.subscribe(queue)\n const stopConnection = client.connection?.generation?.subscribe(queue)\n const visibility = () => { if (document.visibilityState === 'visible') queue() }\n window.addEventListener('focus', queue)\n document.addEventListener('visibilitychange', visibility)\n // Details are collapsed by default; opening an existing panel must get current state.\n document.addEventListener('toggle', queue, true)\n return () => {\n clearTimeout(timer)\n stopEvents?.(); stopConnection?.()\n window.removeEventListener('focus', queue)\n document.removeEventListener('visibilitychange', visibility)\n document.removeEventListener('toggle', queue, true)\n }\n }, [client, sessionId, enabled])\n useEffect(() => {\n if (!enabled || !working) return\n let delay = 250\n const until = Date.now() + 300_000\n let timer: ReturnType<typeof setTimeout>\n const tick = () => {\n if (Date.now() >= until) return\n if (document.visibilityState !== 'hidden') refreshRef.current()\n delay = Math.min(delay * 2, 2000)\n timer = setTimeout(tick, delay)\n }\n timer = setTimeout(tick, delay)\n return () => clearTimeout(timer)\n }, [client, sessionId, working, enabled])\n}\n\n/* Shared \"Session Management\" settings page. The page is root-owned shared\n * infrastructure, not a business plugin: any participant alone yields the same\n * section, and participants never import each other. Synchronization crosses\n * separately bundled copies of this helper through the Cordis root context —\n * `root.get`/`root.provide` is the only rendezvous, so no window global or\n * module singleton is needed and no participant depends on another being enabled.\n */\n\n/** A row contributed into the shared Session Management page. */\nexport interface SessionSettingsItemSpec {\n readonly id: string\n readonly order: number\n}\n\n/* Original line icons for settings-section navigation seats. The official\n * `dsh-client-ui-primitives` icon set covers generic glyphs only; these keep\n * the same conventions (16×16 viewBox, 1px stroke, round caps, currentColor)\n * so the plugin set stays visually consistent with the native nav. Colors\n * never appear here: `currentColor` lets the host style the active state, and\n * `aria-hidden` keeps the section label the single accessible name. */\n\n/** Owner props the host's `settings.section.icon` seat hands to its cell. */\nexport interface SettingsSectionIconProps {\n readonly size?: number\n /** Selection state; the nav styles active icons through color, not geometry. */\n readonly active?: boolean\n}\n\ntype IconChild = readonly [tag: string, attrs: Record<string, string | number>]\n\n/** One `<svg>` with the shared icon chrome; children are plain elements. */\nfunction lineIcon(children: IconChild[]) {\n return function SettingsSectionIcon(props: SettingsSectionIconProps): ReactNode {\n return createElement('svg', {\n 'aria-hidden': 'true',\n width: props.size ?? 16,\n height: props.size ?? 16,\n viewBox: '0 0 16 16',\n fill: 'none',\n stroke: 'currentColor',\n strokeWidth: 1,\n strokeLinecap: 'round',\n strokeLinejoin: 'round',\n }, ...children.map(([tag, attrs], index) => createElement(tag, { key: index, ...attrs })))\n }\n}\n\n/** Chat bubble for the shared Session Management section. */\nexport const IconSessionSettings = lineIcon([\n ['path', { d: 'M8.5 13a5.5 5.5 0 0 0 5.5-5.5A5.5 5.5 0 0 0 8.5 2 5.5 5.5 0 0 0 3 7.5a5.48 5.48 0 0 0 .9 3L2.8 12.6a.5.5 0 0 0 .62.62l2.9-.98a5.48 5.48 0 0 0 2.18.76Z' }],\n])\n\n/** Serif-flavoured letter A for the Font section. */\nexport const IconFontSettings = lineIcon([\n ['path', { d: 'M3.3 12.6 7.9 3.1a.5.5 0 0 1 .9 0l4.6 9.5' }],\n ['path', { d: 'M5.2 9.4h6.3' }],\n ['path', { d: 'M2.4 12.6h2.4' }],\n ['path', { d: 'M11.9 12.6h2.4' }],\n])\n\n/** Keyboard for the Shortcuts section. */\nexport const IconKeyboardSettings = lineIcon([\n ['rect', { x: 1.75, y: 4.75, width: 12.5, height: 7.5, rx: 1.2 }],\n ['path', { d: 'M4.5 7.25h0' }],\n ['path', { d: 'M8 7.25h0' }],\n ['path', { d: 'M11.5 7.25h0' }],\n ['path', { d: 'M5.25 9.75h5.5' }],\n])\n\n/** Linked model nodes for the Model Management section. */\nexport const IconModelNetworkSettings = lineIcon([\n ['line', { x1: 10.6, y1: 5.3, x2: 8.8, y2: 7.1 }],\n ['line', { x1: 7.2, y1: 8.9, x2: 5.4, y2: 10.7 }],\n ['circle', { cx: 12, cy: 4, r: 1.9 }],\n ['circle', { cx: 4, cy: 12, r: 1.9 }],\n ['circle', { cx: 8, cy: 8, r: 1.5 }],\n])\n\n/**\n * Contribute the nav icon of a `settings.section` entry. The `settings\n * .section.icon` seat is declared by the host independently of the section\n * list, so the wait runs in parallel: the icon appears as soon as both seats\n * exist and unloading removes it again, restoring the host's default glyph.\n * The `id` must equal the section id; `order`/`priority` mirror `order` so the\n * icon cell sorts like its section and, for shared sections, several\n * contributors can register the same icon id under different priorities\n * without duplicating nav entries.\n */\nexport function registerSettingsSectionIcon(\n ctx: Context,\n sectionId: string,\n order: number,\n component: (props: SettingsSectionIconProps) => ReactNode,\n): () => void {\n const slots = (ctx as unknown as { slots: SessionSettingsSlots }).slots\n return slots.inject(SETTINGS_SECTION_ICON_SLOT, () =>\n slots.register({ name: SETTINGS_SECTION_ICON_SLOT, id: sectionId, order, priority: order }, component))\n}\n\ninterface SessionSettingsSlots {\n inject(key: string, callback: () => Iterable<(() => void) | void> | (() => void) | void): () => void\n register(spec: Record<string, unknown>, render: unknown): () => void\n}\n\ninterface SessionSettingsRoot {\n get(name: string, strict?: boolean): unknown\n provide(name: string, value: unknown): () => void\n readonly slots?: SessionSettingsSlots\n}\n\n/** Private Cordis rendezvous; every bundled copy of this helper resolves the same root service. */\nconst SESSION_SETTINGS_PAGE_SERVICE = 'dshSessionSettingsPage'\n\ninterface SessionSettingsPage {\n retain(): void\n release(): void\n}\n\n/** The page component renders only its declared child list: items paint their own titles and styles. */\nfunction SessionSettingsPageView(props: { renderSlot: (key: string, owner: Record<string, never>) => ReactNode }): ReactNode {\n return props.renderSlot(SESSION_SETTINGS_ITEM_SLOT, {})\n}\n\n/**\n * Acquire the root-owned page service, creating and providing it on first use.\n * One counter covers participants, the section wait and the provider: the last\n * release removes the wait and then the provider synchronously (Cordis deletes\n * the impl before its async disposer awaits), so a later acquire sees a clean\n * get() miss and can re-establish the page.\n */\nfunction acquireSessionSettingsPage(root: SessionSettingsRoot): SessionSettingsPage {\n const existing = root.get(SESSION_SETTINGS_PAGE_SERVICE, false) as SessionSettingsPage | undefined\n if (existing) return existing\n const slots = root.slots as SessionSettingsSlots\n let users = 0\n let stopPage: (() => void) | undefined\n let unprovide: (() => void) | undefined\n const page: SessionSettingsPage = {\n retain() {\n // The reservation is counted before the throwing install, so a failure\n // lets the caller's release() undo it fully instead of leaking the service.\n users += 1\n if (!stopPage) {\n stopPage = slots.inject('settings.section', () => slots.register(\n {\n name: 'settings.section',\n id: SESSION_SETTINGS_SECTION_ID,\n order: 30,\n label: () => {\n const locale = root.get('locale', false) as { getSnapshot?(): { active?: unknown } } | undefined\n return String(locale?.getSnapshot?.().active ?? '').startsWith('zh') ? '会话管理' : 'Session Management'\n },\n children: { [SESSION_SETTINGS_ITEM_SLOT]: { kind: 'list', scope: 'root' } },\n },\n SessionSettingsPageView,\n ))\n }\n },\n release() {\n if (users <= 0) return\n users -= 1\n if (users > 0) return\n // The last contributor tears the section down; collapsing its children\n // declaration clears the item slot for every other bundle as well.\n stopPage?.()\n stopPage = undefined\n unprovide?.()\n },\n }\n unprovide = root.provide(SESSION_SETTINGS_PAGE_SERVICE, page)\n return page\n}\n\n/**\n * Contribute one row to the shared Session Management settings page.\n *\n * `ctx` is the plugin's own context: the row's slot wait unloads with the\n * plugin, while the page itself is retained on the shared root context for as\n * long as any contributor lives. There is no fallback seat — when every\n * contributor is gone the section disappears with the page. Returns a disposer.\n * Shared root-owned infrastructure (the auxiliary-model card) calls this on\n * `ctx.root` so its row, icon and page share are owned by the root lifetime\n * and released by the returned disposer, not by any plugin fiber.\n */\nexport function registerSessionSettingsItem(\n ctx: Context,\n item: SessionSettingsItemSpec,\n component: (props: never) => ReactNode,\n): () => void {\n const slots = (ctx as unknown as { slots: SessionSettingsSlots }).slots\n const root = ctx.root as unknown as SessionSettingsRoot\n const page = acquireSessionSettingsPage(root)\n let stopItem: (() => void) | undefined\n let stopIcon: (() => void) | undefined\n try {\n page.retain()\n stopItem = slots.inject(SESSION_SETTINGS_ITEM_SLOT, () => slots.register({ name: SESSION_SETTINGS_ITEM_SLOT, ...item }, component))\n // Every contributor also waits on the nav-icon seat, registering the same\n // section icon id under its own priority (its row order). Same-id entries\n // coexist on the ledger while the host renders one icon, so no contributor\n // owns the icon and none depends on another for it; the wait lives with\n // this ctx, so it fires now on fresh loads and retro-fills on HMR.\n stopIcon = slots.inject(SETTINGS_SECTION_ICON_SLOT, () => slots.register(\n { name: SETTINGS_SECTION_ICON_SLOT, id: SESSION_SETTINGS_SECTION_ID, order: item.order, priority: item.order },\n IconSessionSettings,\n ))\n } catch (error) {\n // A synchronous failure (for example a duplicate row id or a failing page\n // install) must not pin the shared page for a contributor that never joined.\n stopIcon?.()\n stopItem?.()\n page.release()\n throw error\n }\n // The disposer may be invoked by both ctx.effect and an explicit caller;\n // releasing twice would unbalance the shared refcount.\n let disposed = false\n return () => {\n if (disposed) return\n disposed = true\n stopIcon?.()\n stopItem?.()\n page.release()\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client-utils.js","names":[],"sources":["../src/client-utils.ts"],"sourcesContent":["import type { Context } from '@deepseek-ai/cordis'\nimport { createElement, useCallback, useEffect, useRef, useSyncExternalStore, type ReactNode } from 'react'\nimport { SESSION_SETTINGS_ITEM_SLOT, SESSION_SETTINGS_SECTION_ID, SETTINGS_SECTION_ICON_SLOT } from './contracts.ts'\nimport type { PluginLogger } from './log-shared.ts'\n\n/** Fixed pause for failing background reads; manual work is never replayed. */\nexport function createPluginReadGate(now: () => number = Date.now, logger?: PluginLogger) {\n let pausedUntil = 0\n let epoch = 0\n return {\n canRead: () => now() >= pausedUntil,\n reset: () => { epoch++; pausedUntil = 0 },\n async run<T>(read: () => Promise<T>): Promise<T> {\n const current = epoch\n try {\n const value = await read()\n if (current === epoch) {\n if (pausedUntil > 0) logger?.debug('read-gate.resumed', 'Background read gate recovered.')\n pausedUntil = 0\n }\n return value\n } catch (error) {\n // The native Connection error is an Error with this exact HTTP text.\n // Authentication, application failures, and cancellation stay distinct.\n const message = error instanceof Error ? error.message : ''\n if (current === epoch && (/(?:^|\\s)transport failure for .+: HTTP (?:404|405|503)\\b/.test(message)\n || (error instanceof TypeError && /^(?:Failed to fetch|fetch failed|NetworkError when attempting to fetch resource\\.?)$/i.test(message)))) {\n pausedUntil = now() + 30_000\n logger?.warn('read-gate.paused', 'Background reads paused for 30s after transport failure.', error)\n }\n throw error\n }\n },\n }\n}\n\nexport interface NativeSurfaceClient {\n sessions: {\n binding?(sessionId: string): { eventSource: { subscribe(listener: () => void): () => void } } | undefined\n }\n uiWorkspace: { current?: { getSnapshot(): { sessionId: string } | undefined; subscribe(listener: () => void): () => void } }\n uiSession: { adapter: { current: { getSnapshot(): { key?: string }; subscribe(listener: () => void): () => void } } }\n locale: { getSnapshot(): { active: string }; subscribe(listener: () => void): () => void }\n connection?: { generation?: { subscribe(listener: () => void): () => void } }\n}\n\n/** The Editor retains its manuscript session separately; native DSH exposes the main-view binding. */\nexport function selectedSessionId(client: Pick<NativeSurfaceClient, 'uiWorkspace' | 'uiSession'>): string {\n const workspace = client.uiWorkspace?.current\n return workspace\n ? workspace.getSnapshot()?.sessionId ?? ''\n : client.uiSession.adapter.current.getSnapshot().key ?? ''\n}\n\n/**\n * Owner props are data the host composed, never a Cordis context: reading an\n * undeclared property off one throws instead of returning undefined, which\n * would crash the slot entry and abdicate its cell for good. Tolerate a\n * throwing read so a mistaken prop degrades to \"not supplied\".\n */\nfunction read(source: unknown, key: string): unknown {\n if (source === null || typeof source !== 'object') return undefined\n try {\n return (source as Record<string, unknown>)[key]\n } catch {\n return undefined\n }\n}\n\nfunction readString(source: unknown, key: string): string | undefined {\n const value = read(source, key)\n return typeof value === 'string' ? value : undefined\n}\n\n/** Owner props win; native settings only supplies close, so resolve its selected session. */\nexport function useNativeSeat(client: NativeSurfaceClient, props?: unknown) {\n const current = useSyncExternalStore(\n useCallback((fn: () => void) => (client.uiWorkspace?.current ?? client.uiSession.adapter.current).subscribe(fn), [client]),\n useCallback(() => selectedSessionId(client), [client]),\n () => '',\n )\n const language = useSyncExternalStore(\n useCallback((fn: () => void) => client.locale.subscribe(fn), [client]),\n useCallback(() => client.locale.getSnapshot().active, [client]),\n () => 'en',\n )\n const sessionId = readString(props, 'sessionId') ?? readString(read(props, 'owner'), 'sessionId') ?? current\n const requestedLocale = read(props, 'locale') ?? read(read(props, 'owner'), 'locale') ?? language\n return { sessionId, locale: (String(requestedLocale).startsWith('zh') ? 'zh' : 'en') as 'zh' | 'en',\n hidden: read(props, 'hidden') === true || read(read(props, 'owner'), 'hidden') === true }\n}\n\n/** Refresh on native log boundaries/reconnect/focus. Read-only settling is bounded to an observed job. */\nexport function useFeatureRefresh(\n client: NativeSurfaceClient, sessionId: string, refresh: () => void,\n working = false, enabled = true,\n): void {\n const refreshRef = useRef(refresh)\n refreshRef.current = refresh\n useEffect(() => {\n if (!enabled) return\n let timer: ReturnType<typeof setTimeout> | undefined\n const queue = () => {\n if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return\n clearTimeout(timer)\n timer = setTimeout(() => refreshRef.current(), 150)\n }\n const source = sessionId ? client.sessions.binding?.(sessionId)?.eventSource : undefined\n const stopEvents = source?.subscribe(queue)\n const stopConnection = client.connection?.generation?.subscribe(queue)\n const visibility = () => { if (document.visibilityState === 'visible') queue() }\n window.addEventListener('focus', queue)\n document.addEventListener('visibilitychange', visibility)\n // Details are collapsed by default; opening an existing panel must get current state.\n document.addEventListener('toggle', queue, true)\n return () => {\n clearTimeout(timer)\n stopEvents?.(); stopConnection?.()\n window.removeEventListener('focus', queue)\n document.removeEventListener('visibilitychange', visibility)\n document.removeEventListener('toggle', queue, true)\n }\n }, [client, sessionId, enabled])\n useEffect(() => {\n if (!enabled || !working) return\n let delay = 250\n const until = Date.now() + 300_000\n let timer: ReturnType<typeof setTimeout>\n const tick = () => {\n if (Date.now() >= until) return\n if (document.visibilityState !== 'hidden') refreshRef.current()\n delay = Math.min(delay * 2, 2000)\n timer = setTimeout(tick, delay)\n }\n timer = setTimeout(tick, delay)\n return () => clearTimeout(timer)\n }, [client, sessionId, working, enabled])\n}\n\n/* Shared \"Session Management\" settings page. The page is root-owned shared\n * infrastructure, not a business plugin: any participant alone yields the same\n * section, and participants never import each other. Synchronization crosses\n * separately bundled copies of this helper through the Cordis root context —\n * `root.get`/`root.provide` is the only rendezvous, so no window global or\n * module singleton is needed and no participant depends on another being enabled.\n */\n\n/** A row contributed into the shared Session Management page. */\nexport interface SessionSettingsItemSpec {\n readonly id: string\n readonly order: number\n}\n\n/* Original line icons for settings-section navigation seats. The official\n * `dsh-client-ui-primitives` icon set covers generic glyphs only; these keep\n * the same conventions (16×16 viewBox, 1px stroke, round caps, currentColor)\n * so the plugin set stays visually consistent with the native nav. Colors\n * never appear here: `currentColor` lets the host style the active state, and\n * `aria-hidden` keeps the section label the single accessible name. */\n\n/** Owner props the host's `settings.section.icon` seat hands to its cell. */\nexport interface SettingsSectionIconProps {\n readonly size?: number\n /** Selection state; the nav styles active icons through color, not geometry. */\n readonly active?: boolean\n}\n\ntype IconChild = readonly [tag: string, attrs: Record<string, string | number>]\n\n/** One `<svg>` with the shared icon chrome; children are plain elements. */\nfunction lineIcon(children: IconChild[]) {\n return function SettingsSectionIcon(props: SettingsSectionIconProps): ReactNode {\n return createElement('svg', {\n 'aria-hidden': 'true',\n width: props.size ?? 16,\n height: props.size ?? 16,\n viewBox: '0 0 16 16',\n fill: 'none',\n stroke: 'currentColor',\n strokeWidth: 1,\n strokeLinecap: 'round',\n strokeLinejoin: 'round',\n }, ...children.map(([tag, attrs], index) => createElement(tag, { key: index, ...attrs })))\n }\n}\n\n/** Chat bubble for the shared Session Management section. */\nexport const IconSessionSettings = lineIcon([\n ['path', { d: 'M8.5 13a5.5 5.5 0 0 0 5.5-5.5A5.5 5.5 0 0 0 8.5 2 5.5 5.5 0 0 0 3 7.5a5.48 5.48 0 0 0 .9 3L2.8 12.6a.5.5 0 0 0 .62.62l2.9-.98a5.48 5.48 0 0 0 2.18.76Z' }],\n])\n\n/** Serif-flavoured letter A for the Font section. */\nexport const IconFontSettings = lineIcon([\n ['path', { d: 'M3.3 12.6 7.9 3.1a.5.5 0 0 1 .9 0l4.6 9.5' }],\n ['path', { d: 'M5.2 9.4h6.3' }],\n ['path', { d: 'M2.4 12.6h2.4' }],\n ['path', { d: 'M11.9 12.6h2.4' }],\n])\n\n/** Keyboard for the Shortcuts section. */\nexport const IconKeyboardSettings = lineIcon([\n ['rect', { x: 1.75, y: 4.75, width: 12.5, height: 7.5, rx: 1.2 }],\n ['path', { d: 'M4.5 7.25h0' }],\n ['path', { d: 'M8 7.25h0' }],\n ['path', { d: 'M11.5 7.25h0' }],\n ['path', { d: 'M5.25 9.75h5.5' }],\n])\n\n/** Linked model nodes for the Model Management section. */\nexport const IconModelNetworkSettings = lineIcon([\n ['line', { x1: 10.6, y1: 5.3, x2: 8.8, y2: 7.1 }],\n ['line', { x1: 7.2, y1: 8.9, x2: 5.4, y2: 10.7 }],\n ['circle', { cx: 12, cy: 4, r: 1.9 }],\n ['circle', { cx: 4, cy: 12, r: 1.9 }],\n ['circle', { cx: 8, cy: 8, r: 1.5 }],\n])\n\n/**\n * Contribute the nav icon of a `settings.section` entry. The `settings\n * .section.icon` seat is declared by the host independently of the section\n * list, so the wait runs in parallel: the icon appears as soon as both seats\n * exist and unloading removes it again, restoring the host's default glyph.\n * The `id` must equal the section id; `order`/`priority` mirror `order` so the\n * icon cell sorts like its section and, for shared sections, several\n * contributors can register the same icon id under different priorities\n * without duplicating nav entries.\n */\nexport function registerSettingsSectionIcon(\n ctx: Context,\n sectionId: string,\n order: number,\n component: (props: SettingsSectionIconProps) => ReactNode,\n): () => void {\n const slots = (ctx as unknown as { slots: SessionSettingsSlots }).slots\n return slots.inject(SETTINGS_SECTION_ICON_SLOT, () =>\n slots.register({ name: SETTINGS_SECTION_ICON_SLOT, id: sectionId, order, priority: order }, component))\n}\n\ninterface SessionSettingsSlots {\n inject(key: string, callback: () => Iterable<(() => void) | void> | (() => void) | void): () => void\n register(spec: Record<string, unknown>, render: unknown): () => void\n}\n\ninterface SessionSettingsRoot {\n get(name: string, strict?: boolean): unknown\n provide(name: string, value: unknown): () => void\n readonly slots?: SessionSettingsSlots\n}\n\n/** Private Cordis rendezvous; every bundled copy of this helper resolves the same root service. */\nconst SESSION_SETTINGS_PAGE_SERVICE = 'dshSessionSettingsPage'\n\ninterface SessionSettingsPage {\n retain(): void\n release(): void\n}\n\n/** The page component renders only its declared child list: items paint their own titles and styles. */\nfunction SessionSettingsPageView(props: { renderSlot: (key: string, owner: Record<string, never>) => ReactNode }): ReactNode {\n return props.renderSlot(SESSION_SETTINGS_ITEM_SLOT, {})\n}\n\n/**\n * Acquire the root-owned page service, creating and providing it on first use.\n * One counter covers participants, the section wait and the provider: the last\n * release removes the wait and then the provider synchronously (Cordis deletes\n * the impl before its async disposer awaits), so a later acquire sees a clean\n * get() miss and can re-establish the page.\n */\nfunction acquireSessionSettingsPage(root: SessionSettingsRoot): SessionSettingsPage {\n const existing = root.get(SESSION_SETTINGS_PAGE_SERVICE, false) as SessionSettingsPage | undefined\n if (existing) return existing\n const slots = root.slots as SessionSettingsSlots\n let users = 0\n let stopPage: (() => void) | undefined\n let unprovide: (() => void) | undefined\n const page: SessionSettingsPage = {\n retain() {\n // The reservation is counted before the throwing install, so a failure\n // lets the caller's release() undo it fully instead of leaking the service.\n users += 1\n if (!stopPage) {\n stopPage = slots.inject('settings.section', () => slots.register(\n {\n name: 'settings.section',\n id: SESSION_SETTINGS_SECTION_ID,\n order: 30,\n label: () => {\n const locale = root.get('locale', false) as { getSnapshot?(): { active?: unknown } } | undefined\n return String(locale?.getSnapshot?.().active ?? '').startsWith('zh') ? '会话管理' : 'Session Management'\n },\n children: { [SESSION_SETTINGS_ITEM_SLOT]: { kind: 'list', scope: 'root' } },\n },\n SessionSettingsPageView,\n ))\n }\n },\n release() {\n if (users <= 0) return\n users -= 1\n if (users > 0) return\n // The last contributor tears the section down; collapsing its children\n // declaration clears the item slot for every other bundle as well.\n stopPage?.()\n stopPage = undefined\n unprovide?.()\n },\n }\n unprovide = root.provide(SESSION_SETTINGS_PAGE_SERVICE, page)\n return page\n}\n\n/**\n * Contribute one row to the shared Session Management settings page.\n *\n * `ctx` is the plugin's own context: the row's slot wait unloads with the\n * plugin, while the page itself is retained on the shared root context for as\n * long as any contributor lives. There is no fallback seat — when every\n * contributor is gone the section disappears with the page. Returns a disposer.\n * Shared root-owned infrastructure (the auxiliary-model card) calls this on\n * `ctx.root` so its row, icon and page share are owned by the root lifetime\n * and released by the returned disposer, not by any plugin fiber.\n */\nexport function registerSessionSettingsItem(\n ctx: Context,\n item: SessionSettingsItemSpec,\n component: (props: never) => ReactNode,\n): () => void {\n const slots = (ctx as unknown as { slots: SessionSettingsSlots }).slots\n const root = ctx.root as unknown as SessionSettingsRoot\n const page = acquireSessionSettingsPage(root)\n let stopItem: (() => void) | undefined\n let stopIcon: (() => void) | undefined\n try {\n page.retain()\n stopItem = slots.inject(SESSION_SETTINGS_ITEM_SLOT, () => slots.register({ name: SESSION_SETTINGS_ITEM_SLOT, ...item }, component))\n // Every contributor also waits on the nav-icon seat, registering the same\n // section icon id under its own priority (its row order). Same-id entries\n // coexist on the ledger while the host renders one icon, so no contributor\n // owns the icon and none depends on another for it; the wait lives with\n // this ctx, so it fires now on fresh loads and retro-fills on HMR.\n stopIcon = slots.inject(SETTINGS_SECTION_ICON_SLOT, () => slots.register(\n { name: SETTINGS_SECTION_ICON_SLOT, id: SESSION_SETTINGS_SECTION_ID, order: item.order, priority: item.order },\n IconSessionSettings,\n ))\n } catch (error) {\n // A synchronous failure (for example a duplicate row id or a failing page\n // install) must not pin the shared page for a contributor that never joined.\n stopIcon?.()\n stopItem?.()\n page.release()\n throw error\n }\n // The disposer may be invoked by both ctx.effect and an explicit caller;\n // releasing twice would unbalance the shared refcount.\n let disposed = false\n return () => {\n if (disposed) return\n disposed = true\n stopIcon?.()\n stopItem?.()\n page.release()\n }\n}\n"],"mappings":"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"}
|
package/lib/domain.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { d as PluginLogger } from "./log-shared2.js";
|
|
1
2
|
//#region src/domain.d.ts
|
|
2
3
|
type PluginDomainSpec = {
|
|
4
|
+
name?: string;
|
|
3
5
|
version: number;
|
|
4
6
|
compatibleVersions?: readonly number[];
|
|
5
7
|
layout?: 'single' | 'per-record';
|
|
@@ -14,13 +16,19 @@ type PluginDomainHandle = {
|
|
|
14
16
|
};
|
|
15
17
|
/**
|
|
16
18
|
* Open table-only plugin state using the host's lossless legacy bootstrap.
|
|
17
|
-
* The JSON backend
|
|
18
|
-
*
|
|
19
|
-
*
|
|
19
|
+
* The JSON backend treats unsupported/malformed legacy files as absent in
|
|
20
|
+
* per-record layout; the probe below opens `single` with compatibleVersions
|
|
21
|
+
* emptied so a legacy stamp reports `version-mismatch` (whether or not the
|
|
22
|
+
* installed backend accepts compatible stamps in single layout, which
|
|
23
|
+
* storage-json began doing at 0.2.2). Probe an empty result so persistent
|
|
24
|
+
* settings cannot silently become defaults.
|
|
20
25
|
* A populated per-record tree is authoritative; the retained legacy source
|
|
21
26
|
* is an initial backup, not a current-data rollback after subsequent writes.
|
|
22
27
|
*/
|
|
23
|
-
declare function openCompatibleDomain<S extends PluginDomainSpec, D extends PluginDomainHandle>(open: (spec: S) => Promise<D>, spec: S
|
|
28
|
+
declare function openCompatibleDomain<S extends PluginDomainSpec, D extends PluginDomainHandle>(open: (spec: S) => Promise<D>, spec: S, options?: {
|
|
29
|
+
logger?: PluginLogger;
|
|
30
|
+
domainName?: string;
|
|
31
|
+
}): Promise<D>;
|
|
24
32
|
//#endregion
|
|
25
33
|
export { openCompatibleDomain };
|
|
26
34
|
//# sourceMappingURL=domain.d.ts.map
|
package/lib/domain.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
//#region src/domain.ts
|
|
2
2
|
/**
|
|
3
3
|
* Open table-only plugin state using the host's lossless legacy bootstrap.
|
|
4
|
-
* The JSON backend
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The JSON backend treats unsupported/malformed legacy files as absent in
|
|
5
|
+
* per-record layout; the probe below opens `single` with compatibleVersions
|
|
6
|
+
* emptied so a legacy stamp reports `version-mismatch` (whether or not the
|
|
7
|
+
* installed backend accepts compatible stamps in single layout, which
|
|
8
|
+
* storage-json began doing at 0.2.2). Probe an empty result so persistent
|
|
9
|
+
* settings cannot silently become defaults.
|
|
7
10
|
* A populated per-record tree is authoritative; the retained legacy source
|
|
8
11
|
* is an initial backup, not a current-data rollback after subsequent writes.
|
|
9
12
|
*/
|
|
10
|
-
async function openCompatibleDomain(open, spec) {
|
|
13
|
+
async function openCompatibleDomain(open, spec, options) {
|
|
14
|
+
const log = options?.logger;
|
|
15
|
+
const domainName = options?.domainName ?? (typeof spec.name === "string" ? spec.name : "plugin-domain");
|
|
11
16
|
const perRecord = {
|
|
12
17
|
...spec,
|
|
13
18
|
layout: "per-record"
|
|
@@ -19,7 +24,13 @@ async function openCompatibleDomain(open, spec) {
|
|
|
19
24
|
} finally {
|
|
20
25
|
if (!populated) await domain.close();
|
|
21
26
|
}
|
|
22
|
-
if (populated)
|
|
27
|
+
if (populated) {
|
|
28
|
+
log?.debug("domain.opened", "Opened populated per-record storage domain.", {
|
|
29
|
+
domain: domainName,
|
|
30
|
+
version: spec.version
|
|
31
|
+
});
|
|
32
|
+
return domain;
|
|
33
|
+
}
|
|
23
34
|
let mismatch;
|
|
24
35
|
for (const version of /* @__PURE__ */ new Set([spec.version, ...spec.compatibleVersions ?? []])) try {
|
|
25
36
|
await (await open({
|
|
@@ -28,11 +39,26 @@ async function openCompatibleDomain(open, spec) {
|
|
|
28
39
|
version,
|
|
29
40
|
compatibleVersions: []
|
|
30
41
|
})).close();
|
|
42
|
+
log?.info("domain.probed", "Opened storage domain after legacy probe.", {
|
|
43
|
+
domain: domainName,
|
|
44
|
+
probedVersion: version,
|
|
45
|
+
targetVersion: spec.version
|
|
46
|
+
});
|
|
31
47
|
return open(perRecord);
|
|
32
48
|
} catch (error) {
|
|
33
|
-
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "version-mismatch")
|
|
49
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "version-mismatch") {
|
|
50
|
+
log?.error("domain.open-failed", "Storage domain open failed during legacy probe.", error, {
|
|
51
|
+
domain: domainName,
|
|
52
|
+
probedVersion: version
|
|
53
|
+
});
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
34
56
|
mismatch = error;
|
|
35
57
|
}
|
|
58
|
+
log?.error("domain.version-mismatch", "Storage domain version is incompatible.", mismatch, {
|
|
59
|
+
domain: domainName,
|
|
60
|
+
targetVersion: spec.version
|
|
61
|
+
});
|
|
36
62
|
throw mismatch;
|
|
37
63
|
}
|
|
38
64
|
//#endregion
|
package/lib/domain.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domain.js","names":[],"sources":["../src/domain.ts"],"sourcesContent":["type PluginDomainSpec = {\n version: number\n compatibleVersions?: readonly number[]\n layout?: 'single' | 'per-record'\n tables: Record<string, unknown>\n global?: never\n}\n\ntype PluginDomainHandle = {\n table(name: string): { entries(): IterableIterator<[unknown, unknown]> }\n close(): Promise<void>\n}\n\n/**\n * Open table-only plugin state using the host's lossless legacy bootstrap.\n * The JSON backend
|
|
1
|
+
{"version":3,"file":"domain.js","names":[],"sources":["../src/domain.ts"],"sourcesContent":["import type { PluginLogger } from './log-shared.ts'\n\ntype PluginDomainSpec = {\n name?: string\n version: number\n compatibleVersions?: readonly number[]\n layout?: 'single' | 'per-record'\n tables: Record<string, unknown>\n global?: never\n}\n\ntype PluginDomainHandle = {\n table(name: string): { entries(): IterableIterator<[unknown, unknown]> }\n close(): Promise<void>\n}\n\n/**\n * Open table-only plugin state using the host's lossless legacy bootstrap.\n * The JSON backend treats unsupported/malformed legacy files as absent in\n * per-record layout; the probe below opens `single` with compatibleVersions\n * emptied so a legacy stamp reports `version-mismatch` (whether or not the\n * installed backend accepts compatible stamps in single layout, which\n * storage-json began doing at 0.2.2). Probe an empty result so persistent\n * settings cannot silently become defaults.\n * A populated per-record tree is authoritative; the retained legacy source\n * is an initial backup, not a current-data rollback after subsequent writes.\n */\nexport async function openCompatibleDomain<S extends PluginDomainSpec, D extends PluginDomainHandle>(\n open: (spec: S) => Promise<D>, spec: S,\n options?: { logger?: PluginLogger; domainName?: string },\n): Promise<D> {\n const log = options?.logger\n const domainName = options?.domainName ?? (typeof spec.name === 'string' ? spec.name : 'plugin-domain')\n const perRecord = { ...spec, layout: 'per-record' } as S\n const domain = await open(perRecord)\n let populated = false\n try {\n populated = Object.keys(spec.tables).some(table => !domain.table(table).entries().next().done)\n } finally {\n if (!populated) await domain.close()\n }\n if (populated) {\n log?.debug('domain.opened', 'Opened populated per-record storage domain.', { domain: domainName, version: spec.version })\n return domain\n }\n\n let mismatch: unknown\n for (const version of new Set([spec.version, ...spec.compatibleVersions ?? []])) {\n try {\n const legacy = await open({ ...spec, layout: 'single', version, compatibleVersions: [] } as S)\n await legacy.close()\n log?.info('domain.probed', 'Opened storage domain after legacy probe.', {\n domain: domainName, probedVersion: version, targetVersion: spec.version,\n })\n return open(perRecord)\n } catch (error) {\n if (!error || typeof error !== 'object' || !('code' in error) || error.code !== 'version-mismatch') {\n log?.error('domain.open-failed', 'Storage domain open failed during legacy probe.', error, {\n domain: domainName, probedVersion: version,\n })\n throw error\n }\n mismatch = error\n }\n }\n log?.error('domain.version-mismatch', 'Storage domain version is incompatible.', mismatch, {\n domain: domainName, targetVersion: spec.version,\n })\n throw mismatch\n}\n"],"mappings":";;;;;;;;;;;;AA2BA,eAAsB,qBACpB,MAA+B,MAC/B,SACY;CACZ,MAAM,MAAM,SAAS;CACrB,MAAM,aAAa,SAAS,eAAe,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO;CACvF,MAAM,YAAY;EAAE,GAAG;EAAM,QAAQ;CAAa;CAClD,MAAM,SAAS,MAAM,KAAK,SAAS;CACnC,IAAI,YAAY;CAChB,IAAI;EACF,YAAY,OAAO,KAAK,KAAK,MAAM,CAAC,CAAC,MAAK,UAAS,CAAC,OAAO,MAAM,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI;CAC/F,UAAU;EACR,IAAI,CAAC,WAAW,MAAM,OAAO,MAAM;CACrC;CACA,IAAI,WAAW;EACb,KAAK,MAAM,iBAAiB,+CAA+C;GAAE,QAAQ;GAAY,SAAS,KAAK;EAAQ,CAAC;EACxH,OAAO;CACT;CAEA,IAAI;CACJ,KAAK,MAAM,2BAAW,IAAI,IAAI,CAAC,KAAK,SAAS,GAAG,KAAK,sBAAsB,CAAC,CAAC,CAAC,GAC5E,IAAI;EAEF,OAAM,MADe,KAAK;GAAE,GAAG;GAAM,QAAQ;GAAU;GAAS,oBAAoB,CAAC;EAAE,CAAM,EAAA,CAChF,MAAM;EACnB,KAAK,KAAK,iBAAiB,6CAA6C;GACtE,QAAQ;GAAY,eAAe;GAAS,eAAe,KAAK;EAClE,CAAC;EACD,OAAO,KAAK,SAAS;CACvB,SAAS,OAAO;EACd,IAAI,CAAC,SAAS,OAAO,UAAU,YAAY,EAAE,UAAU,UAAU,MAAM,SAAS,oBAAoB;GAClG,KAAK,MAAM,sBAAsB,mDAAmD,OAAO;IACzF,QAAQ;IAAY,eAAe;GACrC,CAAC;GACD,MAAM;EACR;EACA,WAAW;CACb;CAEF,KAAK,MAAM,2BAA2B,2CAA2C,UAAU;EACzF,QAAQ;EAAY,eAAe,KAAK;CAC1C,CAAC;CACD,MAAM;AACR"}
|
package/lib/host-rpc.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RpcResult } from "./contracts.js";
|
|
2
|
+
import { d as PluginLogger } from "./log-shared2.js";
|
|
2
3
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
3
4
|
//#region src/host-rpc.d.ts
|
|
4
5
|
interface HostRpcContext {
|
|
@@ -24,6 +25,7 @@ interface HostRpcTransportOptions {
|
|
|
24
25
|
emptyBodyAsNull?: boolean;
|
|
25
26
|
beforeBody?: (request: IncomingMessage, endpoint: string) => Rejection | undefined;
|
|
26
27
|
errorBody?: (status: number, error?: unknown) => string | undefined;
|
|
28
|
+
logger?: PluginLogger;
|
|
27
29
|
}
|
|
28
30
|
interface HostRpcOptions extends HostRpcTransportOptions {
|
|
29
31
|
requirePayload?: boolean;
|