@klarkxy/dsh-plugin-kit 0.7.0 → 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.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 +6 -1
- package/lib/domain.js +26 -3
- 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 +3 -0
- package/lib/llm-call.js +79 -47
- 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/package.json +17 -2
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';
|
|
@@ -23,7 +25,10 @@ type PluginDomainHandle = {
|
|
|
23
25
|
* A populated per-record tree is authoritative; the retained legacy source
|
|
24
26
|
* is an initial backup, not a current-data rollback after subsequent writes.
|
|
25
27
|
*/
|
|
26
|
-
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>;
|
|
27
32
|
//#endregion
|
|
28
33
|
export { openCompatibleDomain };
|
|
29
34
|
//# sourceMappingURL=domain.d.ts.map
|
package/lib/domain.js
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* A populated per-record tree is authoritative; the retained legacy source
|
|
11
11
|
* is an initial backup, not a current-data rollback after subsequent writes.
|
|
12
12
|
*/
|
|
13
|
-
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");
|
|
14
16
|
const perRecord = {
|
|
15
17
|
...spec,
|
|
16
18
|
layout: "per-record"
|
|
@@ -22,7 +24,13 @@ async function openCompatibleDomain(open, spec) {
|
|
|
22
24
|
} finally {
|
|
23
25
|
if (!populated) await domain.close();
|
|
24
26
|
}
|
|
25
|
-
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
|
+
}
|
|
26
34
|
let mismatch;
|
|
27
35
|
for (const version of /* @__PURE__ */ new Set([spec.version, ...spec.compatibleVersions ?? []])) try {
|
|
28
36
|
await (await open({
|
|
@@ -31,11 +39,26 @@ async function openCompatibleDomain(open, spec) {
|
|
|
31
39
|
version,
|
|
32
40
|
compatibleVersions: []
|
|
33
41
|
})).close();
|
|
42
|
+
log?.info("domain.probed", "Opened storage domain after legacy probe.", {
|
|
43
|
+
domain: domainName,
|
|
44
|
+
probedVersion: version,
|
|
45
|
+
targetVersion: spec.version
|
|
46
|
+
});
|
|
34
47
|
return open(perRecord);
|
|
35
48
|
} catch (error) {
|
|
36
|
-
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
|
+
}
|
|
37
56
|
mismatch = error;
|
|
38
57
|
}
|
|
58
|
+
log?.error("domain.version-mismatch", "Storage domain version is incompatible.", mismatch, {
|
|
59
|
+
domain: domainName,
|
|
60
|
+
targetVersion: spec.version
|
|
61
|
+
});
|
|
39
62
|
throw mismatch;
|
|
40
63
|
}
|
|
41
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 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): Promise<D> {\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) return domain\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 return open(perRecord)\n } catch (error) {\n if (!error || typeof error !== 'object' || !('code' in error) || error.code !== 'version-mismatch') throw error\n mismatch = error\n }\n }\n throw mismatch\n}\n"],"mappings":";;;;;;;;;;;;
|
|
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;
|
package/lib/host-rpc.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { nextPluginOperationId, withPluginLogContext } from "./logger.js";
|
|
1
2
|
//#region src/host-rpc.ts
|
|
2
3
|
/** The shared authenticated HTTP boundary. Each plugin still owns its RPC envelope semantics. */
|
|
3
4
|
function registerHostRpcTransport(ctx, channel, dispatch, options = {}) {
|
|
4
5
|
const bodyLimit = options.maxBodyBytes ?? 65536;
|
|
5
6
|
if (!Number.isSafeInteger(bodyLimit) || bodyLimit <= 0) throw new Error("Invalid RPC body limit");
|
|
6
7
|
const isEndpoint = options.isEndpoint ?? ((endpoint) => /^[a-z.]+$/.test(endpoint));
|
|
8
|
+
const log = options.logger;
|
|
7
9
|
return ctx.webServer.register({
|
|
8
10
|
kind: "prefix",
|
|
9
11
|
path: channel,
|
|
@@ -14,29 +16,54 @@ function registerHostRpcTransport(ctx, channel, dispatch, options = {}) {
|
|
|
14
16
|
else res.writeHead(status);
|
|
15
17
|
res.end(override ?? options.errorBody?.(status, error) ?? (status === 500 ? "rpc request failed" : ""));
|
|
16
18
|
};
|
|
19
|
+
let endpoint = "";
|
|
17
20
|
try {
|
|
18
21
|
if (typeof ctx.connection.requestRejection !== "function") {
|
|
22
|
+
log?.error("rpc.admission.unavailable", "Host RPC admission policy is unavailable.", void 0, {
|
|
23
|
+
channel,
|
|
24
|
+
status: 503
|
|
25
|
+
});
|
|
19
26
|
reject(503);
|
|
20
27
|
return;
|
|
21
28
|
}
|
|
22
29
|
const rejection = ctx.connection.requestRejection(req);
|
|
23
30
|
if (rejection !== void 0) {
|
|
31
|
+
log?.warn("rpc.admission.rejected", "Host RPC request rejected by connection policy.", void 0, {
|
|
32
|
+
channel,
|
|
33
|
+
status: rejection
|
|
34
|
+
});
|
|
24
35
|
reject(rejection);
|
|
25
36
|
return;
|
|
26
37
|
}
|
|
27
38
|
const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
|
|
28
|
-
|
|
39
|
+
endpoint = pathname.startsWith(`${channel}/`) ? pathname.slice(channel.length + 1) : "";
|
|
29
40
|
if (req.method !== "POST" || !isEndpoint(endpoint)) {
|
|
41
|
+
log?.debug("rpc.route.rejected", "Host RPC route or method rejected.", {
|
|
42
|
+
channel,
|
|
43
|
+
endpoint: endpoint || null,
|
|
44
|
+
method: req.method ?? null,
|
|
45
|
+
status: 404
|
|
46
|
+
});
|
|
30
47
|
reject(404);
|
|
31
48
|
return;
|
|
32
49
|
}
|
|
33
50
|
const contentType = String(req.headers["content-type"] ?? "").split(";", 1)[0]?.trim();
|
|
34
51
|
if ((options.caseInsensitiveContentType ? contentType.toLowerCase() : contentType) !== "application/json") {
|
|
52
|
+
log?.debug("rpc.content-type.rejected", "Host RPC content-type rejected.", {
|
|
53
|
+
channel,
|
|
54
|
+
endpoint,
|
|
55
|
+
status: 415
|
|
56
|
+
});
|
|
35
57
|
reject(415);
|
|
36
58
|
return;
|
|
37
59
|
}
|
|
38
60
|
const guarded = options.beforeBody?.(req, endpoint);
|
|
39
61
|
if (guarded) {
|
|
62
|
+
log?.debug("rpc.guard.rejected", "Host RPC request rejected before body read.", {
|
|
63
|
+
channel,
|
|
64
|
+
endpoint,
|
|
65
|
+
status: guarded.status
|
|
66
|
+
});
|
|
40
67
|
reject(guarded.status, void 0, guarded.body);
|
|
41
68
|
return;
|
|
42
69
|
}
|
|
@@ -50,6 +77,11 @@ function registerHostRpcTransport(ctx, channel, dispatch, options = {}) {
|
|
|
50
77
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
51
78
|
size += buffer.length;
|
|
52
79
|
if (size > bodyLimit) {
|
|
80
|
+
log?.warn("rpc.body.too-large", "Host RPC request exceeded body limit.", void 0, {
|
|
81
|
+
channel,
|
|
82
|
+
endpoint,
|
|
83
|
+
maxBodyBytes: bodyLimit
|
|
84
|
+
});
|
|
53
85
|
reject(413);
|
|
54
86
|
req.destroy();
|
|
55
87
|
return;
|
|
@@ -60,11 +92,26 @@ function registerHostRpcTransport(ctx, channel, dispatch, options = {}) {
|
|
|
60
92
|
try {
|
|
61
93
|
body = JSON.parse(Buffer.concat(chunks).toString("utf8") || (options.emptyBodyAsNull ? "null" : ""));
|
|
62
94
|
} catch {
|
|
95
|
+
log?.debug("rpc.body.invalid-json", "Host RPC request body is not valid JSON.", {
|
|
96
|
+
channel,
|
|
97
|
+
endpoint,
|
|
98
|
+
status: 400
|
|
99
|
+
});
|
|
63
100
|
reject(400);
|
|
64
101
|
return;
|
|
65
102
|
}
|
|
66
|
-
|
|
103
|
+
if (log) await withPluginLogContext({
|
|
104
|
+
rpcChannel: channel,
|
|
105
|
+
rpcEndpoint: endpoint,
|
|
106
|
+
operationId: nextPluginOperationId("rpc")
|
|
107
|
+
}, () => dispatch(endpoint, body, controller.signal, res));
|
|
108
|
+
else await dispatch(endpoint, body, controller.signal, res);
|
|
67
109
|
} catch (error) {
|
|
110
|
+
log?.error("rpc.dispatch.failed", "Unhandled host RPC handler error.", error, {
|
|
111
|
+
channel,
|
|
112
|
+
endpoint: endpoint || null,
|
|
113
|
+
status: 500
|
|
114
|
+
});
|
|
68
115
|
reject(500, error);
|
|
69
116
|
}
|
|
70
117
|
}
|
|
@@ -85,19 +132,41 @@ function sendHostRpcResponse(response, rpcId, result, noStore = true) {
|
|
|
85
132
|
}
|
|
86
133
|
/** Safe host RPC registration. Missing requestRejection fails closed. */
|
|
87
134
|
function registerHostRpc(ctx, channel, call, options = {}) {
|
|
135
|
+
const log = options.logger;
|
|
88
136
|
return registerHostRpcTransport(ctx, channel, async (endpoint, body, signal, res) => {
|
|
89
137
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
138
|
+
log?.debug("rpc.envelope.invalid", "Host RPC envelope must be a JSON object.", {
|
|
139
|
+
channel,
|
|
140
|
+
endpoint
|
|
141
|
+
});
|
|
90
142
|
res.writeHead(400);
|
|
91
143
|
res.end();
|
|
92
144
|
return;
|
|
93
145
|
}
|
|
94
146
|
const request = body;
|
|
95
147
|
if (request.type !== "client-request" || request.method !== endpoint || typeof request.rpcId !== "string" || options.requirePayload && !Object.hasOwn(request, "payload")) {
|
|
148
|
+
log?.debug("rpc.envelope.invalid", "Host RPC envelope failed validation.", {
|
|
149
|
+
channel,
|
|
150
|
+
endpoint
|
|
151
|
+
});
|
|
96
152
|
res.writeHead(400);
|
|
97
153
|
res.end();
|
|
98
154
|
return;
|
|
99
155
|
}
|
|
156
|
+
const started = Date.now();
|
|
100
157
|
const raw = await call(endpoint, request.payload, signal);
|
|
158
|
+
const durationMs = Date.now() - started;
|
|
159
|
+
if (raw.ok) log?.debug("rpc.call.completed", "Host RPC call succeeded.", {
|
|
160
|
+
channel,
|
|
161
|
+
endpoint,
|
|
162
|
+
durationMs
|
|
163
|
+
});
|
|
164
|
+
else log?.warn("rpc.call.error", "Host RPC call returned an application error.", void 0, {
|
|
165
|
+
channel,
|
|
166
|
+
endpoint,
|
|
167
|
+
code: raw.error?.code ?? null,
|
|
168
|
+
durationMs
|
|
169
|
+
});
|
|
101
170
|
const result = raw.ok ? raw : {
|
|
102
171
|
...raw,
|
|
103
172
|
error: {
|
package/lib/host-rpc.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-rpc.js","names":[],"sources":["../src/host-rpc.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { RpcResult } from './contracts.ts'\n\nexport interface HostRpcContext {\n webServer: { register(route: { kind: 'prefix'; path: string; handler(req: IncomingMessage, res: ServerResponse): void | Promise<void> }): () => void }\n connection: { requestRejection?: (request: IncomingMessage) => number | undefined }\n}\n\ntype Rejection = { status: number; body?: string }\n\nexport interface HostRpcTransportOptions {\n maxBodyBytes?: number\n isEndpoint?: (endpoint: string) => boolean\n caseInsensitiveContentType?: boolean\n emptyBodyAsNull?: boolean\n beforeBody?: (request: IncomingMessage, endpoint: string) => Rejection | undefined\n errorBody?: (status: number, error?: unknown) => string | undefined\n}\n\nexport interface HostRpcOptions extends HostRpcTransportOptions {\n requirePayload?: boolean\n}\n\n/** The shared authenticated HTTP boundary. Each plugin still owns its RPC envelope semantics. */\nexport function registerHostRpcTransport(\n ctx: HostRpcContext,\n channel: string,\n dispatch: (endpoint: string, body: unknown, signal: AbortSignal, response: ServerResponse) => void | Promise<void>,\n options: HostRpcTransportOptions = {},\n): () => void {\n const bodyLimit = options.maxBodyBytes ?? 64 * 1024\n if (!Number.isSafeInteger(bodyLimit) || bodyLimit <= 0) throw new Error('Invalid RPC body limit')\n const isEndpoint = options.isEndpoint ?? ((endpoint: string) => /^[a-z.]+$/.test(endpoint))\n return ctx.webServer.register({ kind: 'prefix', path: channel, handler: async (req, res) => {\n const reject = (status: number, error?: unknown, override?: string) => {\n if (res.destroyed) return\n if (status === 413) res.writeHead(status, { connection: 'close' })\n else res.writeHead(status)\n res.end(override ?? options.errorBody?.(status, error) ?? (status === 500 ? 'rpc request failed' : ''))\n }\n try {\n // Host admission always runs before plugin-specific checks or body reads.\n if (typeof ctx.connection.requestRejection !== 'function') { reject(503); return }\n const rejection = ctx.connection.requestRejection(req)\n if (rejection !== undefined) { reject(rejection); return }\n const pathname = new URL(req.url ?? '/', 'http://dsh.internal').pathname\n const endpoint = pathname.startsWith(`${channel}/`) ? pathname.slice(channel.length + 1) : ''\n if (req.method !== 'POST' || !isEndpoint(endpoint)) { reject(404); return }\n const contentType = String(req.headers['content-type'] ?? '').split(';', 1)[0]?.trim()\n if ((options.caseInsensitiveContentType ? contentType.toLowerCase() : contentType) !== 'application/json') {\n reject(415); return\n }\n const guarded = options.beforeBody?.(req, endpoint)\n if (guarded) { reject(guarded.status, undefined, guarded.body); return }\n\n const controller = new AbortController()\n res.on('close', () => { if (!res.writableEnded) controller.abort() })\n const chunks: Buffer[] = []\n let size = 0\n for await (const chunk of req) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n size += buffer.length\n if (size > bodyLimit) { reject(413); req.destroy(); return }\n chunks.push(buffer)\n }\n let body: unknown\n try { body = JSON.parse(Buffer.concat(chunks).toString('utf8') || (options.emptyBodyAsNull ? 'null' : '')) }\n catch { reject(400); return }\n await dispatch(endpoint, body, controller.signal, res)\n } catch (error) {\n reject(500, error)\n }\n } })\n}\n\n/** Write the native Connection response envelope after a plugin has handled a request. */\nexport function sendHostRpcResponse(response: ServerResponse, rpcId: string, result: unknown, noStore = true): void {\n if (response.destroyed) return\n response.writeHead(200, { 'content-type': 'application/json', ...(noStore ? { 'cache-control': 'no-store' } : {}) })\n response.end(JSON.stringify({ type: 'server-response', rpcId, result }))\n}\n\n/** Safe host RPC registration. Missing requestRejection fails closed. */\nexport function registerHostRpc<R extends RpcResult = RpcResult>(\n ctx: HostRpcContext,\n channel: string,\n call: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<R>,\n options: HostRpcOptions = {},\n): () => void {\n return registerHostRpcTransport(ctx, channel, async (endpoint, body, signal, res) => {\n if (!body || typeof body !== 'object' || Array.isArray(body)) { res.writeHead(400); res.end(); return }\n const request = body as Record<string, unknown>\n if (request.type !== 'client-request' || request.method !== endpoint || typeof request.rpcId !== 'string'\n || (options.requirePayload && !Object.hasOwn(request, 'payload'))) {\n res.writeHead(400); res.end(); return\n }\n const raw = await call(endpoint, request.payload, signal)\n const result = raw.ok ? raw : { ...raw, error: { details: {}, ...raw.error } }\n sendHostRpcResponse(res, request.rpcId, result)\n }, options)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"host-rpc.js","names":[],"sources":["../src/host-rpc.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { RpcResult } from './contracts.ts'\nimport { nextPluginOperationId, withPluginLogContext, type PluginLogger } from './logger.ts'\n\nexport interface HostRpcContext {\n webServer: { register(route: { kind: 'prefix'; path: string; handler(req: IncomingMessage, res: ServerResponse): void | Promise<void> }): () => void }\n connection: { requestRejection?: (request: IncomingMessage) => number | undefined }\n}\n\ntype Rejection = { status: number; body?: string }\n\nexport interface HostRpcTransportOptions {\n maxBodyBytes?: number\n isEndpoint?: (endpoint: string) => boolean\n caseInsensitiveContentType?: boolean\n emptyBodyAsNull?: boolean\n beforeBody?: (request: IncomingMessage, endpoint: string) => Rejection | undefined\n errorBody?: (status: number, error?: unknown) => string | undefined\n logger?: PluginLogger\n}\n\nexport interface HostRpcOptions extends HostRpcTransportOptions {\n requirePayload?: boolean\n}\n\n/** The shared authenticated HTTP boundary. Each plugin still owns its RPC envelope semantics. */\nexport function registerHostRpcTransport(\n ctx: HostRpcContext,\n channel: string,\n dispatch: (endpoint: string, body: unknown, signal: AbortSignal, response: ServerResponse) => void | Promise<void>,\n options: HostRpcTransportOptions = {},\n): () => void {\n const bodyLimit = options.maxBodyBytes ?? 64 * 1024\n if (!Number.isSafeInteger(bodyLimit) || bodyLimit <= 0) throw new Error('Invalid RPC body limit')\n const isEndpoint = options.isEndpoint ?? ((endpoint: string) => /^[a-z.]+$/.test(endpoint))\n const log = options.logger\n return ctx.webServer.register({ kind: 'prefix', path: channel, handler: async (req, res) => {\n const reject = (status: number, error?: unknown, override?: string) => {\n if (res.destroyed) return\n if (status === 413) res.writeHead(status, { connection: 'close' })\n else res.writeHead(status)\n res.end(override ?? options.errorBody?.(status, error) ?? (status === 500 ? 'rpc request failed' : ''))\n }\n let endpoint = ''\n try {\n // Host admission always runs before plugin-specific checks or body reads.\n if (typeof ctx.connection.requestRejection !== 'function') {\n log?.error('rpc.admission.unavailable', 'Host RPC admission policy is unavailable.', undefined, { channel, status: 503 })\n reject(503)\n return\n }\n const rejection = ctx.connection.requestRejection(req)\n if (rejection !== undefined) {\n log?.warn('rpc.admission.rejected', 'Host RPC request rejected by connection policy.', undefined, { channel, status: rejection })\n reject(rejection)\n return\n }\n const pathname = new URL(req.url ?? '/', 'http://dsh.internal').pathname\n endpoint = pathname.startsWith(`${channel}/`) ? pathname.slice(channel.length + 1) : ''\n if (req.method !== 'POST' || !isEndpoint(endpoint)) {\n log?.debug('rpc.route.rejected', 'Host RPC route or method rejected.', { channel, endpoint: endpoint || null, method: req.method ?? null, status: 404 })\n reject(404)\n return\n }\n const contentType = String(req.headers['content-type'] ?? '').split(';', 1)[0]?.trim()\n if ((options.caseInsensitiveContentType ? contentType.toLowerCase() : contentType) !== 'application/json') {\n log?.debug('rpc.content-type.rejected', 'Host RPC content-type rejected.', { channel, endpoint, status: 415 })\n reject(415); return\n }\n const guarded = options.beforeBody?.(req, endpoint)\n if (guarded) {\n log?.debug('rpc.guard.rejected', 'Host RPC request rejected before body read.', { channel, endpoint, status: guarded.status })\n reject(guarded.status, undefined, guarded.body)\n return\n }\n\n const controller = new AbortController()\n res.on('close', () => { if (!res.writableEnded) controller.abort() })\n const chunks: Buffer[] = []\n let size = 0\n for await (const chunk of req) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n size += buffer.length\n if (size > bodyLimit) {\n log?.warn('rpc.body.too-large', 'Host RPC request exceeded body limit.', undefined, { channel, endpoint, maxBodyBytes: bodyLimit })\n reject(413); req.destroy(); return\n }\n chunks.push(buffer)\n }\n let body: unknown\n try { body = JSON.parse(Buffer.concat(chunks).toString('utf8') || (options.emptyBodyAsNull ? 'null' : '')) }\n catch {\n log?.debug('rpc.body.invalid-json', 'Host RPC request body is not valid JSON.', { channel, endpoint, status: 400 })\n reject(400)\n return\n }\n if (log) {\n await withPluginLogContext(\n { rpcChannel: channel, rpcEndpoint: endpoint, operationId: nextPluginOperationId('rpc') },\n () => dispatch(endpoint, body, controller.signal, res),\n )\n } else {\n await dispatch(endpoint, body, controller.signal, res)\n }\n } catch (error) {\n log?.error('rpc.dispatch.failed', 'Unhandled host RPC handler error.', error, { channel, endpoint: endpoint || null, status: 500 })\n reject(500, error)\n }\n } })\n}\n\n/** Write the native Connection response envelope after a plugin has handled a request. */\nexport function sendHostRpcResponse(response: ServerResponse, rpcId: string, result: unknown, noStore = true): void {\n if (response.destroyed) return\n response.writeHead(200, { 'content-type': 'application/json', ...(noStore ? { 'cache-control': 'no-store' } : {}) })\n response.end(JSON.stringify({ type: 'server-response', rpcId, result }))\n}\n\n/** Safe host RPC registration. Missing requestRejection fails closed. */\nexport function registerHostRpc<R extends RpcResult = RpcResult>(\n ctx: HostRpcContext,\n channel: string,\n call: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<R>,\n options: HostRpcOptions = {},\n): () => void {\n const log = options.logger\n return registerHostRpcTransport(ctx, channel, async (endpoint, body, signal, res) => {\n if (!body || typeof body !== 'object' || Array.isArray(body)) {\n log?.debug('rpc.envelope.invalid', 'Host RPC envelope must be a JSON object.', { channel, endpoint })\n res.writeHead(400); res.end(); return\n }\n const request = body as Record<string, unknown>\n if (request.type !== 'client-request' || request.method !== endpoint || typeof request.rpcId !== 'string'\n || (options.requirePayload && !Object.hasOwn(request, 'payload'))) {\n log?.debug('rpc.envelope.invalid', 'Host RPC envelope failed validation.', { channel, endpoint })\n res.writeHead(400); res.end(); return\n }\n const started = Date.now()\n const raw = await call(endpoint, request.payload, signal)\n const durationMs = Date.now() - started\n if (raw.ok) {\n log?.debug('rpc.call.completed', 'Host RPC call succeeded.', { channel, endpoint, durationMs })\n } else {\n log?.warn('rpc.call.error', 'Host RPC call returned an application error.', undefined, {\n channel, endpoint, code: raw.error?.code ?? null, durationMs,\n })\n }\n const result = raw.ok ? raw : { ...raw, error: { details: {}, ...raw.error } }\n sendHostRpcResponse(res, request.rpcId, result)\n }, options)\n}\n"],"mappings":"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"}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { AuxiliaryContextTooLargeError, buildAuxiliaryUserText, packAuxiliaryContext } from "./auxiliary-context.js";
|
|
2
2
|
import { AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_CHANNEL, AUXILIARY_MODEL_NS, AUXILIARY_MODEL_SERVICE, AuxiliaryModelDefaults, AuxiliaryModelShared, CHAT_EVENTS_SLOT, CREATOR_PRESET_ID, ContextKnowledge, DshWebManagementDescriptor, EvidenceRef, KnowledgeKind, KnowledgeScope, MemoryMutationOptions, MemoryQuery, MemoryRecord, MemoryService, ModelRoute, ModelSettingsChangesIdentity, NewMemoryRecord, ProcedureKnowledge, ProducerMessageSource, ProducerSourceKind, RpcResult, SESSION_SETTINGS_ITEM_SLOT, SESSION_SETTINGS_SECTION_ID, SETTINGS_SECTION_ICON_SLOT, SubagentSessionLike, TaskCheckpoint, TaskContract, WEB_PROVIDER_AVAILABILITY_EVENT, getAuxiliaryModel, isSubagentSession, modelSettingsChangesIdentity, producerMessageSource, projectIdFromCwd } from "./contracts.js";
|
|
3
|
+
import { a as PluginLogContext, c as PluginLogInput, d as PluginLogger, l as PluginLogLevel, o as PluginLogEntry } from "./log-shared2.js";
|
|
3
4
|
import { openCompatibleDomain } from "./domain.js";
|
|
5
|
+
import { RotatingPluginLogger, RotatingPluginLoggerOptions, createPluginLogger, currentPluginLogContext, installPluginLogger, nextPluginOperationId, pluginLogDirectory, pluginLogStep, pluginLogSyncStep, readPluginLogTail, withPluginLogContext } from "./logger.js";
|
|
4
6
|
import { HostRpcContext, registerHostRpc } from "./host-rpc.js";
|
|
5
7
|
import { FeatureModelRoute, LlmTextCaller, callLlmText, resolveFeatureModel } from "./llm-call.js";
|
|
6
|
-
export { type AUXILIARY_MODEL_CHANGES_IDENTITY, type AUXILIARY_MODEL_CHANNEL, type AUXILIARY_MODEL_NS, type AUXILIARY_MODEL_SERVICE, AuxiliaryContextTooLargeError, type AuxiliaryModelDefaults, type AuxiliaryModelShared, CHAT_EVENTS_SLOT, type CREATOR_PRESET_ID, type ContextKnowledge, type DshWebManagementDescriptor, type EvidenceRef, type FeatureModelRoute, type HostRpcContext, type KnowledgeKind, type KnowledgeScope, type LlmTextCaller, type MemoryMutationOptions, type MemoryQuery, type MemoryRecord, type MemoryService, type ModelRoute, type ModelSettingsChangesIdentity, type NewMemoryRecord, type ProcedureKnowledge, type ProducerMessageSource, type ProducerSourceKind, type RpcResult, type SESSION_SETTINGS_ITEM_SLOT, type SESSION_SETTINGS_SECTION_ID, type SETTINGS_SECTION_ICON_SLOT, type SubagentSessionLike, type TaskCheckpoint, type TaskContract, type WEB_PROVIDER_AVAILABILITY_EVENT, buildAuxiliaryUserText, callLlmText, type getAuxiliaryModel, type isSubagentSession, type modelSettingsChangesIdentity, openCompatibleDomain, packAuxiliaryContext, producerMessageSource, projectIdFromCwd, registerHostRpc, resolveFeatureModel };
|
|
8
|
+
export { type AUXILIARY_MODEL_CHANGES_IDENTITY, type AUXILIARY_MODEL_CHANNEL, type AUXILIARY_MODEL_NS, type AUXILIARY_MODEL_SERVICE, AuxiliaryContextTooLargeError, type AuxiliaryModelDefaults, type AuxiliaryModelShared, CHAT_EVENTS_SLOT, type CREATOR_PRESET_ID, type ContextKnowledge, type DshWebManagementDescriptor, type EvidenceRef, type FeatureModelRoute, type HostRpcContext, type KnowledgeKind, type KnowledgeScope, type LlmTextCaller, type MemoryMutationOptions, type MemoryQuery, type MemoryRecord, type MemoryService, type ModelRoute, type ModelSettingsChangesIdentity, type NewMemoryRecord, type PluginLogContext, type PluginLogEntry, type PluginLogInput, type PluginLogLevel, type PluginLogger, type ProcedureKnowledge, type ProducerMessageSource, type ProducerSourceKind, RotatingPluginLogger, type RotatingPluginLoggerOptions, type RpcResult, type SESSION_SETTINGS_ITEM_SLOT, type SESSION_SETTINGS_SECTION_ID, type SETTINGS_SECTION_ICON_SLOT, type SubagentSessionLike, type TaskCheckpoint, type TaskContract, type WEB_PROVIDER_AVAILABILITY_EVENT, buildAuxiliaryUserText, callLlmText, createPluginLogger, currentPluginLogContext, type getAuxiliaryModel, installPluginLogger, type isSubagentSession, type modelSettingsChangesIdentity, nextPluginOperationId, openCompatibleDomain, packAuxiliaryContext, pluginLogDirectory, pluginLogStep, pluginLogSyncStep, producerMessageSource, projectIdFromCwd, readPluginLogTail, registerHostRpc, resolveFeatureModel, withPluginLogContext };
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CHAT_EVENTS_SLOT, producerMessageSource, projectIdFromCwd } from "./contracts.js";
|
|
2
|
+
import { RotatingPluginLogger, createPluginLogger, currentPluginLogContext, installPluginLogger, nextPluginOperationId, pluginLogDirectory, pluginLogStep, pluginLogSyncStep, readPluginLogTail, withPluginLogContext } from "./logger.js";
|
|
2
3
|
import { registerHostRpc } from "./host-rpc.js";
|
|
3
4
|
import { openCompatibleDomain } from "./domain.js";
|
|
4
5
|
import { AuxiliaryContextTooLargeError, buildAuxiliaryUserText, packAuxiliaryContext } from "./auxiliary-context.js";
|
|
5
6
|
import { callLlmText, resolveFeatureModel } from "./llm-call.js";
|
|
6
|
-
export { AuxiliaryContextTooLargeError, CHAT_EVENTS_SLOT, buildAuxiliaryUserText, callLlmText, openCompatibleDomain, packAuxiliaryContext, producerMessageSource, projectIdFromCwd, registerHostRpc, resolveFeatureModel };
|
|
7
|
+
export { AuxiliaryContextTooLargeError, CHAT_EVENTS_SLOT, RotatingPluginLogger, buildAuxiliaryUserText, callLlmText, createPluginLogger, currentPluginLogContext, installPluginLogger, nextPluginOperationId, openCompatibleDomain, packAuxiliaryContext, pluginLogDirectory, pluginLogStep, pluginLogSyncStep, producerMessageSource, projectIdFromCwd, readPluginLogTail, registerHostRpc, resolveFeatureModel, withPluginLogContext };
|
package/lib/llm-call.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { d as PluginLogger } from "./log-shared2.js";
|
|
1
2
|
import { GenerateOptions, LlmRuntime } from "@deepseek-ai/dsh-llm";
|
|
2
3
|
//#region src/llm-call.d.ts
|
|
3
4
|
/** A feature's own saved model. Empty means the host default chat model. */
|
|
@@ -32,6 +33,8 @@ interface LlmTextRequest {
|
|
|
32
33
|
configure?: Parameters<LlmRuntime['prepareCall']>[2];
|
|
33
34
|
/** Rechecked before the call returns text. A false result discards the output. */
|
|
34
35
|
isCurrent?: () => boolean;
|
|
36
|
+
/** Optional diagnostic logger for timing, token usage, and failure telemetry. */
|
|
37
|
+
logger?: PluginLogger;
|
|
35
38
|
}
|
|
36
39
|
interface LlmTextResult {
|
|
37
40
|
text: string;
|
package/lib/llm-call.js
CHANGED
|
@@ -72,57 +72,89 @@ async function callLlmText(llm, request) {
|
|
|
72
72
|
request.signal.throwIfAborted();
|
|
73
73
|
const live = () => request.isCurrent?.() !== false && !request.signal.aborted;
|
|
74
74
|
if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
75
|
-
const
|
|
76
|
-
const
|
|
75
|
+
const started = Date.now();
|
|
76
|
+
const logBase = {
|
|
77
|
+
plugin: request.plugin,
|
|
78
|
+
feature: request.feature ?? null,
|
|
77
79
|
provider: request.route.provider,
|
|
78
80
|
model: request.route.model,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}, request.signal, request.configure);
|
|
82
|
-
if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
83
|
-
const maxTokens = request.maxTokens === void 0 ? prepared.config.maxTokens : Math.min(request.maxTokens, prepared.config.maxTokens ?? request.maxTokens);
|
|
84
|
-
const options = {
|
|
85
|
-
provider: prepared.config.provider,
|
|
86
|
-
model: prepared.config.model,
|
|
87
|
-
...prepared.config.reasoningEffort ? { reasoningEffort: prepared.config.reasoningEffort } : {},
|
|
88
|
-
...maxTokens !== void 0 ? { maxTokens } : {},
|
|
89
|
-
...prepared.config.temperature !== void 0 ? { temperature: prepared.config.temperature } : {},
|
|
90
|
-
...prepared.config.stop ? { stop: [...prepared.config.stop] } : {},
|
|
91
|
-
system: request.system,
|
|
92
|
-
messages: [createUserMessage({
|
|
93
|
-
source: producerMessageSource(request.plugin),
|
|
94
|
-
content: [{
|
|
95
|
-
type: "text",
|
|
96
|
-
text: buildAuxiliaryUserText(request)
|
|
97
|
-
}]
|
|
98
|
-
})],
|
|
99
|
-
signal: request.signal,
|
|
100
|
-
...request.purpose ? { purpose: request.purpose } : {},
|
|
101
|
-
...request.sessionId ? { sessionId: request.sessionId } : {}
|
|
81
|
+
purpose: request.purpose ?? null,
|
|
82
|
+
sessionId: request.sessionId ?? null
|
|
102
83
|
};
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
84
|
+
request.logger?.debug("llm.call.started", "Auxiliary model call started.", logBase);
|
|
85
|
+
try {
|
|
86
|
+
const effort = request.route.reasoningEffort?.trim();
|
|
87
|
+
const prepared = await llm.prepareCall({
|
|
88
|
+
provider: request.route.provider,
|
|
89
|
+
model: request.route.model,
|
|
90
|
+
...effort ? { reasoningEffort: ReasoningEffortId(effort) } : {},
|
|
91
|
+
...request.maxTokens !== void 0 ? { maxTokens: request.maxTokens } : {}
|
|
92
|
+
}, request.signal, request.configure);
|
|
93
|
+
if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
94
|
+
const maxTokens = request.maxTokens === void 0 ? prepared.config.maxTokens : Math.min(request.maxTokens, prepared.config.maxTokens ?? request.maxTokens);
|
|
95
|
+
const options = {
|
|
96
|
+
provider: prepared.config.provider,
|
|
97
|
+
model: prepared.config.model,
|
|
98
|
+
...prepared.config.reasoningEffort ? { reasoningEffort: prepared.config.reasoningEffort } : {},
|
|
99
|
+
...maxTokens !== void 0 ? { maxTokens } : {},
|
|
100
|
+
...prepared.config.temperature !== void 0 ? { temperature: prepared.config.temperature } : {},
|
|
101
|
+
...prepared.config.stop ? { stop: [...prepared.config.stop] } : {},
|
|
102
|
+
system: request.system,
|
|
103
|
+
messages: [createUserMessage({
|
|
104
|
+
source: producerMessageSource(request.plugin),
|
|
105
|
+
content: [{
|
|
106
|
+
type: "text",
|
|
107
|
+
text: buildAuxiliaryUserText(request)
|
|
108
|
+
}]
|
|
109
|
+
})],
|
|
110
|
+
signal: request.signal,
|
|
111
|
+
...request.purpose ? { purpose: request.purpose } : {},
|
|
112
|
+
...request.sessionId ? { sessionId: request.sessionId } : {}
|
|
113
|
+
};
|
|
114
|
+
const assembler = await readStream(prepared.stream(options), request.signal, live);
|
|
115
|
+
if (request.signal.aborted || assembler.finish.kind === "aborted") throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
116
|
+
if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
117
|
+
if (assembler.finish.kind === "error") {
|
|
118
|
+
const code = assembler.finish.failure.code;
|
|
119
|
+
throw Object.assign(new Error(assembler.finish.failure.message || "模型调用失败。"), { code });
|
|
120
|
+
}
|
|
121
|
+
if (assembler.blocks().some((block) => block.type === "tool-call") || assembler.finish.kind === "tool-calls") throw new Error("这次调用不使用工具。");
|
|
122
|
+
if (assembler.finish.kind !== "stop") throw new Error("模型调用未完成。");
|
|
123
|
+
const usage = assembler.usage;
|
|
124
|
+
request.logger?.debug("llm.call.completed", "Auxiliary model call completed.", {
|
|
125
|
+
...logBase,
|
|
126
|
+
provider: prepared.config.provider,
|
|
127
|
+
model: prepared.config.model,
|
|
128
|
+
durationMs: Date.now() - started,
|
|
129
|
+
...usage?.inputTokens !== void 0 ? { inputTokens: usage.inputTokens } : {},
|
|
130
|
+
...usage?.outputTokens !== void 0 ? { outputTokens: usage.outputTokens } : {},
|
|
131
|
+
...usage?.totalTokens !== void 0 ? { totalTokens: usage.totalTokens } : {}
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
text: joinText(assembler),
|
|
135
|
+
provider: prepared.config.provider,
|
|
136
|
+
model: prepared.config.model,
|
|
137
|
+
...usage ? {
|
|
138
|
+
inputTokens: usage.inputTokens,
|
|
139
|
+
outputTokens: usage.outputTokens,
|
|
140
|
+
...usage.totalTokens !== void 0 ? { totalTokens: usage.totalTokens } : {},
|
|
141
|
+
...usage.cacheReadTokens !== void 0 ? { cacheReadTokens: usage.cacheReadTokens } : {},
|
|
142
|
+
...usage.cacheWriteTokens !== void 0 ? { cacheWriteTokens: usage.cacheWriteTokens } : {},
|
|
143
|
+
...usage.reasoningTokens !== void 0 ? { reasoningTokens: usage.reasoningTokens } : {}
|
|
144
|
+
} : {}
|
|
145
|
+
};
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const durationMs = Date.now() - started;
|
|
148
|
+
if (request.signal.aborted || error instanceof Error && error.name === "AbortError") request.logger?.debug("llm.call.cancelled", "Auxiliary model call cancelled.", {
|
|
149
|
+
...logBase,
|
|
150
|
+
durationMs
|
|
151
|
+
});
|
|
152
|
+
else request.logger?.warn("llm.call.failed", "Auxiliary model call failed.", error, {
|
|
153
|
+
...logBase,
|
|
154
|
+
durationMs
|
|
155
|
+
});
|
|
156
|
+
throw error;
|
|
109
157
|
}
|
|
110
|
-
if (assembler.blocks().some((block) => block.type === "tool-call") || assembler.finish.kind === "tool-calls") throw new Error("这次调用不使用工具。");
|
|
111
|
-
if (assembler.finish.kind !== "stop") throw new Error("模型调用未完成。");
|
|
112
|
-
const usage = assembler.usage;
|
|
113
|
-
return {
|
|
114
|
-
text: joinText(assembler),
|
|
115
|
-
provider: prepared.config.provider,
|
|
116
|
-
model: prepared.config.model,
|
|
117
|
-
...usage ? {
|
|
118
|
-
inputTokens: usage.inputTokens,
|
|
119
|
-
outputTokens: usage.outputTokens,
|
|
120
|
-
...usage.totalTokens !== void 0 ? { totalTokens: usage.totalTokens } : {},
|
|
121
|
-
...usage.cacheReadTokens !== void 0 ? { cacheReadTokens: usage.cacheReadTokens } : {},
|
|
122
|
-
...usage.cacheWriteTokens !== void 0 ? { cacheWriteTokens: usage.cacheWriteTokens } : {},
|
|
123
|
-
...usage.reasoningTokens !== void 0 ? { reasoningTokens: usage.reasoningTokens } : {}
|
|
124
|
-
} : {}
|
|
125
|
-
};
|
|
126
158
|
}
|
|
127
159
|
//#endregion
|
|
128
160
|
export { callLlmText, resolveFeatureModel };
|