dsh-plugin-tool-management 0.9.0 → 0.9.1

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.
@@ -158,20 +158,28 @@ function textOf(message) {
158
158
  const block = content[0];
159
159
  return block && block.type === 'text' && typeof block.text === 'string' ? block.text : undefined;
160
160
  }
161
+ /** 本插件自己的五个注入 kind(去重比对只用这一份 —— 绝不能让官方正文影响"发不发"的判断)。 */
162
+ const PLUGIN_KINDS = new Map(INJECT_DOMAIN_KEYS.map((key) => [INJECT_KIND_OF[key], { key, official: false }]));
161
163
  /**
162
- * 会话**可见表面上**每个域最新一条己方注入的正文。
163
- *
164
- * 可见表面 = `session.surface.nodes`(dsh-session 的公开面,官方 skill-catalog 与
165
- * agent-instructions 都直接扫它):一条注入若已被压缩移出表面,说明它不在模型上下文里
166
- * → 该域不在 map 里 → 该重发。表面接口整个缺失时退回扫事件流(按"仍可见"处理,
167
- * 宁可与现状一致,也不制造每步重发)。
164
+ * 官方载体消息的 kind → 本插件域:技能目录(`@deepseek-ai/dsh-tool-skill`)与
165
+ * AGENTS.md(`@deepseek-ai/dsh-agent-instructions`)。仅用于「注入实况」展示 ——
166
+ * 标准类预设下这两个域由官方在送,它同样是"模型看到的内容",体积要算官方那份。
167
+ * 字符串取自官方包,是宿主侧的稳定契约。
168
168
  */
