@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.
Files changed (44) hide show
  1. package/README.md +1 -1
  2. package/docs/README.zh-CN.md +1 -1
  3. package/lib/auxiliary-model-client.d.ts +4 -1
  4. package/lib/auxiliary-model-client.js +72 -33
  5. package/lib/auxiliary-model-client.js.map +1 -1
  6. package/lib/auxiliary-model.js +26 -2
  7. package/lib/auxiliary-model.js.map +1 -1
  8. package/lib/cli-media.js +1 -1
  9. package/lib/cli-process.d.ts +2 -0
  10. package/lib/cli-process.js +36 -2
  11. package/lib/cli-process.js.map +1 -1
  12. package/lib/client-logger.d.ts +19 -0
  13. package/lib/client-logger.js +153 -0
  14. package/lib/client-logger.js.map +1 -0
  15. package/lib/client-utils.d.ts +2 -1
  16. package/lib/client-utils.js +9 -3
  17. package/lib/client-utils.js.map +1 -1
  18. package/lib/domain.d.ts +12 -4
  19. package/lib/domain.js +32 -6
  20. package/lib/domain.js.map +1 -1
  21. package/lib/host-rpc.d.ts +2 -0
  22. package/lib/host-rpc.js +71 -2
  23. package/lib/host-rpc.js.map +1 -1
  24. package/lib/index.d.ts +3 -1
  25. package/lib/index.js +2 -1
  26. package/lib/llm-call.d.ts +5 -1
  27. package/lib/llm-call.js +79 -46
  28. package/lib/llm-call.js.map +1 -1
  29. package/lib/log-shared.d.ts +2 -0
  30. package/lib/log-shared.js +169 -0
  31. package/lib/log-shared.js.map +1 -0
  32. package/lib/log-shared2.d.ts +72 -0
  33. package/lib/logger.d.ts +87 -0
  34. package/lib/logger.js +572 -0
  35. package/lib/logger.js.map +1 -0
  36. package/lib/model-picker.d.ts +34 -0
  37. package/lib/model-picker.js +96 -0
  38. package/lib/model-picker.js.map +1 -0
  39. package/lib/official-ui.d.ts +1 -1
  40. package/lib/official-ui.js +3 -1
  41. package/lib/official-ui.js.map +1 -1
  42. package/lib/zod.js +79 -79
  43. package/lib/zod.js.map +1 -1
  44. package/package.json +34 -14
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
- const endpoint = pathname.startsWith(`${channel}/`) ? pathname.slice(channel.length + 1) : "";
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
- await dispatch(endpoint, body, controller.signal, res);
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: {
@@ -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":";;;AA0BA,SAAgB,yBACd,KACA,SACA,UACA,UAAmC,CAAC,GACxB;CACZ,MAAM,YAAY,QAAQ,gBAAgB;CAC1C,IAAI,CAAC,OAAO,cAAc,SAAS,KAAK,aAAa,GAAG,MAAM,IAAI,MAAM,wBAAwB;CAChG,MAAM,aAAa,QAAQ,gBAAgB,aAAqB,YAAY,KAAK,QAAQ;CACzF,MAAM,MAAM,QAAQ;CACpB,OAAO,IAAI,UAAU,SAAS;EAAE,MAAM;EAAU,MAAM;EAAS,SAAS,OAAO,KAAK,QAAQ;GAC1F,MAAM,UAAU,QAAgB,OAAiB,aAAsB;IACrE,IAAI,IAAI,WAAW;IACnB,IAAI,WAAW,KAAK,IAAI,UAAU,QAAQ,EAAE,YAAY,QAAQ,CAAC;SAC5D,IAAI,UAAU,MAAM;IACzB,IAAI,IAAI,YAAY,QAAQ,YAAY,QAAQ,KAAK,MAAM,WAAW,MAAM,uBAAuB,GAAG;GACxG;GACA,IAAI,WAAW;GACf,IAAI;IAEF,IAAI,OAAO,IAAI,WAAW,qBAAqB,YAAY;KACzD,KAAK,MAAM,6BAA6B,6CAA6C,KAAA,GAAW;MAAE;MAAS,QAAQ;KAAI,CAAC;KACxH,OAAO,GAAG;KACV;IACF;IACA,MAAM,YAAY,IAAI,WAAW,iBAAiB,GAAG;IACrD,IAAI,cAAc,KAAA,GAAW;KAC3B,KAAK,KAAK,0BAA0B,mDAAmD,KAAA,GAAW;MAAE;MAAS,QAAQ;KAAU,CAAC;KAChI,OAAO,SAAS;KAChB;IACF;IACA,MAAM,WAAW,IAAI,IAAI,IAAI,OAAO,KAAK,qBAAqB,CAAC,CAAC;IAChE,WAAW,SAAS,WAAW,GAAG,QAAQ,EAAE,IAAI,SAAS,MAAM,QAAQ,SAAS,CAAC,IAAI;IACrF,IAAI,IAAI,WAAW,UAAU,CAAC,WAAW,QAAQ,GAAG;KAClD,KAAK,MAAM,sBAAsB,sCAAsC;MAAE;MAAS,UAAU,YAAY;MAAM,QAAQ,IAAI,UAAU;MAAM,QAAQ;KAAI,CAAC;KACvJ,OAAO,GAAG;KACV;IACF;IACA,MAAM,cAAc,OAAO,IAAI,QAAQ,mBAAmB,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK;IACrF,KAAK,QAAQ,6BAA6B,YAAY,YAAY,IAAI,iBAAiB,oBAAoB;KACzG,KAAK,MAAM,6BAA6B,mCAAmC;MAAE;MAAS;MAAU,QAAQ;KAAI,CAAC;KAC7G,OAAO,GAAG;KAAG;IACf;IACA,MAAM,UAAU,QAAQ,aAAa,KAAK,QAAQ;IAClD,IAAI,SAAS;KACX,KAAK,MAAM,sBAAsB,+CAA+C;MAAE;MAAS;MAAU,QAAQ,QAAQ;KAAO,CAAC;KAC7H,OAAO,QAAQ,QAAQ,KAAA,GAAW,QAAQ,IAAI;KAC9C;IACF;IAEA,MAAM,aAAa,IAAI,gBAAgB;IACvC,IAAI,GAAG,eAAe;KAAE,IAAI,CAAC,IAAI,eAAe,WAAW,MAAM;IAAE,CAAC;IACpE,MAAM,SAAmB,CAAC;IAC1B,IAAI,OAAO;IACX,WAAW,MAAM,SAAS,KAAK;KAC7B,MAAM,SAAS,OAAO,SAAS,KAAK,IAAI,QAAQ,OAAO,KAAK,KAAK;KACjE,QAAQ,OAAO;KACf,IAAI,OAAO,WAAW;MACpB,KAAK,KAAK,sBAAsB,yCAAyC,KAAA,GAAW;OAAE;OAAS;OAAU,cAAc;MAAU,CAAC;MAClI,OAAO,GAAG;MAAG,IAAI,QAAQ;MAAG;KAC9B;KACA,OAAO,KAAK,MAAM;IACpB;IACA,IAAI;IACJ,IAAI;KAAE,OAAO,KAAK,MAAM,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,MAAM,MAAM,QAAQ,kBAAkB,SAAS,GAAG;IAAE,QACrG;KACJ,KAAK,MAAM,yBAAyB,4CAA4C;MAAE;MAAS;MAAU,QAAQ;KAAI,CAAC;KAClH,OAAO,GAAG;KACV;IACF;IACA,IAAI,KACF,MAAM,qBACJ;KAAE,YAAY;KAAS,aAAa;KAAU,aAAa,sBAAsB,KAAK;IAAE,SAClF,SAAS,UAAU,MAAM,WAAW,QAAQ,GAAG,CACvD;SAEA,MAAM,SAAS,UAAU,MAAM,WAAW,QAAQ,GAAG;GAEzD,SAAS,OAAO;IACd,KAAK,MAAM,uBAAuB,qCAAqC,OAAO;KAAE;KAAS,UAAU,YAAY;KAAM,QAAQ;IAAI,CAAC;IAClI,OAAO,KAAK,KAAK;GACnB;EACF;CAAE,CAAC;AACL;;AAGA,SAAgB,oBAAoB,UAA0B,OAAe,QAAiB,UAAU,MAAY;CAClH,IAAI,SAAS,WAAW;CACxB,SAAS,UAAU,KAAK;EAAE,gBAAgB;EAAoB,GAAI,UAAU,EAAE,iBAAiB,WAAW,IAAI,CAAC;CAAG,CAAC;CACnH,SAAS,IAAI,KAAK,UAAU;EAAE,MAAM;EAAmB;EAAO;CAAO,CAAC,CAAC;AACzE;;AAGA,SAAgB,gBACd,KACA,SACA,MACA,UAA0B,CAAC,GACf;CACZ,MAAM,MAAM,QAAQ;CACpB,OAAO,yBAAyB,KAAK,SAAS,OAAO,UAAU,MAAM,QAAQ,QAAQ;EACnF,IAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,GAAG;GAC5D,KAAK,MAAM,wBAAwB,4CAA4C;IAAE;IAAS;GAAS,CAAC;GACpG,IAAI,UAAU,GAAG;GAAG,IAAI,IAAI;GAAG;EACjC;EACA,MAAM,UAAU;EAChB,IAAI,QAAQ,SAAS,oBAAoB,QAAQ,WAAW,YAAY,OAAO,QAAQ,UAAU,YAC3F,QAAQ,kBAAkB,CAAC,OAAO,OAAO,SAAS,SAAS,GAAI;GACnE,KAAK,MAAM,wBAAwB,wCAAwC;IAAE;IAAS;GAAS,CAAC;GAChG,IAAI,UAAU,GAAG;GAAG,IAAI,IAAI;GAAG;EACjC;EACA,MAAM,UAAU,KAAK,IAAI;EACzB,MAAM,MAAM,MAAM,KAAK,UAAU,QAAQ,SAAS,MAAM;EACxD,MAAM,aAAa,KAAK,IAAI,IAAI;EAChC,IAAI,IAAI,IACN,KAAK,MAAM,sBAAsB,4BAA4B;GAAE;GAAS;GAAU;EAAW,CAAC;OAE9F,KAAK,KAAK,kBAAkB,gDAAgD,KAAA,GAAW;GACrF;GAAS;GAAU,MAAM,IAAI,OAAO,QAAQ;GAAM;EACpD,CAAC;EAEH,MAAM,SAAS,IAAI,KAAK,MAAM;GAAE,GAAG;GAAK,OAAO;IAAE,SAAS,CAAC;IAAG,GAAG,IAAI;GAAM;EAAE;EAC7E,oBAAoB,KAAK,QAAQ,OAAO,MAAM;CAChD,GAAG,OAAO;AACZ"}
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. */
@@ -21,7 +22,8 @@ interface LlmTextRequest {
21
22
  route: FeatureModelRoute;
22
23
  system: string;
23
24
  text: string;
24
- maxTokens: number;
25
+ /** Omit to use the selected model's normal output settings. */
26
+ maxTokens?: number;
25
27
  signal: AbortSignal;
26
28
  /** The real session id for host attribution; never a feature-scoped alias. */
27
29
  sessionId?: string;
@@ -31,6 +33,8 @@ interface LlmTextRequest {
31
33
  configure?: Parameters<LlmRuntime['prepareCall']>[2];
32
34
  /** Rechecked before the call returns text. A false result discards the output. */
33
35
  isCurrent?: () => boolean;
36
+ /** Optional diagnostic logger for timing, token usage, and failure telemetry. */
37
+ logger?: PluginLogger;
34
38
  }
35
39
  interface LlmTextResult {
36
40
  text: string;
package/lib/llm-call.js CHANGED
@@ -72,56 +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 effort = request.route.reasoningEffort?.trim();
76
- const prepared = await llm.prepareCall({
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
- ...effort ? { reasoningEffort: ReasoningEffortId(effort) } : {},
80
- maxTokens: request.maxTokens
81
- }, request.signal, request.configure);
82
- if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
83
- const options = {
84
- provider: prepared.config.provider,
85
- model: prepared.config.model,
86
- ...prepared.config.reasoningEffort ? { reasoningEffort: prepared.config.reasoningEffort } : {},
87
- ...prepared.config.maxTokens !== void 0 ? { maxTokens: Math.min(request.maxTokens, prepared.config.maxTokens) } : { maxTokens: request.maxTokens },
88
- ...prepared.config.temperature !== void 0 ? { temperature: prepared.config.temperature } : {},
89
- ...prepared.config.stop ? { stop: [...prepared.config.stop] } : {},
90
- system: request.system,
91
- messages: [createUserMessage({
92
- source: producerMessageSource(request.plugin),
93
- content: [{
94
- type: "text",
95
- text: buildAuxiliaryUserText(request)
96
- }]
97
- })],
98
- signal: request.signal,
99
- ...request.purpose ? { purpose: request.purpose } : {},
100
- ...request.sessionId ? { sessionId: request.sessionId } : {}
81
+ purpose: request.purpose ?? null,
82
+ sessionId: request.sessionId ?? null
101
83
  };
102
- const assembler = await readStream(prepared.stream(options), request.signal, live);
103
- if (request.signal.aborted || assembler.finish.kind === "aborted") throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
104
- if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
105
- if (assembler.finish.kind === "error") {
106
- const code = assembler.finish.failure.code;
107
- throw Object.assign(new Error(assembler.finish.failure.message || "模型调用失败。"), { code });
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;
108
157
  }
109
- if (assembler.blocks().some((block) => block.type === "tool-call") || assembler.finish.kind === "tool-calls") throw new Error("这次调用不使用工具。");
110
- if (assembler.finish.kind !== "stop") throw new Error("模型调用未完成。");
111
- const usage = assembler.usage;
112
- return {
113
- text: joinText(assembler),
114
- provider: prepared.config.provider,
115
- model: prepared.config.model,
116
- ...usage ? {
117
- inputTokens: usage.inputTokens,
118
- outputTokens: usage.outputTokens,
119
- ...usage.totalTokens !== void 0 ? { totalTokens: usage.totalTokens } : {},
120
- ...usage.cacheReadTokens !== void 0 ? { cacheReadTokens: usage.cacheReadTokens } : {},
121
- ...usage.cacheWriteTokens !== void 0 ? { cacheWriteTokens: usage.cacheWriteTokens } : {},
122
- ...usage.reasoningTokens !== void 0 ? { reasoningTokens: usage.reasoningTokens } : {}
123
- } : {}
124
- };
125
158
  }
126
159
  //#endregion
127
160
  export { callLlmText, resolveFeatureModel };
@@ -1 +1 @@
1
- {"version":3,"file":"llm-call.js","names":[],"sources":["../src/llm-call.ts"],"sourcesContent":["import {\n BlockAssembler, ReasoningEffortId, createUserMessage,\n type GenerateOptions, type LlmRuntime, type StreamChunk,\n} from '@deepseek-ai/dsh-llm'\nimport { getAuxiliaryModel, producerMessageSource, type ModelRoute } from './contracts.ts'\nimport { buildAuxiliaryUserText } from './auxiliary-context.ts'\n\n/** A feature's own saved model. Empty means the host default chat model. */\nexport interface FeatureModelRoute {\n readonly provider: string\n readonly model: string\n readonly reasoningEffort?: string\n}\n\nexport type LlmTextCaller = Pick<LlmRuntime, 'prepareCall' | 'resolveCallConfig'>\n\nexport interface LlmTextRequest {\n plugin: string\n /** Optional feature id, stamped on the context envelope for attribution. */\n feature?: string\n /**\n * Stable material rebuilt from current facts (session events, durable\n * records). Packed before the live text so provider-side caching reuses\n * the prefix; never model outputs or approval history. Strings or\n * pre-ordered segments.\n */\n context?: string | readonly string[]\n route: FeatureModelRoute\n system: string\n text: string\n maxTokens: number\n signal: AbortSignal\n /** The real session id for host attribution; never a feature-scoped alias. */\n sessionId?: string\n /** Auxiliary-call marker adapters may special-case (e.g. disable thinking for titles). */\n purpose?: GenerateOptions['purpose']\n /** Pure control selection using the exact model captured for this call. */\n configure?: Parameters<LlmRuntime['prepareCall']>[2]\n /** Rechecked before the call returns text. A false result discards the output. */\n isCurrent?: () => boolean\n}\n\nexport interface LlmTextResult {\n text: string\n provider: string\n model: string\n inputTokens?: number\n outputTokens?: number\n totalTokens?: number\n cacheReadTokens?: number\n cacheWriteTokens?: number\n reasoningTokens?: number\n}\n\ntype SessionModelsHost = {\n agents?: { get(sessionId: string): { session?: { requestHeader?: () => { config?: ModelRoute; adapterDefaults?: { reasoningEffort?: unknown } } | undefined } } | undefined }\n sessionProjections?: { stateOf(session: unknown, key: string): { pending?: ModelRoute | null } | undefined }\n agentDefaultModel?: { currentSelection(): ModelRoute | undefined }\n}\n\nfunction routeOf(value: { provider?: unknown; model?: unknown; reasoningEffort?: unknown } | undefined): FeatureModelRoute | undefined {\n if (typeof value?.provider !== 'string' || !value.provider.trim()) return undefined\n if (typeof value.model !== 'string' || !value.model.trim()) return undefined\n const effort = typeof value.reasoningEffort === 'string' ? value.reasoningEffort.trim() : ''\n return effort\n ? { provider: value.provider.trim(), model: value.model.trim(), reasoningEffort: effort }\n : { provider: value.provider.trim(), model: value.model.trim() }\n}\n\n/**\n * Plugin selection first; then the shared auxiliary default (when any\n * participant retains it); otherwise the live session model, then the host\n * default. Cordis callers must declare agents, sessionProjections and\n * agentDefaultModel in their inject list so the fallback reads use the\n * caller's live services.\n */\nexport function resolveFeatureModel(host: unknown, saved: FeatureModelRoute | undefined, sessionId?: string): FeatureModelRoute | undefined {\n const explicit = routeOf(saved)\n if (explicit) return explicit\n const auxiliary = getAuxiliaryModel(host)\n if (auxiliary) return auxiliary\n const face = host as SessionModelsHost | undefined\n if (sessionId && face?.agents && face.sessionProjections) {\n const session = face.agents.get(sessionId)?.session\n if (session) {\n try {\n const pending = face.sessionProjections.stateOf(session, 'modelSelection')?.pending\n const selected = routeOf(pending ?? undefined)\n if (selected) return selected\n } catch { /* fall through to the recorded route */ }\n const header = session.requestHeader?.()\n const recorded = header?.config\n if (recorded) {\n const selected = header?.adapterDefaults?.reasoningEffort === true\n ? { provider: recorded.provider, model: recorded.model }\n : recorded\n const route = routeOf(selected)\n if (route) return route\n }\n }\n }\n return routeOf(face?.agentDefaultModel?.currentSelection())\n}\n\nfunction joinText(assembler: BlockAssembler): string {\n return assembler.blocks()\n .filter((block): block is { type: 'text'; text: string } => block.type === 'text')\n .map(block => block.text)\n .join('')\n}\n\nasync function readStream(stream: AsyncIterable<StreamChunk>, signal: AbortSignal, live: () => boolean): Promise<BlockAssembler> {\n const assembler = new BlockAssembler()\n const iterator = stream[Symbol.asyncIterator]()\n try {\n while (live()) {\n if (signal.aborted) break\n const item = await iterator.next()\n if (item.done) break\n assembler.push(item.value)\n }\n } finally {\n // Settlement includes producer cleanup; never leave a detached return()\n // running after the caller has released its lifecycle ownership.\n await iterator.return?.()\n }\n return assembler\n}\n\n/** One native model call. No retry, timeout policy, or provider queue. */\nexport async function callLlmText(llm: LlmTextCaller, request: LlmTextRequest): Promise<LlmTextResult> {\n request.signal.throwIfAborted()\n const live = () => request.isCurrent?.() !== false && !request.signal.aborted\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n const effort = request.route.reasoningEffort?.trim()\n const prepared = await llm.prepareCall({\n provider: request.route.provider,\n model: request.route.model,\n ...(effort ? { reasoningEffort: ReasoningEffortId(effort) } : {}),\n maxTokens: request.maxTokens,\n }, request.signal, request.configure)\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n const options: GenerateOptions = {\n provider: prepared.config.provider,\n model: prepared.config.model,\n ...(prepared.config.reasoningEffort ? { reasoningEffort: prepared.config.reasoningEffort } : {}),\n ...(prepared.config.maxTokens !== undefined ? { maxTokens: Math.min(request.maxTokens, prepared.config.maxTokens) } : { maxTokens: request.maxTokens }),\n ...(prepared.config.temperature !== undefined ? { temperature: prepared.config.temperature } : {}),\n ...(prepared.config.stop ? { stop: [...prepared.config.stop] } : {}),\n system: request.system,\n messages: [createUserMessage({\n source: producerMessageSource(request.plugin),\n content: [{ type: 'text', text: buildAuxiliaryUserText(request) }],\n })],\n signal: request.signal,\n ...(request.purpose ? { purpose: request.purpose } : {}),\n ...(request.sessionId ? { sessionId: request.sessionId as GenerateOptions['sessionId'] } : {}),\n }\n const assembler = await readStream(prepared.stream(options), request.signal, live)\n if (request.signal.aborted || assembler.finish.kind === 'aborted') {\n throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n }\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n if (assembler.finish.kind === 'error') {\n const code = assembler.finish.failure.code\n throw Object.assign(new Error(assembler.finish.failure.message || '模型调用失败。'), { code })\n }\n if (assembler.blocks().some(block => block.type === 'tool-call') || assembler.finish.kind === 'tool-calls') {\n throw new Error('这次调用不使用工具。')\n }\n if (assembler.finish.kind !== 'stop') throw new Error('模型调用未完成。')\n // Every usable host usage field passes through; absent fields stay absent\n // (the caller's accounting must not see invented zeros).\n const usage = assembler.usage\n return {\n text: joinText(assembler),\n provider: prepared.config.provider,\n model: prepared.config.model,\n ...(usage ? {\n inputTokens: usage.inputTokens,\n outputTokens: usage.outputTokens,\n ...(usage.totalTokens !== undefined ? { totalTokens: usage.totalTokens } : {}),\n ...(usage.cacheReadTokens !== undefined ? { cacheReadTokens: usage.cacheReadTokens } : {}),\n ...(usage.cacheWriteTokens !== undefined ? { cacheWriteTokens: usage.cacheWriteTokens } : {}),\n ...(usage.reasoningTokens !== undefined ? { reasoningTokens: usage.reasoningTokens } : {}),\n } : {}),\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"llm-call.js","names":[],"sources":["../src/llm-call.ts"],"sourcesContent":["import {\n BlockAssembler, ReasoningEffortId, createUserMessage,\n type GenerateOptions, type LlmRuntime, type StreamChunk,\n} from '@deepseek-ai/dsh-llm'\nimport { getAuxiliaryModel, producerMessageSource, type ModelRoute } from './contracts.ts'\nimport { buildAuxiliaryUserText } from './auxiliary-context.ts'\nimport type { PluginLogger } from './log-shared.ts'\n\n/** A feature's own saved model. Empty means the host default chat model. */\nexport interface FeatureModelRoute {\n readonly provider: string\n readonly model: string\n readonly reasoningEffort?: string\n}\n\nexport type LlmTextCaller = Pick<LlmRuntime, 'prepareCall' | 'resolveCallConfig'>\n\nexport interface LlmTextRequest {\n plugin: string\n /** Optional feature id, stamped on the context envelope for attribution. */\n feature?: string\n /**\n * Stable material rebuilt from current facts (session events, durable\n * records). Packed before the live text so provider-side caching reuses\n * the prefix; never model outputs or approval history. Strings or\n * pre-ordered segments.\n */\n context?: string | readonly string[]\n route: FeatureModelRoute\n system: string\n text: string\n /** Omit to use the selected model's normal output settings. */\n maxTokens?: number\n signal: AbortSignal\n /** The real session id for host attribution; never a feature-scoped alias. */\n sessionId?: string\n /** Auxiliary-call marker adapters may special-case (e.g. disable thinking for titles). */\n purpose?: GenerateOptions['purpose']\n /** Pure control selection using the exact model captured for this call. */\n configure?: Parameters<LlmRuntime['prepareCall']>[2]\n /** Rechecked before the call returns text. A false result discards the output. */\n isCurrent?: () => boolean\n /** Optional diagnostic logger for timing, token usage, and failure telemetry. */\n logger?: PluginLogger\n}\n\nexport interface LlmTextResult {\n text: string\n provider: string\n model: string\n inputTokens?: number\n outputTokens?: number\n totalTokens?: number\n cacheReadTokens?: number\n cacheWriteTokens?: number\n reasoningTokens?: number\n}\n\ntype SessionModelsHost = {\n agents?: { get(sessionId: string): { session?: { requestHeader?: () => { config?: ModelRoute; adapterDefaults?: { reasoningEffort?: unknown } } | undefined } } | undefined }\n sessionProjections?: { stateOf(session: unknown, key: string): { pending?: ModelRoute | null } | undefined }\n agentDefaultModel?: { currentSelection(): ModelRoute | undefined }\n}\n\nfunction routeOf(value: { provider?: unknown; model?: unknown; reasoningEffort?: unknown } | undefined): FeatureModelRoute | undefined {\n if (typeof value?.provider !== 'string' || !value.provider.trim()) return undefined\n if (typeof value.model !== 'string' || !value.model.trim()) return undefined\n const effort = typeof value.reasoningEffort === 'string' ? value.reasoningEffort.trim() : ''\n return effort\n ? { provider: value.provider.trim(), model: value.model.trim(), reasoningEffort: effort }\n : { provider: value.provider.trim(), model: value.model.trim() }\n}\n\n/**\n * Plugin selection first; then the shared auxiliary default (when any\n * participant retains it); otherwise the live session model, then the host\n * default. Cordis callers must declare agents, sessionProjections and\n * agentDefaultModel in their inject list so the fallback reads use the\n * caller's live services.\n */\nexport function resolveFeatureModel(host: unknown, saved: FeatureModelRoute | undefined, sessionId?: string): FeatureModelRoute | undefined {\n const explicit = routeOf(saved)\n if (explicit) return explicit\n const auxiliary = getAuxiliaryModel(host)\n if (auxiliary) return auxiliary\n const face = host as SessionModelsHost | undefined\n if (sessionId && face?.agents && face.sessionProjections) {\n const session = face.agents.get(sessionId)?.session\n if (session) {\n try {\n const pending = face.sessionProjections.stateOf(session, 'modelSelection')?.pending\n const selected = routeOf(pending ?? undefined)\n if (selected) return selected\n } catch { /* fall through to the recorded route */ }\n const header = session.requestHeader?.()\n const recorded = header?.config\n if (recorded) {\n const selected = header?.adapterDefaults?.reasoningEffort === true\n ? { provider: recorded.provider, model: recorded.model }\n : recorded\n const route = routeOf(selected)\n if (route) return route\n }\n }\n }\n return routeOf(face?.agentDefaultModel?.currentSelection())\n}\n\nfunction joinText(assembler: BlockAssembler): string {\n return assembler.blocks()\n .filter((block): block is { type: 'text'; text: string } => block.type === 'text')\n .map(block => block.text)\n .join('')\n}\n\nasync function readStream(stream: AsyncIterable<StreamChunk>, signal: AbortSignal, live: () => boolean): Promise<BlockAssembler> {\n const assembler = new BlockAssembler()\n const iterator = stream[Symbol.asyncIterator]()\n try {\n while (live()) {\n if (signal.aborted) break\n const item = await iterator.next()\n if (item.done) break\n assembler.push(item.value)\n }\n } finally {\n // Settlement includes producer cleanup; never leave a detached return()\n // running after the caller has released its lifecycle ownership.\n await iterator.return?.()\n }\n return assembler\n}\n\n/** One native model call. No retry, timeout policy, or provider queue. */\nexport async function callLlmText(llm: LlmTextCaller, request: LlmTextRequest): Promise<LlmTextResult> {\n request.signal.throwIfAborted()\n const live = () => request.isCurrent?.() !== false && !request.signal.aborted\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n const started = Date.now()\n const logBase = {\n plugin: request.plugin,\n feature: request.feature ?? null,\n provider: request.route.provider,\n model: request.route.model,\n purpose: request.purpose ?? null,\n sessionId: request.sessionId ?? null,\n }\n request.logger?.debug('llm.call.started', 'Auxiliary model call started.', logBase)\n try {\n const effort = request.route.reasoningEffort?.trim()\n const prepared = await llm.prepareCall({\n provider: request.route.provider,\n model: request.route.model,\n ...(effort ? { reasoningEffort: ReasoningEffortId(effort) } : {}),\n ...(request.maxTokens !== undefined ? { maxTokens: request.maxTokens } : {}),\n }, request.signal, request.configure)\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n const maxTokens = request.maxTokens === undefined\n ? prepared.config.maxTokens\n : Math.min(request.maxTokens, prepared.config.maxTokens ?? request.maxTokens)\n const options: GenerateOptions = {\n provider: prepared.config.provider,\n model: prepared.config.model,\n ...(prepared.config.reasoningEffort ? { reasoningEffort: prepared.config.reasoningEffort } : {}),\n ...(maxTokens !== undefined ? { maxTokens } : {}),\n ...(prepared.config.temperature !== undefined ? { temperature: prepared.config.temperature } : {}),\n ...(prepared.config.stop ? { stop: [...prepared.config.stop] } : {}),\n system: request.system,\n messages: [createUserMessage({\n source: producerMessageSource(request.plugin),\n content: [{ type: 'text', text: buildAuxiliaryUserText(request) }],\n })],\n signal: request.signal,\n ...(request.purpose ? { purpose: request.purpose } : {}),\n ...(request.sessionId ? { sessionId: request.sessionId as GenerateOptions['sessionId'] } : {}),\n }\n const assembler = await readStream(prepared.stream(options), request.signal, live)\n if (request.signal.aborted || assembler.finish.kind === 'aborted') {\n throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n }\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n if (assembler.finish.kind === 'error') {\n const code = assembler.finish.failure.code\n throw Object.assign(new Error(assembler.finish.failure.message || '模型调用失败。'), { code })\n }\n if (assembler.blocks().some(block => block.type === 'tool-call') || assembler.finish.kind === 'tool-calls') {\n throw new Error('这次调用不使用工具。')\n }\n if (assembler.finish.kind !== 'stop') throw new Error('模型调用未完成。')\n // Every usable host usage field passes through; absent fields stay absent\n // (the caller's accounting must not see invented zeros).\n const usage = assembler.usage\n request.logger?.debug('llm.call.completed', 'Auxiliary model call completed.', {\n ...logBase,\n provider: prepared.config.provider,\n model: prepared.config.model,\n durationMs: Date.now() - started,\n ...(usage?.inputTokens !== undefined ? { inputTokens: usage.inputTokens } : {}),\n ...(usage?.outputTokens !== undefined ? { outputTokens: usage.outputTokens } : {}),\n ...(usage?.totalTokens !== undefined ? { totalTokens: usage.totalTokens } : {}),\n })\n return {\n text: joinText(assembler),\n provider: prepared.config.provider,\n model: prepared.config.model,\n ...(usage ? {\n inputTokens: usage.inputTokens,\n outputTokens: usage.outputTokens,\n ...(usage.totalTokens !== undefined ? { totalTokens: usage.totalTokens } : {}),\n ...(usage.cacheReadTokens !== undefined ? { cacheReadTokens: usage.cacheReadTokens } : {}),\n ...(usage.cacheWriteTokens !== undefined ? { cacheWriteTokens: usage.cacheWriteTokens } : {}),\n ...(usage.reasoningTokens !== undefined ? { reasoningTokens: usage.reasoningTokens } : {}),\n } : {}),\n }\n } catch (error) {\n const durationMs = Date.now() - started\n const aborted = request.signal.aborted || (error instanceof Error && error.name === 'AbortError')\n if (aborted) {\n request.logger?.debug('llm.call.cancelled', 'Auxiliary model call cancelled.', { ...logBase, durationMs })\n } else {\n request.logger?.warn('llm.call.failed', 'Auxiliary model call failed.', error, { ...logBase, durationMs })\n }\n throw error\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
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
+ export { 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, createChildPluginLogger, isPluginLogLevel, mergePluginLogContext, normalizePluginLogEvent, normalizePluginLogSource, parsePluginLogLevel, pluginLogLevelEnabled, sanitizePluginLogContext, sanitizePluginLogError, sanitizePluginLogText, truncateLogText };
@@ -0,0 +1,169 @@
1
+ //#region src/log-shared.ts
2
+ const PLUGIN_LOG_LEVELS = [
3
+ "debug",
4
+ "info",
5
+ "warn",
6
+ "error",
7
+ "fault"
8
+ ];
9
+ const DEFAULT_PLUGIN_LOG_LEVEL = "info";
10
+ const PLUGIN_LOG_MAX_MESSAGE_CHARS = 16e3;
11
+ const PLUGIN_LOG_MAX_CONTEXT_CHARS = 8e3;
12
+ const SOURCE_RE = /^[a-z][a-z0-9-]{0,63}$/;
13
+ const EVENT_RE = /^[a-z][a-z0-9._-]{0,127}$/;
14
+ const CONTEXT_KEY_RE = /^[a-zA-Z][a-zA-Z0-9._-]{0,63}$/;
15
+ const SECRET_KEY_RE = /(?:api[_-]?key|access[_-]?token|refresh[_-]?token|bearer|authorization|cookie|set[_-]?cookie|nonce|credential|private[_-]?key|password|secret|app[_-]?secret|client[_-]?secret|z_c0|d_c0)$/i;
16
+ const CHAT_PAYLOAD_RE = /"role"\s*:\s*"(?:system|user|assistant|tool)"|"(?:messages|content|prompt|input_text|input_image|image_url)"\s*:\s*[\[{"']/i;
17
+ const CREDENTIAL_BODY_RE = /-----BEGIN (?:RSA |EC |OPENSSH |PRIVATE )?PRIVATE KEY-----|"type"\s*:\s*"service_account"|credentials\.json|\.dsh[/\\](?:credentials|auth)/i;
18
+ const AUTH_HEADER_RE = /\b(Authorization|Proxy-Authorization|Cookie|Set-Cookie)\s*:\s*[^\r\n]*/gi;
19
+ const BEARER_RE = /\bBearer\s+[A-Za-z0-9._\-+=/]{8,}/g;
20
+ const SECRET_FIELD_RE = /(["']?(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|bearer|authorization|cookie|set-cookie|nonce|credential|private[_-]?key|password|secret|app[_-]?secret|client[_-]?secret|password|z_c0|d_c0)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,&;}]+)/gi;
21
+ const SECRET_QUERY_RE = /([?&](?:token|api[_-]?key|key|secret|password|bootstrap|code|sig|signature|credential|password)=)[^\s&#"']+/gi;
22
+ const URL_CREDENTIAL_RE = /\b(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi;
23
+ const BOOTSTRAP_RE = /\/bootstrap\/[^\s"'?&#]+/gi;
24
+ const KNOWN_TOKEN_RE = /\b(?:sk-[A-Za-z0-9_-]{10,}|gh[pousr]_[A-Za-z0-9_]{16,}|github_pat_[A-Za-z0-9_]{20,}|tvly-[A-Za-z0-9_-]{10,}|AKIA[0-9A-Z]{16})\b/g;
25
+ const PATH_RE = /(?:[A-Za-z]:\\|\\\\)[^\s"'`<>|]+|(?:\/(?:home|Users|tmp|var|root)\/|~\/)[^\s"'`<>|]+/g;
26
+ function isPluginLogLevel(value) {
27
+ return typeof value === "string" && PLUGIN_LOG_LEVELS.includes(value);
28
+ }
29
+ function pluginLogLevelEnabled(level, minimum) {
30
+ return PLUGIN_LOG_LEVELS.indexOf(level) >= PLUGIN_LOG_LEVELS.indexOf(minimum);
31
+ }
32
+ function normalizePluginLogSource(raw) {
33
+ const stripped = String(raw || "").trim().replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
34
+ const bounded = (/^[a-z]/.test(stripped) ? stripped : `p-${stripped}`).slice(0, 64).replace(/-+$/g, "");
35
+ return SOURCE_RE.test(bounded) ? bounded : "dsh-plugin";
36
+ }
37
+ function normalizePluginLogEvent(raw) {
38
+ const cleaned = String(raw || "").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z]+/, "").slice(0, 128);
39
+ return EVENT_RE.test(cleaned) ? cleaned : "plugin.event";
40
+ }
41
+ /**
42
+ * Parse a level specification like `debug` or `dsh-zhihu=debug,dsh-dot:warn,info`.
43
+ * Source-specific rules win over a bare fallback level.
44
+ */
45
+ function parsePluginLogLevel(spec, source, fallback = DEFAULT_PLUGIN_LOG_LEVEL) {
46
+ if (!spec || typeof spec !== "string") return fallback;
47
+ const normalizedSource = source ? normalizePluginLogSource(source) : void 0;
48
+ let wildcard;
49
+ for (const token of spec.split(",").map((part) => part.trim()).filter(Boolean)) {
50
+ const lowered = token.toLowerCase();
51
+ if (isPluginLogLevel(lowered)) {
52
+ wildcard = lowered;
53
+ continue;
54
+ }
55
+ const match = /^([^=:]+)[=:]([a-z]+)$/i.exec(token);
56
+ if (!match) continue;
57
+ const target = match[1].trim();
58
+ const candidate = match[2].trim().toLowerCase();
59
+ if (!isPluginLogLevel(candidate)) continue;
60
+ if (target === "*") {
61
+ wildcard = candidate;
62
+ continue;
63
+ }
64
+ if (normalizedSource && normalizePluginLogSource(target) === normalizedSource) return candidate;
65
+ }
66
+ return wildcard ?? fallback;
67
+ }
68
+ function sanitizePluginLogText(text, maxChars = PLUGIN_LOG_MAX_MESSAGE_CHARS) {
69
+ if (!text) return "";
70
+ if (CREDENTIAL_BODY_RE.test(text)) return "[omitted credential material]";
71
+ if (CHAT_PAYLOAD_RE.test(text)) return "[omitted chat payload]";
72
+ return truncateLogText(text.replace(AUTH_HEADER_RE, "$1: [redacted]").replace(BEARER_RE, "Bearer [redacted]").replace(SECRET_FIELD_RE, (full, prefix) => {
73
+ const value = full.slice(prefix.length);
74
+ if (value.startsWith("\"")) return `${prefix}"[redacted]"`;
75
+ if (value.startsWith("'")) return `${prefix}'[redacted]'`;
76
+ return `${prefix}[redacted]`;
77
+ }).replace(SECRET_QUERY_RE, "$1[redacted]").replace(URL_CREDENTIAL_RE, "$1[redacted]@").replace(BOOTSTRAP_RE, "/bootstrap/[redacted]").replace(KNOWN_TOKEN_RE, "[redacted]").replace(PATH_RE, "[path]"), maxChars);
78
+ }
79
+ function sanitizePluginLogContext(input) {
80
+ if (!input) return void 0;
81
+ const output = {};
82
+ for (const [key, value] of Object.entries(input).slice(0, 32)) {
83
+ if (value === void 0 || !CONTEXT_KEY_RE.test(key)) continue;
84
+ if (typeof value === "number" && !Number.isFinite(value)) continue;
85
+ if (SECRET_KEY_RE.test(key)) {
86
+ output[key] = "[redacted]";
87
+ continue;
88
+ }
89
+ output[key] = typeof value === "string" ? sanitizePluginLogText(value, 1e3) : value;
90
+ }
91
+ if (!Object.keys(output).length) return void 0;
92
+ if (JSON.stringify(output).length <= 8e3) return output;
93
+ return {
94
+ omitted: true,
95
+ reason: "Diagnostic context exceeded its size limit."
96
+ };
97
+ }
98
+ function sanitizePluginLogError(error) {
99
+ if (error === void 0) return void 0;
100
+ const row = error && typeof error === "object" ? error : {};
101
+ const rawMessage = typeof row.message === "string" ? row.message : String(error);
102
+ const result = {
103
+ name: typeof row.name === "string" && row.name.trim() ? truncateLogText(row.name.trim(), 100) : "Error",
104
+ message: sanitizePluginLogText(rawMessage) || "Unknown error"
105
+ };
106
+ if (typeof row.stack === "string" && row.stack.trim()) result.stack = sanitizePluginLogText(row.stack, 24e3);
107
+ if (typeof row.code === "string" && /^[A-Za-z0-9_./:-]{1,100}$/.test(row.code)) result.code = row.code;
108
+ else if (typeof row.code === "number" && Number.isFinite(row.code)) result.code = String(row.code);
109
+ return result;
110
+ }
111
+ function mergePluginLogContext(base, extra) {
112
+ if (!base) return extra;
113
+ if (!extra) return base;
114
+ return {
115
+ ...base,
116
+ ...extra
117
+ };
118
+ }
119
+ function buildPluginLogEntry(input, meta) {
120
+ const entry = {
121
+ version: 1,
122
+ at: meta.at.toISOString(),
123
+ level: isPluginLogLevel(input.level) ? input.level : "info",
124
+ source: normalizePluginLogSource(meta.source),
125
+ surface: meta.surface,
126
+ ...meta.profile ? { profile: meta.profile } : {},
127
+ event: normalizePluginLogEvent(input.event),
128
+ message: sanitizePluginLogText(input.message || "No diagnostic message was provided.", PLUGIN_LOG_MAX_MESSAGE_CHARS),
129
+ ...meta.pid !== void 0 ? { pid: meta.pid } : {}
130
+ };
131
+ const context = sanitizePluginLogContext(mergePluginLogContext(meta.correlation, input.context));
132
+ if (context) entry.context = context;
133
+ const error = sanitizePluginLogError(input.error);
134
+ if (error) entry.error = error;
135
+ return entry;
136
+ }
137
+ function createChildPluginLogger(parent, bound) {
138
+ return {
139
+ get source() {
140
+ return parent.source;
141
+ },
142
+ get file() {
143
+ return parent.file;
144
+ },
145
+ get lastWriteError() {
146
+ return parent.lastWriteError;
147
+ },
148
+ debug: (event, message, context) => parent.debug(event, message, mergePluginLogContext(bound, context)),
149
+ info: (event, message, context) => parent.info(event, message, mergePluginLogContext(bound, context)),
150
+ warn: (event, message, error, context) => parent.warn(event, message, error, mergePluginLogContext(bound, context)),
151
+ error: (event, message, error, context) => parent.error(event, message, error, mergePluginLogContext(bound, context)),
152
+ fault: (event, message, error, context) => parent.fault(event, message, error, mergePluginLogContext(bound, context)),
153
+ write: (input) => parent.write({
154
+ ...input,
155
+ context: mergePluginLogContext(bound, input.context)
156
+ }),
157
+ child: (next) => createChildPluginLogger(parent, mergePluginLogContext(bound, next) ?? {})
158
+ };
159
+ }
160
+ function truncateLogText(text, maxChars) {
161
+ if (text.length <= maxChars) return text;
162
+ const marker = "[truncated]\n";
163
+ if (maxChars <= 12) return text.slice(0, Math.max(0, maxChars));
164
+ return `${marker}${text.slice(-(maxChars - 12))}`;
165
+ }
166
+ //#endregion
167
+ export { 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 };
168
+
169
+ //# sourceMappingURL=log-shared.js.map