dsh-harmonyos-hiboard 0.7.0 → 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
@@ -142,7 +142,8 @@ dsh plugin --profile web add "file:<不含空格的路径>"
142
142
 
143
143
  **2) 填授权码** —— 两种方式,任选其一。
144
144
 
145
- **方式 A(推荐):图形界面。** 重启 dsh 后打开 **设置 → 插件 → `dsh-harmonyos-hiboard`**,
145
+ **方式 A(推荐):图形界面。** 重启 dsh 后打开 **主界面左侧栏的「插件」面板 →
146
+ `dsh-harmonyos-hiboard` → 点进它的页面**(**不在「设置」里**,见下方「设置卡片」一节),
146
147
  在「推送设置」卡片里填写。输入框**两种写法都认**:
147
148
 
148
149
  ```text
@@ -199,14 +200,35 @@ L2 抢救"承诺了却没调用"、L3 按错误类别重试并自动补 `schedul
199
200
 
200
201
  ---
201
202
 
202
- ## 设置页(插件卡片)
203
+ ## 设置卡片(在「插件」面板里)
203
204
 
204
- 打开 **设置 → 插件 → `dsh-harmonyos-hiboard`**,卡片提供两件事:
205
+ 打开 **主界面左侧栏的「插件」面板 → `dsh-harmonyos-hiboard` → 点进它的页面**,
206
+ 「推送设置」卡片就在简介与条目行之间。
207
+
208
+ > **注意不是「设置」里。** dsh 的设置导航里那个「内置插件」页只提供只读清单与市场页签,
209
+ > 它**不声明** `plugins.bundle.config` 插槽 —— 这条卡片永远不可能出现在那里。
210
+ > 承载配置卡片的是 `dsh-client-ui-plugin-manager`(侧栏面板 id `plugins`,文案「插件」),
211
+ > 本插件与 modsearch 的卡片都在同一个位置。
212
+ > 新装或升级后如果没看到卡片,**先刷新页面**;**版本升级还需重启 dsh**(已挂载的插件不会被热替换)。
213
+
214
+ 卡片提供三件事:
205
215
 
206
216
  1. **授权码输入框** —— 裸授权码与整条关联指令都认(自动取末尾的码),保存前显示打码预览,
207
217
  保存后**立即生效**;卡片还显示**当前生效来源**(设置页 / 补丁层 / 环境变量),
208
218
  避免"改了一处却不生效"。点「清除」删掉设置页那份,回退到下一层。
209
219
  2. **推送兜底开关** —— **默认开启**,关掉**立即生效**(不必重启)。
220
+ 文案里也直说了:建议保持开启。
221
+ 3. **兜底战绩** —— 列出**「原本没推出去、但兜底救回来了」**的次数与最近几次,
222
+ 数据来自兜底自己的审计流水(`<状态目录>/audit.jsonl`):
223
+ - `retry-ok`:工具真的调用了但失败,按错误类别重试后成功
224
+ - `rescue-ok`:根本没调用工具(写成纯文本 / 只承诺没做),兜底代为补推
225
+
226
+ **关掉兜底后面板不会消失**,而是改显示「**漏掉未推送 N 次**」——
227
+ 兜底关闭期间它不再补推,但**继续记账**(记成 `missed`):本该补推的(写成纯文本 /
228
+ 只承诺没调用)以及调用失败却没重试的,都会被记下来。
229
+ 关掉它的代价应该看得见,而不是等你发现卡片少了几张。
230
+
231
+ 这个面板的意义是:以前"保险丝到底有没有干活"只能靠猜,现在直接列给你看。
210
232
 
211
233
  配置写在 `<用户目录>/.dsh/hiboard-push/config.json`(0600,先写临时文件再原子替换)。
212
234
  该文件属于当前系统用户,本机所有 dsh profile 共用一份。
@@ -358,6 +380,35 @@ npm test
358
380
 
359
381
  ---
360
382
 