169
- function newestDomainTexts(agent) {
169
+ const OFFICIAL_KIND_OF = {
170
+ 'skill-catalog': 'skills',
171
+ 'agent-instructions': 'prompt',
172
+ };
173
+ /** 实况展示用的 kind 表:本插件的五条 + 官方两条。 */
174
+ const LIVE_KINDS = new Map([
175
+ ...PLUGIN_KINDS,
176
+ ...Object.entries(OFFICIAL_KIND_OF).map(([kind, key]) => [kind, { key, official: true }]),
177
+ ]);
178
+ function newestDomainTexts(agent, kinds) {
170
179
  const out = new Map();
171
180
  const session = agent?.session;
172
181
  if (!session || typeof session.seq !== 'number' || typeof session.eventAt !== 'function')
173
182
  return out;
174
- const keyOfKind = new Map(INJECT_DOMAIN_KEYS.map((key) => [INJECT_KIND_OF[key], key]));
175
183
  const scan = (seq) => {
176
184
  let event;
177
185
  try {
@@ -183,13 +191,13 @@ function newestDomainTexts(agent) {
183
191
  if (!event || event.type !== 'user/message')
184
192
  return false;
185
193
  const source = event.data && event.data.source;
186
- const key = source && typeof source.kind === 'string' ? keyOfKind.get(source.kind) : undefined;
187
- if (key === undefined || out.has(key))
194
+ const mapping = source && typeof source.kind === 'string' ? kinds.get(source.kind) : undefined;
195
+ if (mapping === undefined || out.has(mapping.key))
188
196
  return false;
189
197
  const text = textOf(event.data);
190
198
  if (text === undefined)
191
199
  return false;
192
- out.set(key, text);
200
+ out.set(mapping.key, { text, form: source && typeof source.form === 'string' ? source.form : '', official: mapping.official });
193
201
  return out.size === INJECT_DOMAIN_KEYS.length;
194
202
  };
195
203
  const nodes = session.surface && Array.isArray(session.surface.nodes) ? session.surface.nodes : undefined;
@@ -211,6 +219,14 @@ function newestDomainTexts(agent) {
211
219
  break;
212
220
  return out;
213
221
  }
222
+ const bytesOf = (text) => {
223
+ try {
224
+ return Buffer.byteLength(text, 'utf8');
225
+ }
226
+ catch {
227
+ return text.length;
228
+ }
229
+ };
214
230
  /**
215
231
  * 注册 `agent/pre-step` 注入监听;返回清理函数。
216
232
  *
@@ -224,9 +240,49 @@ export function createContextInjector(deps) {
224
240
  }
225
241
  catch { /* ignore */ }
226
242
  };
243
+ // 最近活跃的会话(WeakRef:诊断用,不阻止会话被回收)。每次 pre-step 都刷新 ——
244
+ // 包括"空 turn 提前返回"和"这一步没有内容可发"的分支,页面才能如实说"没投过"。
245
+ let lastAgent = null;
246
+ const delivered = { count: 0, lastAt: null, byDomain: {} };
247
+ // 最近一步里"每个域为什么发/不发"(官方载体 / 开关关 / 空 / 本插件负责)。
248
+ // 只在没有可见注入时用来解释状态 —— 标准类预设下技能与提示词由官方在送,
249
+ // 插件的实况若只说"未投递",读起来像出了问题。
250
+ let lastReasons = {};
251
+ const live = () => {
252
+ let agent;
253
+ try {
254
+ agent = lastAgent ? lastAgent.deref() : undefined;
255
+ }
256
+ catch {
257
+ agent = undefined;
258
+ }
259
+ const visible = agent === undefined
260
+ ? new Map()
261
+ : newestDomainTexts(agent, LIVE_KINDS);
262
+ const labelOf = new Map(deps.domains().map((domain) => [domain.key, domain.label]));
263
+ const rows = INJECT_DOMAIN_KEYS.map((key) => {
264
+ const entry = visible.get(key);
265
+ const reason = lastReasons[key];
266
+ const state = agent === undefined
267
+ ? 'unknown'
268
+ : entry !== undefined
269
+ // 官方载体发的那条也算"模型看到的内容"(只是不是本插件送的);
270
+ // 本插件发过又清空的,报「已清空」。
271
+ ? (entry.official ? 'official' : entry.form === 'notice' ? 'cleared' : 'in-context')
272
+ // 没在上下文里:用最近一步的原因解释;`sent`(该发)却没看到 = 压缩后还没补发。
273
+ : reason === 'off' ? 'off' : reason === 'official' ? 'official' : reason === 'empty' ? 'empty' : 'absent';
274
+ const text = (state === 'in-context' || state === 'official') && entry !== undefined ? entry.text : '';
275
+ return { key, label: labelOf.get(key) ?? key, kind: INJECT_KIND_OF[key], state, bytes: bytesOf(text), text };
276
+ });
277
+ return {
278
+ hasAgent: agent !== undefined,
279
+ delivered: { count: delivered.count, lastAt: delivered.lastAt, byDomain: { ...delivered.byDomain } },
280
+ domains: rows,
281
+ };
282
+ };
227
283
  const ctx = deps.ctx;
228
284
  if (!ctx || typeof ctx.on !== 'function')
229
- return { dispose: () => { } };
285
+ return { dispose: () => { }, live };
230
286
  const stop = ctx.on('agent/pre-step', async (payload, next) => {
231
287
  const decision = await next();
232
288
  try {
@@ -235,22 +291,57 @@ export function createContextInjector(deps) {
235
291
  const agent = payload && payload.agent;
236
292
  if (!agent)
237
293
  return decision;
294
+ if (typeof agent === 'object') {
295
+ try {
296
+ lastAgent = new WeakRef(agent);
297
+ }
298
+ catch { /* 环境没有 WeakRef → 实况显示"没有会话" */ }
299
+ }
238
300
  // 空 turn 不注入(官方 dsh-agent-instructions 同款守卫):step 1 且一条消息都没有时,
239
301
  // 这一步本来就该原地结束(宿主随后把 turn 判为 completed)。此时注入会把空 turn
240
302
  // 变成一次真实的模型请求 —— 凭空烧一次调用。
241
303
  if (Number(payload.step) === 1 && messagesOf(decision).length === 0)
242
304
  return decision;
243
305
  const domains = deps.domains();
244
- const sections = selectInjections(domains, deps.settings(), await deps.factsFor(agent));
245
- const visible = newestDomainTexts(agent);
306
+ const settings = deps.settings();
307
+ const facts = await deps.factsFor(agent);
308
+ const sections = selectInjections(domains, settings, facts);
309
+ // 记录每个域这一步"为什么发 / 为什么不发",供「注入实况」解释状态:
310
+ // off = 开关关了;official = 官方载体在送(技能 / 提示词,标准类预设下的常态);
311
+ // empty = 本插件负责但没内容;sent = 本插件负责且有内容(上下文里看不到时说明还没补发)。
312
+ const reasons = {};
313
+ for (const domain of domains) {
314
+ if (settings.domains[domain.key] !== true) {
315
+ reasons[domain.key] = 'off';
316
+ continue;
317
+ }
318
+ const carrier = CARRIER_FACT_OF[domain.key];
319
+ if (carrier !== undefined && facts?.[carrier] !== false) {
320
+ reasons[domain.key] = 'official';
321
+ continue;
322
+ }
323
+ let text = '';
324
+ try {
325
+ text = String(domain.text() ?? '');
326
+ }
327
+ catch {
328
+ text = '';
329
+ }
330
+ reasons[domain.key] = text.trim() === '' ? 'empty' : 'sent';
331
+ }
332
+ lastReasons = reasons;
333
+ const visible = newestDomainTexts(agent, PLUGIN_KINDS);
246
334
  const additions = [];
335
+ const appended = [];
247
336
  const published = new Set();
248
337
  for (const section of sections) {
249
338
  published.add(section.key);
250
339
  const text = renderDomainText(section);
251
- if (visible.get(section.key) === text)
340
+ const current = visible.get(section.key);
341
+ if (current !== undefined && current.text === text)
252
342
  continue;
253
343
  additions.push(domainMessage(section, text));
344
+ appended.push(section.key);
254
345
  }
255
346
  // 曾经注入过、这一轮没有内容的域 → 一条「已清空」;从没注入过的域什么都不用说。
256
347
  const labelOf = new Map(domains.map((domain) => [domain.key, domain.label]));
@@ -261,12 +352,17 @@ export function createContextInjector(deps) {
261
352
  if (previous === undefined)
262
353
  continue;
263
354
  const label = labelOf.get(key) ?? key;
264
- if (previous === clearedDomainText(key, label))
355
+ if (previous.form === 'notice')
265
356
  continue;
266
357
  additions.push(clearedMessage(key, label));
358
+ appended.push(key);
267
359
  }
268
360
  if (additions.length === 0)
269
361
  return decision;
362
+ delivered.count += additions.length;
363
+ delivered.lastAt = Date.now();
364
+ for (const key of appended)
365
+ delivered.byDomain[key] = (delivered.byDomain[key] || 0) + 1;
270
366
  return { ...decision, messages: [...messagesOf(decision), ...additions] };
271
367
  }
272
368
  catch (error) {
@@ -281,6 +377,7 @@ export function createContextInjector(deps) {
281
377
  stop();
282
378
  }
283
379
  catch { /* ignore */ } },
380
+ live,
284
381
  };
285
382
  }
286
383
  function messagesOf(decision) {