dsh-harmonyos-hiboard 0.7.1 → 0.7.2

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 CHANGED
@@ -223,6 +223,11 @@ L2 抢救"承诺了却没调用"、L3 按错误类别重试并自动补 `schedul
223
223
  - `retry-ok`:工具真的调用了但失败,按错误类别重试后成功
224
224
  - `rescue-ok`:根本没调用工具(写成纯文本 / 只承诺没做),兜底代为补推
225
225
 
226
+ **关掉兜底后面板不会消失**,而是改显示「**漏掉未推送 N 次**」——
227
+ 兜底关闭期间它不再补推,但**继续记账**(记成 `missed`):本该补推的(写成纯文本 /
228
+ 只承诺没调用)以及调用失败却没重试的,都会被记下来。
229
+ 关掉它的代价应该看得见,而不是等你发现卡片少了几张。
230
+
226
231
  这个面板的意义是:以前"保险丝到底有没有干活"只能靠猜,现在直接列给你看。
227
232
 
228
233
  配置写在 `<用户目录>/.dsh/hiboard-push/config.json`(0600,先写临时文件再原子替换)。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-harmonyos-hiboard",
3
- "version": "0.7.1",
4
- "description": "把DSH的任务结果,以服务卡片形式推送到华为鸿蒙负一屏\nPush DSH task results to the Huawei HarmonyOS assistant-today screen as service cards.",
3
+ "version": "0.7.2",
4
+ "description": "把DSH的任务结果,以服务卡片形式推送到华为鸿蒙负一屏 | Push DSH task results to the Huawei HarmonyOS assistant-today screen as service cards.",
5
5
  "author": "Jim Chen",
6
6
  "license": "MIT",
7
7
  "repository": {
package/src/lib/client.js CHANGED
@@ -101,8 +101,13 @@ window.__ModuleLoader__.load({
101
101
  },
102
102
  debugEmpty: '还没有需要兜底的记录 —— 每次推送都成功调用了工具。',
103
103
  debugNone: '兜底还没有运行记录。',
104
+ debugMissedSummary: function (c) {
105
+ return '兜底已关闭 —— 期间漏掉未推送 ' + c.missed + ' 次';
106
+ },
107
+ debugMissedEmpty: '兜底关闭期间还没有漏推记录。',
104
108
  kindRetry: '重试',
105
109
  kindRescue: '补推',
110
+ kindMissed: '漏推',
106
111
  loadFailed: '读取配置失败',
107
112
  saveFailed: '保存失败',
108
113
  unavailable: '设置路由不可用(宿主未提供)。'
@@ -139,8 +144,13 @@ window.__ModuleLoader__.load({
139
144
  },
140
145
  debugEmpty: 'Nothing needed rescuing yet — every push called the tool successfully.',
141
146
  debugNone: 'The safety net has no run records yet.',
147
+ debugMissedSummary: function (c) {
148
+ return 'Safety net is off — ' + c.missed + ' pushes have gone missing since';
149
+ },
150
+ debugMissedEmpty: 'Nothing has gone missing while the net was off.',
142
151
  kindRetry: 'retry',
143
152
  kindRescue: 'rescue',
153
+ kindMissed: 'missed',
144
154
  loadFailed: 'Could not load the configuration',
145
155
  saveFailed: 'Could not save',
146
156
  unavailable: 'The settings route is unavailable (host did not provide it).'
@@ -323,26 +333,48 @@ window.__ModuleLoader__.load({
323
333
  preview = h('div', { style: { marginTop: '6px', fontSize: '12px', color: V.danger } }, t.invalid);
324
334
  }
325
335
 
326
- // ---- 兜底战绩:把「原本没推出去、但兜底救回来了」摆出来 ----
336
+ // ---- 战绩面板 ----
337
+ // 兜底**开着**:显示「救回了几次」。
338
+ // 兜底**关掉**:面板不消失,改显示「漏掉了几张」—— 关掉它的代价要看得见,
339
+ // 否则用户只会觉得"关了就关了",直到发现卡片少了几张。
340
+ var guardOn = Boolean(guard && guard.enabled);
341
+ var itemRows = function (items, kindOf) {
342
+ return (items || []).map(function (item, index) {
343
+ return h(
344
+ 'div',
345
+ { key: String(index), style: { fontSize: '12px', lineHeight: '1.7', color: V.sub } },
346
+ '· ' + shortTime(item.at) + ' [' + kindOf(item) + '] ' + (item.label || '-') + ' — ' + item.detail
347
+ );
348
+ });
349
+ };
350
+ var boldLine = function (text) {
351
+ return h('div', { style: { fontSize: '12px', fontWeight: 600, marginBottom: '4px' } }, text);
352
+ };
327
353
  var debugBody;
328
354
  if (!audit) {
329
355
  debugBody = muted(t.debugNone);
330
- } else if (!audit.exists || audit.counts.retryOk + audit.counts.rescueOk === 0) {
331
- debugBody = muted(t.debugEmpty);
356
+ } else if (guardOn) {
357
+ debugBody =
358
+ !audit.exists || audit.counts.retryOk + audit.counts.rescueOk === 0
359
+ ? muted(t.debugEmpty)
360
+ : h(
361
+ 'div',
362
+ null,
363
+ boldLine(t.debugSummary(audit.counts)),
364
+ itemRows(audit.recent, function (item) {
365
+ return item.kind === 'retry-ok' ? t.kindRetry : t.kindRescue;
366
+ })
367
+ );
332
368
  } else {
333
- debugBody = h(
334
- 'div',
335
- null,
336
- h('div', { style: { fontSize: '12px', fontWeight: 600, marginBottom: '4px' } }, t.debugSummary(audit.counts)),
337
- (audit.recent || []).map(function (item, index) {
338
- return h(
339
- 'div',
340
- { key: String(index), style: { fontSize: '12px', lineHeight: '1.7', color: V.sub } },
341
- '· ' + shortTime(item.at) + ' [' + (item.kind === 'retry-ok' ? t.kindRetry : t.kindRescue) + '] ' +
342
- (item.label || '-') + ' — ' + item.detail
343
- );
344
- })
345
- );
369
+ debugBody =
370
+ !audit.exists || audit.counts.missed === 0
371
+ ? muted(t.debugMissedEmpty)
372
+ : h(
373
+ 'div',
374
+ null,
375
+ boldLine(t.debugMissedSummary(audit.counts)),
376
+ itemRows(audit.missedRecent, function () { return t.kindMissed; })
377
+ );
346
378
  }
347
379
 
348
380
  return h(
package/src/lib/index.js CHANGED
@@ -446,7 +446,7 @@ function buildConfigRoute({ authCodeInfo, settings, storeFile }) {
446
446
  */
447
447
  const auditSummary = () => {
448
448
  const file = join(GUARD_STATE_DIR, "audit.jsonl");
449
- const empty = { file, exists: false, counts: { retryOk: 0, rescueOk: 0, failed: 0 }, recent: [] };
449
+ const empty = { file, exists: false, counts: { retryOk: 0, rescueOk: 0, failed: 0, missed: 0 }, recent: [], missedRecent: [] };
450
450
  let text;
451
451
  try {
452
452
  text = readFileSync(file, "utf8");
@@ -455,8 +455,16 @@ function buildConfigRoute({ authCodeInfo, settings, storeFile }) {
455
455
  return empty;
456
456
  }
457
457
  const lines = text.split("\n").filter(Boolean).slice(-AUDIT_MAX_LINES);
458
- const counts = { retryOk: 0, rescueOk: 0, failed: 0 };
458
+ const counts = { retryOk: 0, rescueOk: 0, failed: 0, missed: 0 };
459
459
  const recent = [];
460
+ const missedRecent = [];
461
+ /** missed 的原因码 → 面板上的说法。 */
462
+ const MISSED_REASON = {
463
+ "literal-text-unrescued": "模型把调用写成了纯文本(兜底已关闭,未补推)",
464
+ "broken-promise-unrescued": "模型承诺了推送却没调用工具(兜底已关闭,未补推)",
465
+ "failed-retryable-unretried": "调用失败,本可重试(兜底已关闭,未重试)",
466
+ "failed-permanent": "调用永久失败(兜底不会重试这类错误)"
467
+ };
460
468
  for (const line of lines) {
461
469
  let row;
462
470
  try {
@@ -484,13 +492,24 @@ function buildConfigRoute({ authCodeInfo, settings, storeFile }) {
484
492
  label: row.name ?? "",
485
493
  detail: row.level === "L1" ? "模型把调用写成了纯文本" : "模型承诺了推送却没调用工具"
486
494
  });
495
+ } else if (row?.kind === "missed") {
496
+ // 兜底关闭期间"本该补推 / 本该重试"的记录:这是关掉它的代价,要看得到
497
+ counts.missed += 1;
498
+ missedRecent.push({
499
+ kind: "missed",
500
+ at: row.at ?? "",
501
+ level: row.level ?? "",
502
+ label: row.name ?? "",
503
+ detail: MISSED_REASON[row.reason] ?? String(row.reason ?? "未推送")
504
+ });
487
505
  } else if (row?.kind === "push-failed" || row?.kind === "rescue-failed" || row?.kind === "rescue-error") {
488
506
  counts.failed += 1;
489
507
  }
490
508
  }
491
509
  // 文件是追加写的,末尾最新;面板要「最近几次」,所以倒序后取前几条
492
510
  recent.reverse();
493
- return { file, exists: true, counts, recent: recent.slice(0, 5) };
511
+ missedRecent.reverse();
512
+ return { file, exists: true, counts, recent: recent.slice(0, 5), missedRecent: missedRecent.slice(0, 5) };
494
513
  };
495
514
 
496
515
  const summary = () => {
@@ -546,8 +546,9 @@ export function apply(ctx, config) {
546
546
  ctx.effect(() => {
547
547
  const onEvent = (session, event) => {
548
548
  try {
549
- // 设置页关掉后,兜底立即停止(不必重启)
550
- if (guardOff()) return;
549
+ // 注意:**关掉兜底后这个处理器仍要继续跑**。它不再补推,但仍要记账
550
+ // (见下面的 missed)——「关掉期间漏掉多少张卡片」只有继续观察才数得出来,
551
+ // 否则设置页那一栏永远是空的。
551
552
  if (!event || typeof event !== "object") return;
552
553
  const sessionId = session?.id ?? "?";
553
554
 
@@ -584,6 +585,25 @@ export function apply(ctx, config) {
584
585
  }
585
586
  return;
586
587
  }
588
+
589
+ // 兜底被关掉时:**不补推,但照样记账**。
590
+ // 记成 missed 而不是 rescue-ok —— 设置页据此显示「关掉期间漏掉 N 张」。
591
+ // 关掉兜底的代价应该是看得见的,而不是悄悄少几张卡片。
592
+ if (guardOff()) {
593
+ audit({
594
+ kind: "missed",
595
+ level: card.level,
596
+ name: card.name,
597
+ sessionId,
598
+ turn: current.turn,
599
+ reason: card.level === "L1" ? "literal-text-unrescued" : "broken-promise-unrescued"
600
+ });
601
+ ctx.logger?.warn?.(
602
+ "[push-guard] ⚠️ %s 本该补推(会话 %s 第 %s 轮):%s —— 但兜底已关闭,仅记账未推送",
603
+ card.level, sessionId, current.turn ?? "?", card.name
604
+ );
605
+ return;
606
+ }
587
607
  const fingerprint = `${card.name}\n${card.content.length}\n${card.content.slice(0, 80)}`;
588
608
  if (lastRescued.get(sessionId) === fingerprint) return;
589
609
  lastRescued.set(sessionId, fingerprint);
@@ -639,8 +659,31 @@ export function apply(ctx, config) {
639
659
  const ambiguousAttempts = ambiguousBudget;
640
660
  const fallbackScheduleId = String(settings.defaultScheduleId || DEFAULTS.defaultScheduleId).trim() || DEFAULTS.defaultScheduleId;
641
661
  const wrapped = async (args, exec) => {
642
- // 设置页关掉后:不补参、不重试,按原样调用(关闭必须真的关闭)
643
- if (guardOff()) return original(args, exec);
662
+ // 设置页关掉后:不补参、不重试,按原样调用(关闭必须真的关闭)。
663
+ // 但**照样给失败记账**:调用失败了却没重试,就是"漏掉一张"。
664
+ if (guardOff()) {
665
+ const outcome = await original(args, exec);
666
+ try {
667
+ const verdict = classifyPush(outcome);
668
+ if (!verdict.ok) {
669
+ audit({
670
+ kind: "missed",
671
+ level: "L3",
672
+ name: String(args?.name ?? ""),
673
+ reason: verdict.retryable ? "failed-retryable-unretried" : "failed-permanent",
674
+ code: verdict.code ?? "",
675
+ message: String(verdict.message ?? "").slice(0, 160)
676
+ });
677
+ ctx.logger?.warn?.(
678
+ "[push-guard] ⚠️ hiboard_push 失败(%s,%s)—— 兜底已关闭,未重试,仅记账",
679
+ verdict.code || "-", verdict.retryable ? "本可重试" : "永久错误"
680
+ );
681
+ }
682
+ } catch {
683
+ /* 记账失败不影响工具本身的返回 */
684
+ }
685
+ return outcome;
686
+ }
644
687
  // ---- 先补 schedule_id,再让工具校验/发送 ----
645
688
  // 为什么必须补:scheduleTaskId 为空时负一屏**只显示一行标题**,
646
689
  // 正文不渲染、详情页点不开(2026-09-27 用户实际遇到)。