383
+ ## 新环境部署清单(怎么对齐到「每轮都推送」)
384
+
385
+ 换一台电脑、或让别人从零装一遍,**装完插件只是一半**。完整清单:
386
+
387
+ | # | 做什么 | 不做会怎样 |
388
+ |---|---|---|
389
+ | 1 | 装插件:`dsh plugin --profile web add dsh-harmonyos-hiboard` | 没有工具 |
390
+ | 2 | 配授权码(设置卡片,或 profile 补丁层) | 调用时报「未配置授权码」 |
391
+ | 3 | **把推送规则写进 `~/.dsh/AGENTS.md`** | 功能在,但**没人调用它** —— 每轮都不会推 |
392
+ | 4 | 重启 dsh | 新装/升级的插件不生效(升级不会热替换已挂载插件) |
393
+
394
+ 第 3 步的现成模板见 **[`docs/AGENTS-rule.md`](docs/AGENTS-rule.md)**:dsh 由
395
+ `dsh-agent-instructions` 在**每个会话**自动加载 `$DSH_HOME/AGENTS.md`,写在那里就是全局生效。
396
+
397
+ > ⚠️ **「每轮必定推送」不是插件能保证的事,要如实理解。**
398
+ > `hiboard_push` 是一个**工具**,工具不会自己调用 —— 调不调用由模型按指令决定。
399
+ > 内置兜底把「漏推」的概率压得很低,但它是**启发式**,不是硬保证:
400
+ > - L1/L2 需要正文里**看得出推送意图**才会补推(保守优先,宁可少推也不乱推);
401
+ > - L3 只对**可重试**的失败生效(授权码无效、内容超长这类永久错误不重试);
402
+ > - 授权码未配置、或端点处于坏窗口且重试预算用尽时,仍然推不出去。
403
+ >
404
+ > 准确的表述是:**规则 + 兜底让「每轮都推」在实践上基本成立**,而不是被保证。
405
+
406
+ **不必迁移、与本插件无关的东西**:本机网络入口(局域网 3081 / tailnet 8443 / 公网 18443)、
407
+ token 广播、Steam++ 之类的 hosts 加速器 —— 那些属于本机环境。
408
+ 本插件只需要:Node 18+、pnpm、一台能取到授权码的华为手机。
409
+
410
+ ---
411
+
361
412
  ## 兼容性
362
413
 
363
414
  | 组件 | 版本 |
package/docs/DEPLOY.md CHANGED
@@ -51,7 +51,8 @@ dsh plugin --profile web add "file:E:/dsh-vendor/dsh-harmonyos-hiboard"
51
51
 
52
52
  ## 第 3 步:写入授权码
53
53
 
54
- **推荐做法(图形界面)**:不用命令行 —— 打开 **设置 → 插件 → `dsh-harmonyos-hiboard`**,
54
+ **推荐做法(图形界面)**:不用命令行 —— 打开 **主界面侧栏的「插件」面板 →
55
+ `dsh-harmonyos-hiboard` → 点进它的页面**(**不在「设置」里**,见 README「设置卡片」一节),
55
56
  在「推送设置」卡片里填。输入框**裸授权码**和**整条关联指令**
56
57
  (`openclaw config set skills.entries.today-task.config.authCode <AUTH_CODE>`)都认,
57
58
  保存后立即生效,并显示当前生效来源。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-harmonyos-hiboard",
3
- "version": "0.7.0",
4
- "description": "把 dsh 的任务结果推送到华为鸿蒙负一屏服务卡片;设置页可直接粘贴关联指令;内置推送兜底,默认开启。 | Push dsh task results to Huawei HarmonyOS assistant-today cards, with a settings card and a push safety net that is on by default.",
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
@@ -348,7 +348,7 @@ function hiboardPushTool({ resolveAuthCode, settings }) {
348
348
  const authCode = resolveAuthCode();
349
349
  if (!authCode) {
350
350
  throw new Error(
351
- "hiboard_push: 未配置授权码(authCode)。请在 dsh 的「插件」页打开本插件的设置卡片填写" +
351
+ "hiboard_push: 未配置授权码(authCode)。请打开 dsh 侧栏的「插件」面板 → 本插件 → 在「推送设置」卡片里填写" +
352
352
  "(支持整条关联指令或裸授权码),或写进 profile 补丁层 hiboard-push.config.authCode," +
353
353
  `或设置环境变量 ${AUTH_CODE_ENV}。授权码获取:负一屏 → 我的 → 动态管理 → 关联账号 → Claw 智能体。`
354
354
  );
@@ -388,7 +388,7 @@ function hiboardVerifyTool({ resolveAuthCode, settings }) {
388
388
  const authCode = resolveAuthCode();
389
389
  if (!authCode) {
390
390
  throw new Error(
391
- "hiboard_verify: 未配置授权码(authCode)。请在 dsh 的「插件」页打开本插件的设置卡片填写" +
391
+ "hiboard_verify: 未配置授权码(authCode)。请打开 dsh 侧栏的「插件」面板 → 本插件 → 在「推送设置」卡片里填写" +
392
392
  "(支持整条关联指令或裸授权码),或写进 profile 补丁层 hiboard-push.config.authCode," +
393
393
  `或设置环境变量 ${AUTH_CODE_ENV}。授权码获取:负一屏 → 我的 → 动态管理 → 关联账号 → Claw 智能体。`
394
394
  );
@@ -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 用户实际遇到)。