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 +54 -3
- package/docs/DEPLOY.md +2 -1
- package/package.json +2 -2
- package/src/lib/client.js +48 -16
- package/src/lib/index.js +24 -5
- package/src/lib/push-guard.js +47 -4
package/README.md
CHANGED
|
@@ -142,7 +142,8 @@ dsh plugin --profile web add "file:<不含空格的路径>"
|
|
|
142
142
|
|
|
143
143
|
**2) 填授权码** —— 两种方式,任选其一。
|
|
144
144
|
|
|
145
|
-
**方式 A(推荐):图形界面。** 重启 dsh 后打开
|
|
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
|
-
打开
|
|
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
|
-
**推荐做法(图形界面)**:不用命令行 —— 打开
|
|
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.
|
|
4
|
-
"description": "把
|
|
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 (
|
|
331
|
-
debugBody =
|
|
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 =
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 = () => {
|
package/src/lib/push-guard.js
CHANGED
|
@@ -546,8 +546,9 @@ export function apply(ctx, config) {
|
|
|
546
546
|
ctx.effect(() => {
|
|
547
547
|
const onEvent = (session, event) => {
|
|
548
548
|
try {
|
|
549
|
-
//
|
|
550
|
-
|
|
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
|
-
|
|
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 用户实际遇到)。
|