dsh-harmonyos-hiboard 0.7.2 → 0.9.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.
- package/README.md +22 -1
- package/package.json +1 -1
- package/src/lib/agents-rule.js +145 -0
- package/src/lib/client.js +111 -2
- package/src/lib/index.js +108 -7
- package/src/lib/push-guard.js +48 -17
package/README.md
CHANGED
|
@@ -230,6 +230,27 @@ L2 抢救"承诺了却没调用"、L3 按错误类别重试并自动补 `schedul
|
|
|
230
230
|
|
|
231
231
|
这个面板的意义是:以前"保险丝到底有没有干活"只能靠猜,现在直接列给你看。
|
|
232
232
|
|
|
233
|
+
4. **常驻推送规则**(默认开启)—— **保存授权码时,会把「每轮回答都要推送」自动写进
|
|
234
|
+
全局指令文件 `$DSH_HOME/AGENTS.md`**,这样「一键安装 + 粘一次授权码」之后就不必再
|
|
235
|
+
手改任何文件,新会话立刻按规则推送。
|
|
236
|
+
|
|
237
|
+
> ⚠️ **这是本插件唯一会改动你已有文件的行为,且明确声明在此。** 具体约束:
|
|
238
|
+
> - 规则被 **BEGIN/END 标记**包裹,插件**只改标记之间那一段**,标记之外一个字节都不动;
|
|
239
|
+
> - **幂等**:重复保存不会产生第二份规则;
|
|
240
|
+
> - **首次改动前自动备份** `AGENTS.md.bak-hiboard-<时间戳>`(只备份一次);
|
|
241
|
+
> - **检测到你已手写过推送规则就完全跳过**,并如实提示,而不是硬塞第二份;
|
|
242
|
+
> - 卡片上那颗开关可以**一键移除**该段(其余内容原样保留),关掉后再保存授权码也不会重新写入。
|
|
243
|
+
|
|
244
|
+
**为什么必须写这个文件**:dsh 的 `dsh-agent-instructions` 每个会话都加载
|
|
245
|
+
`$DSH_HOME/AGENTS.md`,而且**只认这一个全局文件** —— `.local` 叠加(`AGENTS.local.md`)
|
|
246
|
+
只在**项目目录**的祖先链里生效,全局那段代码写死了只探测 `AGENTS.md`。
|
|
247
|
+
(`agent/pre-step` 注入要拼宿主内部消息形状,在预览版上太脆,所以没有采用。)
|
|
248
|
+
|
|
249
|
+
> **授权码仍然必须手动粘一次**,这一点无法自动化:它是你在手机上
|
|
250
|
+
> 「负一屏 → 我的 → 动态管理 → 关联账号 → Claw 智能体」取到的**个人凭据**,
|
|
251
|
+
> 任何插件都不可能凭空得到它。所以准确说法是:安装后规则自动就位,
|
|
252
|
+
> 你只需**在卡片里粘一次授权码**。
|
|
253
|
+
|
|
233
254
|
配置写在 `<用户目录>/.dsh/hiboard-push/config.json`(0600,先写临时文件再原子替换)。
|
|
234
255
|
该文件属于当前系统用户,本机所有 dsh profile 共用一份。
|
|
235
256
|
|
|
@@ -388,7 +409,7 @@ npm test
|
|
|
388
409
|
|---|---|---|
|
|
389
410
|
| 1 | 装插件:`dsh plugin --profile web add dsh-harmonyos-hiboard` | 没有工具 |
|
|
390
411
|
| 2 | 配授权码(设置卡片,或 profile 补丁层) | 调用时报「未配置授权码」 |
|
|
391
|
-
| 3 |
|
|
412
|
+
| 3 | 保存授权码 —— 插件会把推送规则**自动**写进 `~/.dsh/AGENTS.md`(默认开启、可一键关掉) | 功能在,但**没人调用它** —— 每轮都不会推 |
|
|
392
413
|
| 4 | 重启 dsh | 新装/升级的插件不生效(升级不会热替换已挂载插件) |
|
|
393
414
|
|
|
394
415
|
第 3 步的现成模板见 **[`docs/AGENTS-rule.md`](docs/AGENTS-rule.md)**:dsh 由
|
package/package.json
CHANGED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 常驻推送规则:把「每轮回答都要推送」写进 dsh 的全局指令文件。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么必须动这个文件
|
|
5
|
+
*
|
|
6
|
+
* dsh 的 `dsh-agent-instructions` 在每个会话自动加载 `$DSH_HOME/AGENTS.md`。
|
|
7
|
+
* 它**只认这一个全局文件** —— `.local` 叠加(`AGENTS.local.md`)只在**项目目录**的
|
|
8
|
+
* 祖先链里生效,全局那段代码写死了只探测 `AGENTS.md`(见 dsh-agent-instructions
|
|
9
|
+
* 的 `discoverInstructionFiles`)。所以想让规则对所有会话、所有项目生效,
|
|
10
|
+
* 除了写这个文件没有第二条路(`agent/pre-step` 注入要拼宿主内部消息形状,预览版上太脆)。
|
|
11
|
+
*
|
|
12
|
+
* ## 安全约定(每一条都是必须的)
|
|
13
|
+
*
|
|
14
|
+
* 1. **只改自己那一段**:规则块由 BEGIN/END 标记包裹,标记之外一个字节都不碰。
|
|
15
|
+
* 2. **幂等**:标记已存在就原地替换;重复调用不会产生第二份规则。
|
|
16
|
+
* 3. **不覆盖用户手写的规则**:文件里已有等价规则(非标记块)时**直接跳过**,
|
|
17
|
+
* 并如实报告,而不是硬塞一份重复的。
|
|
18
|
+
* 4. **首次改动前备份**:`AGENTS.md.bak-hiboard-<时间戳>`,且只备份一次,不刷屏。
|
|
19
|
+
* 5. **可一键移除**:`removeRule()` 把标记块整段删掉,其余内容原样保留。
|
|
20
|
+
*/
|
|
21
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, readdirSync } from "node:fs";
|
|
22
|
+
import { basename, dirname } from "node:path";
|
|
23
|
+
|
|
24
|
+
/** 规则块的起止标记。改这里等于改契约,不要动。 */
|
|
25
|
+
export const RULE_BEGIN = "<!-- BEGIN dsh-harmonyos-hiboard (managed block; the plugin rewrites this region) -->";
|
|
26
|
+
export const RULE_END = "<!-- END dsh-harmonyos-hiboard -->";
|
|
27
|
+
|
|
28
|
+
/** 规则块正文。中英并列:dsh 的全局文件对所有会话生效,读者可能是任意语言。 */
|
|
29
|
+
export function buildRuleBlock() {
|
|
30
|
+
return [
|
|
31
|
+
RULE_BEGIN,
|
|
32
|
+
"## 强制要求:每轮回答都要推送到手机负一屏",
|
|
33
|
+
"",
|
|
34
|
+
"1. 给出最终回答时,调用 `hiboard_push` 推送本轮的详细结论。",
|
|
35
|
+
"2. **必须传 `schedule_id`**(例如 `dsh_worklog`)—— 它为空时卡片只有一行标题、正文不渲染、也点不开。",
|
|
36
|
+
"3. `content` 用 Markdown,**上限 5000 字符**;超长就推精简版,并在开头注明被截断,完整内容留在对话里。",
|
|
37
|
+
"4. `result` 是状态标签,默认「任务已完成」。",
|
|
38
|
+
"5. 本轮确实没有实质输出时,也推一张简短卡片,不要跳过。",
|
|
39
|
+
"6. 推送失败要写明原因(未配置授权码 / 接口不通),不要静默略过。",
|
|
40
|
+
"",
|
|
41
|
+
"> Every turn must push: call `hiboard_push` with a non-empty `schedule_id`; Markdown body is",
|
|
42
|
+
"> capped at 5000 characters; push a short card even on a thin turn; never fail silently.",
|
|
43
|
+
"",
|
|
44
|
+
"(本段由 dsh-harmonyos-hiboard 维护,可在插件的设置卡片里一键关闭与移除。)",
|
|
45
|
+
RULE_END
|
|
46
|
+
].join("\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 判定文件里是否已存在「等价规则」(非本插件标记块)。 */
|
|
50
|
+
export function looksLikePushRule(text) {
|
|
51
|
+
if (!/hiboard_push/.test(text)) return false;
|
|
52
|
+
return /(每轮|每一轮|每次|每一条|必须)[^\n]{0,24}(推|发送|推送)/.test(text) || /every\s+(turn|reply|response)/i.test(text);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readText(file) {
|
|
56
|
+
try {
|
|
57
|
+
return readFileSync(file, "utf8");
|
|
58
|
+
} catch {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hasBackup(file) {
|
|
64
|
+
try {
|
|
65
|
+
return readdirSync(dirname(file)).some((name) => name.startsWith(`${basename(file)}.bak-hiboard-`));
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 原子写入;`backup` 为真且尚无备份时,先留一份原件。 */
|
|
72
|
+
function writeText(file, text, { backup = false } = {}) {
|
|
73
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
74
|
+
if (backup && existsSync(file) && !hasBackup(file)) {
|
|
75
|
+
try {
|
|
76
|
+
copyFileSync(file, `${file}.bak-hiboard-${new Date().toISOString().replace(/[-:T]/g, "").slice(0, 15)}`);
|
|
77
|
+
} catch {
|
|
78
|
+
/* 备份失败不阻止写入:用户要的是规则生效,不是备份成功 */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
82
|
+
writeFileSync(tmp, text, "utf8");
|
|
83
|
+
renameSync(tmp, file);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 用新块替换旧块(保留前后一切内容)。 */
|
|
87
|
+
function replaceBlock(text, block) {
|
|
88
|
+
const start = text.indexOf(RULE_BEGIN);
|
|
89
|
+
const end = text.indexOf(RULE_END);
|
|
90
|
+
if (start < 0 || end < 0 || end < start) return text;
|
|
91
|
+
return `${text.slice(0, start)}${block}${text.slice(end + RULE_END.length)}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 状态:文件在哪、有没有我们的块、有没有用户自己写的等价规则。 */
|
|
95
|
+
export function ruleStatus(file) {
|
|
96
|
+
const text = readText(file);
|
|
97
|
+
const hasBlock = text.includes(RULE_BEGIN) && text.includes(RULE_END);
|
|
98
|
+
return {
|
|
99
|
+
file,
|
|
100
|
+
exists: existsSync(file),
|
|
101
|
+
block: hasBlock,
|
|
102
|
+
customRule: !hasBlock && looksLikePushRule(text)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 确保规则就位(幂等)。
|
|
108
|
+
* @returns {{action: 'added'|'updated'|'unchanged'|'skipped-custom', status: object}}
|
|
109
|
+
*/
|
|
110
|
+
export function ensureRule(file) {
|
|
111
|
+
const block = buildRuleBlock();
|
|
112
|
+
const before = readText(file);
|
|
113
|
+
const hasBlock = before.includes(RULE_BEGIN) && before.includes(RULE_END);
|
|
114
|
+
|
|
115
|
+
if (hasBlock) {
|
|
116
|
+
const next = replaceBlock(before, block);
|
|
117
|
+
if (next === before) return { action: "unchanged", status: ruleStatus(file) };
|
|
118
|
+
writeText(file, next);
|
|
119
|
+
return { action: "updated", status: ruleStatus(file) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 用户自己已经写过等价规则 → 不硬塞第二份
|
|
123
|
+
if (looksLikePushRule(before)) return { action: "skipped-custom", status: ruleStatus(file) };
|
|
124
|
+
|
|
125
|
+
const body = before.replace(/\s*$/, "");
|
|
126
|
+
const next = body ? `${body}\n\n${block}\n` : `${block}\n`;
|
|
127
|
+
writeText(file, next, { backup: true });
|
|
128
|
+
return { action: "added", status: ruleStatus(file) };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 移除规则块(只删标记之间那一段)。
|
|
133
|
+
* @returns {{action: 'removed'|'absent', status: object}}
|
|
134
|
+
*/
|
|
135
|
+
export function removeRule(file) {
|
|
136
|
+
const before = readText(file);
|
|
137
|
+
if (!before.includes(RULE_BEGIN)) return { action: "absent", status: ruleStatus(file) };
|
|
138
|
+
const start = before.indexOf(RULE_BEGIN);
|
|
139
|
+
const end = before.indexOf(RULE_END);
|
|
140
|
+
let next = `${before.slice(0, start)}${before.slice(end + RULE_END.length)}`;
|
|
141
|
+
// 清掉移除后留下的空行堆,但不动其它内容
|
|
142
|
+
next = next.replace(/\n{3,}/g, "\n\n").replace(/^\s*\n/, "");
|
|
143
|
+
writeText(file, next);
|
|
144
|
+
return { action: "removed", status: ruleStatus(file) };
|
|
145
|
+
}
|
package/src/lib/client.js
CHANGED
|
@@ -95,6 +95,19 @@ window.__ModuleLoader__.load({
|
|
|
95
95
|
guardBudget: function (g) {
|
|
96
96
|
return '重试预算:确定未送达 ' + g.retryCount + ' 次 / 可能已送达 ' + g.ambiguousRetryCount + ' 次 · schedule_id 兜底:' + g.defaultScheduleId;
|
|
97
97
|
},
|
|
98
|
+
ruleTitle: '每轮对话推送',
|
|
99
|
+
ruleOn: '已启用',
|
|
100
|
+
ruleOff: '未启用',
|
|
101
|
+
ruleHint: '保存授权码时,会自动把「每轮都要推送」写进全局指令文件 AGENTS.md(带标记、可随时移除),新会话即生效。',
|
|
102
|
+
ruleCustom: '检测到你已手写过推送规则 —— 插件不会改动你的 AGENTS.md。',
|
|
103
|
+
ruleBlockPresent: 'AGENTS.md 里已有本插件维护的规则段(带标记;关掉即可移除)。',
|
|
104
|
+
sourceTitle: '推送源名称',
|
|
105
|
+
sourceCurrent: function (v) {
|
|
106
|
+
return '当前生效:' + v;
|
|
107
|
+
},
|
|
108
|
+
sourcePlaceholder: 'DeepSeek Harness',
|
|
109
|
+
sourceHint: '负一屏卡片上显示的「来源 / AI 工具名」。改这里,本机所有推送(含启动卡片)会一起改用同一个名称。输入框留空并保存即恢复默认。',
|
|
110
|
+
sourceReset: '恢复默认',
|
|
98
111
|
debugTitle: '兜底战绩',
|
|
99
112
|
debugSummary: function (c) {
|
|
100
113
|
return '已救回 ' + (c.retryOk + c.rescueOk) + ' 次(重试 ' + c.retryOk + ' · 补推 ' + c.rescueOk + ')';
|
|
@@ -138,6 +151,19 @@ window.__ModuleLoader__.load({
|
|
|
138
151
|
guardBudget: function (g) {
|
|
139
152
|
return 'Retry budget: definitely-undelivered ' + g.retryCount + ' / possibly-delivered ' + g.ambiguousRetryCount + ' · schedule_id fallback: ' + g.defaultScheduleId;
|
|
140
153
|
},
|
|
154
|
+
ruleTitle: 'Push every turn',
|
|
155
|
+
ruleOn: 'enabled',
|
|
156
|
+
ruleOff: 'disabled',
|
|
157
|
+
ruleHint: 'Saving the authCode also writes an "every turn must push" rule into your global instruction file AGENTS.md (marker-delimited, removable); new sessions pick it up.',
|
|
158
|
+
ruleCustom: 'You already wrote a push rule by hand, so this plugin leaves AGENTS.md untouched.',
|
|
159
|
+
ruleBlockPresent: "AGENTS.md already carries this plugin's rule block (marker-delimited; switch this off to remove it).",
|
|
160
|
+
sourceTitle: 'Push source name',
|
|
161
|
+
sourceCurrent: function (v) {
|
|
162
|
+
return 'Currently: ' + v;
|
|
163
|
+
},
|
|
164
|
+
sourcePlaceholder: 'DeepSeek Harness',
|
|
165
|
+
sourceHint: 'The brand / AI tool name shown on the card. Changing it moves every push on this machine (including the startup card) to the same name. Save an empty value to restore the default.',
|
|
166
|
+
sourceReset: 'Reset',
|
|
141
167
|
debugTitle: 'Rescues so far',
|
|
142
168
|
debugSummary: function (c) {
|
|
143
169
|
return 'Saved ' + (c.retryOk + c.rescueOk) + ' (retries ' + c.retryOk + ', pushes ' + c.rescueOk + ')';
|
|
@@ -264,6 +290,7 @@ window.__ModuleLoader__.load({
|
|
|
264
290
|
var info = summary.data && summary.data.authCode;
|
|
265
291
|
var guard = summary.data && summary.data.guard;
|
|
266
292
|
var audit = summary.data && summary.data.audit;
|
|
293
|
+
var rule = summary.data && summary.data.rule;
|
|
267
294
|
|
|
268
295
|
var onSave = function () {
|
|
269
296
|
if (!draft.trim()) { setNote(t.emptyErr); return; }
|
|
@@ -287,6 +314,34 @@ window.__ModuleLoader__.load({
|
|
|
287
314
|
setNote((data.notes && data.notes[0]) || '');
|
|
288
315
|
});
|
|
289
316
|
};
|
|
317
|
+
var sourceState = useState('');
|
|
318
|
+
var sourceDraft = sourceState[0];
|
|
319
|
+
var setSourceDraft = sourceState[1];
|
|
320
|
+
var onSaveSource = function () {
|
|
321
|
+
setBusy('source');
|
|
322
|
+
setNote('');
|
|
323
|
+
post({ source: sourceDraft }).then(function (data) {
|
|
324
|
+
setBusy('');
|
|
325
|
+
if (data && data.notes && data.notes[0]) setNote(data.notes[0]);
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
var onResetSource = function () {
|
|
329
|
+
setSourceDraft('');
|
|
330
|
+
setBusy('source');
|
|
331
|
+
setNote('');
|
|
332
|
+
post({ source: '' }).then(function (data) {
|
|
333
|
+
setBusy('');
|
|
334
|
+
if (data && data.notes && data.notes[0]) setNote(data.notes[0]);
|
|
335
|
+
});
|
|
336
|
+
};
|
|
337
|
+
var onToggleRule = function (next) {
|
|
338
|
+
setBusy('rule');
|
|
339
|
+
setNote('');
|
|
340
|
+
post({ ruleEnabled: next }).then(function (data) {
|
|
341
|
+
setBusy('');
|
|
342
|
+
if (data && data.notes && data.notes[0]) setNote(data.notes[0]);
|
|
343
|
+
});
|
|
344
|
+
};
|
|
290
345
|
var onToggleGuard = function (next) {
|
|
291
346
|
setBusy('guard');
|
|
292
347
|
setNote('');
|
|
@@ -323,7 +378,7 @@ window.__ModuleLoader__.load({
|
|
|
323
378
|
disabled: busy === 'auth',
|
|
324
379
|
onChange: function (event) { setDraft(event.target.value); },
|
|
325
380
|
onKeyDown: function (event) { if (event.key === 'Enter') onSave(); },
|
|
326
|
-
style: { flex: '1 1
|
|
381
|
+
style: { flex: '1 1 300px', minWidth: '300px' }
|
|
327
382
|
});
|
|
328
383
|
|
|
329
384
|
var preview = null;
|
|
@@ -425,6 +480,60 @@ window.__ModuleLoader__.load({
|
|
|
425
480
|
)
|
|
426
481
|
: null,
|
|
427
482
|
|
|
483
|
+
// ---- 每轮对话推送(保存授权码时自动写进全局指令文件)----
|
|
484
|
+
h(
|
|
485
|
+
'div',
|
|
486
|
+
{ style: { marginTop: '14px', paddingTop: '12px', borderTop: '1px solid ' + V.border } },
|
|
487
|
+
row([
|
|
488
|
+
h('span', { style: { fontWeight: 600 } }, t.ruleTitle),
|
|
489
|
+
h('span', { style: { color: V.sub } }, rule && rule.enabled ? t.ruleOn : t.ruleOff),
|
|
490
|
+
h(Switch, {
|
|
491
|
+
checked: Boolean(rule && rule.enabled),
|
|
492
|
+
onChange: onToggleRule,
|
|
493
|
+
label: t.ruleTitle,
|
|
494
|
+
disabled: busy === 'rule' || !rule
|
|
495
|
+
})
|
|
496
|
+
]),
|
|
497
|
+
muted(
|
|
498
|
+
rule && rule.customRule ? t.ruleCustom : rule && rule.block ? t.ruleBlockPresent : t.ruleHint,
|
|
499
|
+
{ marginTop: '6px' }
|
|
500
|
+
)
|
|
501
|
+
),
|
|
502
|
+
|
|
503
|
+
// ---- 推送源名称:改一处,本机所有推送一起改 ----
|
|
504
|
+
h(
|
|
505
|
+
'div',
|
|
506
|
+
{ style: { marginTop: '14px', paddingTop: '12px', borderTop: '1px solid ' + V.border } },
|
|
507
|
+
row([
|
|
508
|
+
h('span', { style: { fontWeight: 600 } }, t.sourceTitle),
|
|
509
|
+
h(
|
|
510
|
+
'span',
|
|
511
|
+
{ style: { color: V.sub } },
|
|
512
|
+
t.sourceCurrent(String((summary.data && summary.data.source) || t.sourcePlaceholder))
|
|
513
|
+
)
|
|
514
|
+
]),
|
|
515
|
+
h(
|
|
516
|
+
'div',
|
|
517
|
+
{ style: { display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center', marginTop: '6px' } },
|
|
518
|
+
[
|
|
519
|
+
h(Input, {
|
|
520
|
+
type: 'text',
|
|
521
|
+
value: sourceDraft,
|
|
522
|
+
placeholder: t.sourcePlaceholder,
|
|
523
|
+
spellCheck: false,
|
|
524
|
+
autoComplete: 'off',
|
|
525
|
+
disabled: busy === 'source',
|
|
526
|
+
onChange: function (event) { setSourceDraft(event.target.value); },
|
|
527
|
+
onKeyDown: function (event) { if (event.key === 'Enter') onSaveSource(); },
|
|
528
|
+
style: { flex: '1 1 300px', minWidth: '300px' }
|
|
529
|
+
}),
|
|
530
|
+
h(Button, { variant: 'primary', size: 'sm', onClick: onSaveSource, disabled: busy === 'source' }, t.save),
|
|
531
|
+
h(Button, { variant: 'ghost', size: 'sm', onClick: onResetSource, disabled: busy === 'source' }, t.sourceReset)
|
|
532
|
+
]
|
|
533
|
+
),
|
|
534
|
+
muted(t.sourceHint, { marginTop: '6px' })
|
|
535
|
+
),
|
|
536
|
+
|
|
428
537
|
// ---- 推送兜底 + 调试图 ----
|
|
429
538
|
h(
|
|
430
539
|
'div',
|
|
@@ -461,7 +570,7 @@ window.__ModuleLoader__.load({
|
|
|
461
570
|
),
|
|
462
571
|
showDebug ? h('div', { style: { marginTop: '4px' } }, debugBody) : null
|
|
463
572
|
)
|
|
464
|
-
)
|
|
573
|
+
),
|
|
465
574
|
);
|
|
466
575
|
};
|
|
467
576
|
}
|
package/src/lib/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import { join } from "node:path";
|
|
|
34
34
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
35
35
|
import z from "@deepseek-ai/schemastery";
|
|
36
36
|
import { maskAuthCode, parseAuthCodeInput } from "./authcode.js";
|
|
37
|
+
import { ensureRule, removeRule, ruleStatus } from "./agents-rule.js";
|
|
37
38
|
import { configPath, readStore, writeStore } from "./config-store.js";
|
|
38
39
|
import { apply as applyPushGuard } from "./push-guard.js";
|
|
39
40
|
|
|
@@ -60,7 +61,19 @@ const DEFAULT_RESULT = "任务已完成";
|
|
|
60
61
|
* name); the platform accepts other values, but rendering is platform-defined,
|
|
61
62
|
* so check a real card after changing it.
|
|
62
63
|
*/
|
|
63
|
-
|
|
64
|
+
/**
|
|
65
|
+
* The HIBoard contract brands cards by this value — the phone renders it as the
|
|
66
|
+
* card's brand / tool name. It defaults to `"DeepSeek Harness"` (this project's
|
|
67
|
+
* branding; the official today-task skill and hwpush send `"OpenClaw"` for the
|
|
68
|
+
* same field, and the platform accepts either — rendering of a custom value is
|
|
69
|
+
* platform-defined, so look at a real card after changing it).
|
|
70
|
+
*
|
|
71
|
+
* Overridable via the `source` config option, which the settings card exposes as
|
|
72
|
+
* 「推送源名称」. Other local pushers (e.g. the startup/token card in
|
|
73
|
+
* `dsh-token-broadcast`) read this same value from the plugin's config file, so
|
|
74
|
+
* one edit moves the whole machine to one brand.
|
|
75
|
+
*/
|
|
76
|
+
const WIRE_SOURCE = "DeepSeek Harness";
|
|
64
77
|
|
|
65
78
|
/** Environment fallback for the authCode. */
|
|
66
79
|
const AUTH_CODE_ENV = "DSH_HIBOARD_AUTH_CODE";
|
|
@@ -74,6 +87,22 @@ const GUARD_STATE_DIR = join(homedir(), ".dsh", "lan", "push-guard");
|
|
|
74
87
|
/** 调试图最多回溯多少条审计(文件本身有大小上限,读全量没必要)。 */
|
|
75
88
|
const AUDIT_MAX_LINES = 4000;
|
|
76
89
|
|
|
90
|
+
/**
|
|
91
|
+
* dsh 的全局指令文件。`dsh-agent-instructions` 每个会话都加载它 ——
|
|
92
|
+
* 而且**只认这一个全局文件**:`.local` 叠加(`AGENTS.local.md`)只在**项目目录**的
|
|
93
|
+
* 祖先链里生效,全局那段代码写死了只探测 `AGENTS.md`。所以「每轮都推」这条规则
|
|
94
|
+
* 想对所有会话、所有项目生效,只能写在这里。
|
|
95
|
+
*/
|
|
96
|
+
const AGENTS_FILE = join(process.env.DSH_HOME || join(homedir(), ".dsh"), "AGENTS.md");
|
|
97
|
+
|
|
98
|
+
/** 规则写入的结果 → 卡片上的提示语。 */
|
|
99
|
+
const RULE_NOTE = {
|
|
100
|
+
added: "已把「每轮都要推送」写进全局指令文件 AGENTS.md,新会话即生效。",
|
|
101
|
+
updated: "已更新 AGENTS.md 里的推送规则。",
|
|
102
|
+
unchanged: "AGENTS.md 里的推送规则已是最新。",
|
|
103
|
+
"skipped-custom": "检测到你已手写过推送规则,未改动 AGENTS.md。"
|
|
104
|
+
};
|
|
105
|
+
|
|
77
106
|
/** Success codes accepted by the official client (string or numeric zero). */
|
|
78
107
|
const SUCCESS_CODES = new Set(["0000000000", "0"]);
|
|
79
108
|
|
|
@@ -118,7 +147,14 @@ const Config = z.object({
|
|
|
118
147
|
/** hiboard_push 没带有效 schedule_id 时自动补的值(为空则卡片点不开)。 */
|
|
119
148
|
defaultScheduleId: z.string().default("dsh_worklog"),
|
|
120
149
|
/** 设置页路由开关;关掉后 `/hiboard-push/config` 不注册,浏览器半边也停手。 */
|
|
121
|
-
settingsCard: z.boolean().default(true)
|
|
150
|
+
settingsCard: z.boolean().default(true),
|
|
151
|
+
/**
|
|
152
|
+
* 保存授权码时,是否顺带把「每轮都要推送」的规则写进全局指令文件
|
|
153
|
+
* (`$DSH_HOME/AGENTS.md` 里的一个标记块)。**默认开启** —— 这样一键安装后
|
|
154
|
+
* 只要粘一次授权码,规则就自动就位,不必再手改 AGENTS.md。
|
|
155
|
+
* 关掉它(卡片上的按钮)会把已写入的那一段一起移除。
|
|
156
|
+
*/
|
|
157
|
+
autoRule: z.boolean().default(true)
|
|
122
158
|
});
|
|
123
159
|
|
|
124
160
|
const entryDefaults = {
|
|
@@ -136,7 +172,8 @@ const entryDefaults = {
|
|
|
136
172
|
guardAmbiguousRetryCount: 1,
|
|
137
173
|
guardRetryDelayMs: 3000,
|
|
138
174
|
defaultScheduleId: "dsh_worklog",
|
|
139
|
-
settingsCard: true
|
|
175
|
+
settingsCard: true,
|
|
176
|
+
autoRule: true
|
|
140
177
|
};
|
|
141
178
|
|
|
142
179
|
// ---------------------------------------------------------------- payload
|
|
@@ -429,7 +466,7 @@ function hiboardVerifyTool({ resolveAuthCode, settings }) {
|
|
|
429
466
|
* 不继承 dsh Connection 的登录认证,本插件也不自己做 Host / Origin 策略。
|
|
430
467
|
* 部署时鉴权必须同时覆盖本路由与 dsh 本身;不想暴露就把 settingsCard 设为 false。
|
|
431
468
|
*/
|
|
432
|
-
function buildConfigRoute({ authCodeInfo, settings, storeFile }) {
|
|
469
|
+
function buildConfigRoute({ authCodeInfo, settings, storeFile, selfcheckReport }) {
|
|
433
470
|
const SOURCE_LABEL = {
|
|
434
471
|
settings: "设置页(本插件配置)",
|
|
435
472
|
entry: "profile 补丁层 hiboard-push.config.authCode",
|
|
@@ -532,9 +569,15 @@ function buildConfigRoute({ authCodeInfo, settings, storeFile }) {
|
|
|
532
569
|
defaultScheduleId: s.defaultScheduleId
|
|
533
570
|
},
|
|
534
571
|
endpoint: s.pushServiceUrl,
|
|
572
|
+
// 推送源名称:卡片上显示的"来源 / AI 工具名"
|
|
573
|
+
source: s.source,
|
|
574
|
+
// 自检工具是否真的注册上了(失败会被回显,不再静默)
|
|
575
|
+
selfcheck: selfcheckReport(),
|
|
535
576
|
storeFile,
|
|
536
577
|
// 调试图:兜底到底把多少次「原本失败的推送」救了回来
|
|
537
|
-
audit: auditSummary()
|
|
578
|
+
audit: auditSummary(),
|
|
579
|
+
// 常驻规则:是否开启、有没有已写入的块、有没有用户自己写的等价规则
|
|
580
|
+
rule: Object.assign(ruleStatus(AGENTS_FILE), { enabled: s.autoRule })
|
|
538
581
|
};
|
|
539
582
|
};
|
|
540
583
|
|
|
@@ -574,6 +617,10 @@ function buildConfigRoute({ authCodeInfo, settings, storeFile }) {
|
|
|
574
617
|
const body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
575
618
|
const patch = {};
|
|
576
619
|
const notes = [];
|
|
620
|
+
/** 本次请求是否真的写入了授权码(清除不算)——用它触发"顺带写规则"。 */
|
|
621
|
+
let authCodeTouched = false;
|
|
622
|
+
/** 卡片上的规则开关:true 写入 / false 移除 / undefined 不表态。 */
|
|
623
|
+
let ruleRequest;
|
|
577
624
|
|
|
578
625
|
// ---- 授权码:完整指令或裸码都行;空串表示"清除覆盖" ----
|
|
579
626
|
if (Object.prototype.hasOwnProperty.call(body, "authCode")) {
|
|
@@ -589,10 +636,17 @@ function buildConfigRoute({ authCodeInfo, settings, storeFile }) {
|
|
|
589
636
|
return;
|
|
590
637
|
}
|
|
591
638
|
patch.authCode = parsed.code;
|
|
639
|
+
authCodeTouched = true;
|
|
592
640
|
notes.push(`${parsed.note}(${maskAuthCode(parsed.code)})`);
|
|
593
641
|
}
|
|
594
642
|
}
|
|
595
643
|
|
|
644
|
+
// ---- 常驻规则开关 ----
|
|
645
|
+
if (Object.prototype.hasOwnProperty.call(body, "ruleEnabled")) {
|
|
646
|
+
patch.autoRule = body.ruleEnabled === true;
|
|
647
|
+
ruleRequest = body.ruleEnabled === true;
|
|
648
|
+
}
|
|
649
|
+
|
|
596
650
|
// ---- 兜底开关:布尔直接落盘,立即生效 ----
|
|
597
651
|
for (const key of ["guardEnabled", "guardRescueEnabled", "guardPromiseRescueEnabled", "guardRetryEnabled"]) {
|
|
598
652
|
if (Object.prototype.hasOwnProperty.call(body, key)) patch[key] = body[key] === true;
|
|
@@ -601,12 +655,40 @@ function buildConfigRoute({ authCodeInfo, settings, storeFile }) {
|
|
|
601
655
|
const text = String(body.defaultScheduleId ?? "").trim();
|
|
602
656
|
patch.defaultScheduleId = text || null;
|
|
603
657
|
}
|
|
658
|
+
// ---- 推送源名称(source):卡片上显示的"来源 / AI 工具名" ----
|
|
659
|
+
// 空串 → 清除覆盖,回退到默认 "DeepSeek Harness"
|
|
660
|
+
if (Object.prototype.hasOwnProperty.call(body, "source")) {
|
|
661
|
+
const text = String(body.source ?? "").trim();
|
|
662
|
+
patch.source = text || null;
|
|
663
|
+
notes.push(text ? `推送源名称已改为「${text}」,本机所有推送都会用它。` : "已恢复默认推送源名称(DeepSeek Harness)。");
|
|
664
|
+
}
|
|
604
665
|
|
|
605
666
|
if (Object.keys(patch).length === 0) {
|
|
606
667
|
send(400, { error: "没有可保存的字段" });
|
|
607
668
|
return;
|
|
608
669
|
}
|
|
609
670
|
writeStore(patch, storeFile);
|
|
671
|
+
|
|
672
|
+
// ---- 常规规则动作:放在落盘之后,读到的才是"最终"的 autoRule ----
|
|
673
|
+
// 保存授权码时**顺带**把「每轮都要推送」写进全局指令文件:用户此刻的意图
|
|
674
|
+
// 就是"让它开始工作",不该再要求他手改 AGENTS.md。默认开启,卡片可关。
|
|
675
|
+
try {
|
|
676
|
+
if (ruleRequest === false) {
|
|
677
|
+
const r = removeRule(AGENTS_FILE);
|
|
678
|
+
notes.push(r.action === "removed" ? "已移除全局指令文件里的推送规则。" : "全局推送规则本来就不存在,未改动。");
|
|
679
|
+
} else if (ruleRequest === true) {
|
|
680
|
+
const r = ensureRule(AGENTS_FILE);
|
|
681
|
+
notes.push(RULE_NOTE[r.action] ?? "已同步全局推送规则。");
|
|
682
|
+
} else if (authCodeTouched && settings().autoRule) {
|
|
683
|
+
const r = ensureRule(AGENTS_FILE);
|
|
684
|
+
// unchanged 不提示:每次保存都重复一句"已是最新"只是噪音
|
|
685
|
+
if (r.action !== "unchanged") notes.push(RULE_NOTE[r.action] ?? "已同步全局推送规则。");
|
|
686
|
+
}
|
|
687
|
+
} catch (error) {
|
|
688
|
+
// 规则写不进去不该让"授权码已保存"这件事看起来失败
|
|
689
|
+
notes.push(`授权码已保存,但写入全局规则失败:${String(error?.message ?? error)}`);
|
|
690
|
+
}
|
|
691
|
+
|
|
610
692
|
send(200, { ...summary(), notes });
|
|
611
693
|
} catch (error) {
|
|
612
694
|
send(400, { error: String(error?.message ?? error) });
|
|
@@ -645,7 +727,8 @@ function apply(ctx, rawConfig) {
|
|
|
645
727
|
guardAmbiguousRetryCount: Number(resolved.guardAmbiguousRetryCount) >= 0 ? Number(resolved.guardAmbiguousRetryCount) : 1,
|
|
646
728
|
guardRetryDelayMs: Number(resolved.guardRetryDelayMs) > 0 ? Number(resolved.guardRetryDelayMs) : 3000,
|
|
647
729
|
defaultScheduleId: String(resolved.defaultScheduleId ?? "").trim() || "dsh_worklog",
|
|
648
|
-
settingsCard: resolved.settingsCard !== false
|
|
730
|
+
settingsCard: resolved.settingsCard !== false,
|
|
731
|
+
autoRule: resolved.autoRule !== false
|
|
649
732
|
};
|
|
650
733
|
};
|
|
651
734
|
|
|
@@ -670,8 +753,13 @@ function apply(ctx, rawConfig) {
|
|
|
670
753
|
ctx.tools.register(hiboardPushTool({ resolveAuthCode, settings }));
|
|
671
754
|
ctx.tools.register(hiboardVerifyTool({ resolveAuthCode, settings }));
|
|
672
755
|
|
|
756
|
+
/** 自检工具的注册结果,由兜底回调写入(见下面的 selfcheckReport)。 */
|
|
757
|
+
let selfcheckStatus = { ok: null, error: "" };
|
|
758
|
+
|
|
673
759
|
// ---- 推送兜底(原 dsh-push-guard,现并入本包):默认开启,可实时关闭 ----
|
|
674
760
|
applyPushGuard(ctx, {
|
|
761
|
+
// 注册成败都要回显:失败原本只写了一条**看不见的** warn
|
|
762
|
+
onSelfcheckRegister: (result) => { selfcheckStatus = result; },
|
|
675
763
|
// 静态值:只有把 guardEnabled:false 写死在配置里,才会整个不加载
|
|
676
764
|
enabled: entry.guardEnabled !== false,
|
|
677
765
|
// 实时值:每次判定都重新读,设置页一关就立即停
|
|
@@ -685,6 +773,8 @@ function apply(ctx, rawConfig) {
|
|
|
685
773
|
// 让兜底走"实时解析"而不是把挂载时的值钉死,
|
|
686
774
|
// 否则设置页里新存的授权码要等重启才对兜底生效。
|
|
687
775
|
resolveAuthCode,
|
|
776
|
+
// 同理实时解析:设置页改了「推送源名称」,兜底补推的卡片也要立刻跟着变
|
|
777
|
+
resolveSource: () => settings().source,
|
|
688
778
|
pushServiceUrl: settings().pushServiceUrl,
|
|
689
779
|
maxContentLength: settings().maxContentLength,
|
|
690
780
|
defaultResult: settings().defaultResult,
|
|
@@ -694,12 +784,23 @@ function apply(ctx, rawConfig) {
|
|
|
694
784
|
source: settings().source
|
|
695
785
|
});
|
|
696
786
|
|
|
787
|
+
/**
|
|
788
|
+
* 自检工具的注册状态(**回显给设置页**)。
|
|
789
|
+
* `registered` 取实时查询 —— 即便注册被延迟或撤下也能如实反映;
|
|
790
|
+
* `error` 是注册时捕获到的原因。没有它,注册失败就只剩一条无处可见的 warn:
|
|
791
|
+
* 本机 `~/.dsh/logs` 下没有该进程的日志文件(2026-10-13 验收报告第 5 节)。
|
|
792
|
+
*/
|
|
793
|
+
const selfcheckReport = () => ({
|
|
794
|
+
registered: Boolean(ctx.tools?.get?.("hiboard_push_selfcheck")),
|
|
795
|
+
error: selfcheckStatus.error || ""
|
|
796
|
+
});
|
|
797
|
+
|
|
697
798
|
// ---- 设置页路由 ----
|
|
698
799
|
// webServer 只在 web profile 存在,而 cordis 没有"可选注入"的写法,
|
|
699
800
|
// 所以走一次 scoped inject:服务不在时闭包根本不执行,插件照常加载。
|
|
700
801
|
if (settings().settingsCard && typeof ctx.inject === "function") {
|
|
701
802
|
ctx.inject(["webServer"], (scope) => {
|
|
702
|
-
const route = buildConfigRoute({ authCodeInfo, settings, storeFile });
|
|
803
|
+
const route = buildConfigRoute({ authCodeInfo, settings, storeFile, selfcheckReport });
|
|
703
804
|
// 挂在 effect 上:插件被关掉时路由一起撤下,否则它会继续为一个
|
|
704
805
|
// 已关闭的插件读写配置(modsearch 踩过这个坑,见其 dsh/index.js 注释)。
|
|
705
806
|
scope.effect(() => scope.webServer.register(route), "hiboard-push: settings route");
|
package/src/lib/push-guard.js
CHANGED
|
@@ -83,7 +83,7 @@ const DEFAULTS = {
|
|
|
83
83
|
retryCount: 3,
|
|
84
84
|
ambiguousRetryCount: 1,
|
|
85
85
|
defaultResult: "任务已完成",
|
|
86
|
-
source: "
|
|
86
|
+
source: "DeepSeek Harness",
|
|
87
87
|
defaultScheduleId: "dsh_worklog",
|
|
88
88
|
stateDir: "",
|
|
89
89
|
minPromiseLength: 120,
|
|
@@ -123,7 +123,8 @@ export const Config = z.object({
|
|
|
123
123
|
defaultResult: z.string().default(DEFAULTS.defaultResult),
|
|
124
124
|
/**
|
|
125
125
|
* 卡片 `source` 字段 —— 就是负一屏上那个**来源 / AI 工具名**的品牌化渲染值。
|
|
126
|
-
* 默认 `"
|
|
126
|
+
* 默认 `"DeepSeek Harness"`(本项目品牌)。官方 `today-task` skill 与 `hwpush` 在同一
|
|
127
|
+
* 字段上发的是 `"OpenClaw"`,平台两者都接受。可在设置卡片「推送源名称」里改。
|
|
127
128
|
* 实测平台**接受**任意值(自定义值同样返回 `0000000000`),但渲染样式由平台决定,
|
|
128
129
|
* 换值后请先在真机上看一眼卡片再长期使用。
|
|
129
130
|
*/
|
|
@@ -488,7 +489,13 @@ export function apply(ctx, config) {
|
|
|
488
489
|
).trim();
|
|
489
490
|
const pushUrl = String(settings.pushServiceUrl || DEFAULTS.pushServiceUrl);
|
|
490
491
|
const maxLen = Number(settings.maxContentLength) > 0 ? Number(settings.maxContentLength) : DEFAULTS.maxContentLength;
|
|
491
|
-
|
|
492
|
+
/**
|
|
493
|
+
* 卡片来源(负一屏上的"来源 / AI 工具名")**实时解析**:设置页改一次,
|
|
494
|
+
* 之后**所有**卡片(含兜底补推的那张)都用新值,而不是钉在挂载那一刻。
|
|
495
|
+
*/
|
|
496
|
+
const resolveCardSource = () =>
|
|
497
|
+
String((typeof settings.resolveSource === "function" ? settings.resolveSource() : settings.source) ?? "").trim() ||
|
|
498
|
+
DEFAULTS.source;
|
|
492
499
|
const retryBudget = Math.max(0, Number(settings.retryCount) >= 0 ? Number(settings.retryCount) : DEFAULTS.retryCount);
|
|
493
500
|
const ambiguousBudget = Math.max(0, Number(settings.ambiguousRetryCount) >= 0 ? Number(settings.ambiguousRetryCount) : DEFAULTS.ambiguousRetryCount);
|
|
494
501
|
const stateDir = String(settings.stateDir || "").trim() || join(homedir(), ".dsh", "lan", "push-guard");
|
|
@@ -514,7 +521,7 @@ export function apply(ctx, config) {
|
|
|
514
521
|
retryEnabled: settings.retryEnabled !== false,
|
|
515
522
|
retryCount: retryBudget,
|
|
516
523
|
ambiguousRetryCount: ambiguousBudget,
|
|
517
|
-
source:
|
|
524
|
+
source: resolveCardSource(),
|
|
518
525
|
defaultScheduleId: String(settings.defaultScheduleId || DEFAULTS.defaultScheduleId),
|
|
519
526
|
stateDir
|
|
520
527
|
};
|
|
@@ -522,7 +529,7 @@ export function apply(ctx, config) {
|
|
|
522
529
|
ctx.logger?.info?.(
|
|
523
530
|
"[push-guard] 启动自检:authCode=%s;L1 抢救=%s,L2 承诺兜底=%s,L3 重试=%s(确定未送达 %d 次 / 可能已送达 %d 次);卡片来源=%s;schedule_id 兜底=%s;状态目录=%s",
|
|
524
531
|
startup.authCode, startup.rescueEnabled, startup.promiseRescueEnabled, startup.retryEnabled,
|
|
525
|
-
retryBudget, ambiguousBudget,
|
|
532
|
+
retryBudget, ambiguousBudget, resolveCardSource(), startup.defaultScheduleId, stateDir
|
|
526
533
|
);
|
|
527
534
|
if (startup.authCode === "MISSING") {
|
|
528
535
|
ctx.logger?.warn?.("[push-guard] ⚠️ 未配置 authCode,任何兜底推送都会失败(请检查 hiboard-push / push-guard 配置)");
|
|
@@ -618,7 +625,7 @@ export function apply(ctx, config) {
|
|
|
618
625
|
// 兜底卡片同样必须是"能点开"的:schedule_id 为空时用默认分组
|
|
619
626
|
defaultScheduleId: String(settings.defaultScheduleId || DEFAULTS.defaultScheduleId),
|
|
620
627
|
// 兜底卡片与正常卡片用同一个来源名(负一屏上显示的 AI 工具名)
|
|
621
|
-
source:
|
|
628
|
+
source: resolveCardSource(),
|
|
622
629
|
stateDir,
|
|
623
630
|
audit
|
|
624
631
|
});
|
|
@@ -776,16 +783,26 @@ export function apply(ctx, config) {
|
|
|
776
783
|
schema: {
|
|
777
784
|
type: "object",
|
|
778
785
|
additionalProperties: false,
|
|
786
|
+
// ⚠️ `required` 必须写在**对象层**,不能写进每个属性节点。
|
|
787
|
+
// 本工具没有走 defineTool()(那个会把 DSL 编译掉),而是把裸对象直接
|
|
788
|
+
// 交给 ctx.tools.register(),于是要过 dsh-tools 的
|
|
789
|
+
// assertSupportedJsonSchema —— 它只允许 required 出现在 type:"object" 上。
|
|
790
|
+
// 写成属性级 `required: true` 会抛 JsonSchemaError,而被下面的 catch 吞掉,
|
|
791
|
+
// 表现为"自检工具根本不存在"且毫无提示(2026-10-13 验收实测)。
|
|
792
|
+
required: [
|
|
793
|
+
"guardActive", "hooked", "authCode", "source", "retryBudget",
|
|
794
|
+
"endpoint", "endpointReachable", "lastState", "auditTail"
|
|
795
|
+
],
|
|
779
796
|
properties: {
|
|
780
|
-
guardActive: { type: "boolean"
|
|
781
|
-
hooked: { type: "boolean"
|
|
782
|
-
authCode: { type: "string"
|
|
783
|
-
source: { type: "string"
|
|
784
|
-
retryBudget: { type: "string"
|
|
785
|
-
endpoint: { type: "string"
|
|
786
|
-
endpointReachable: { type: "boolean"
|
|
787
|
-
lastState: { type: "string"
|
|
788
|
-
auditTail: { type: "string"
|
|
797
|
+
guardActive: { type: "boolean" },
|
|
798
|
+
hooked: { type: "boolean" },
|
|
799
|
+
authCode: { type: "string" },
|
|
800
|
+
source: { type: "string" },
|
|
801
|
+
retryBudget: { type: "string" },
|
|
802
|
+
endpoint: { type: "string" },
|
|
803
|
+
endpointReachable: { type: "boolean" },
|
|
804
|
+
lastState: { type: "string" },
|
|
805
|
+
auditTail: { type: "string" }
|
|
789
806
|
}
|
|
790
807
|
},
|
|
791
808
|
render(_args, value) {
|
|
@@ -832,7 +849,7 @@ export function apply(ctx, config) {
|
|
|
832
849
|
guardActive: !guardOff(),
|
|
833
850
|
hooked,
|
|
834
851
|
authCode: resolveAuthCode() ? "已配置" : "未配置(推送会失败)",
|
|
835
|
-
source:
|
|
852
|
+
source: resolveCardSource(),
|
|
836
853
|
retryBudget: `确定未送达 ${retryBudget} 次 / 可能已送达 ${ambiguousBudget} 次`,
|
|
837
854
|
endpoint: pushUrl,
|
|
838
855
|
endpointReachable: reachable,
|
|
@@ -841,10 +858,24 @@ export function apply(ctx, config) {
|
|
|
841
858
|
};
|
|
842
859
|
}
|
|
843
860
|
});
|
|
861
|
+
try {
|
|
862
|
+
settings.onSelfcheckRegister?.({ ok: true, error: "" });
|
|
863
|
+
} catch {
|
|
864
|
+
/* 上报失败不影响工具本身 */
|
|
865
|
+
}
|
|
844
866
|
return () => off();
|
|
845
867
|
}, "push-guard: self-check tool");
|
|
846
868
|
} catch (error) {
|
|
847
|
-
|
|
869
|
+
const message = String(error?.message ?? error);
|
|
870
|
+
ctx.logger?.warn?.("[push-guard] 注册自检工具失败:%s", message);
|
|
871
|
+
// **不再静默**:把失败回显给宿主,设置页会在 /hiboard-push/config 里报
|
|
872
|
+
// selfcheck.registered=false。本机 ~/.dsh/logs 下没有该进程的日志文件,warn
|
|
873
|
+
// 是无处可见的 —— 2026-10-13 的验收就是因为这一点完全无感(报告第 5 节)。
|
|
874
|
+
try {
|
|
875
|
+
settings.onSelfcheckRegister?.({ ok: false, error: message });
|
|
876
|
+
} catch {
|
|
877
|
+
/* 上报失败不该影响其它功能 */
|
|
878
|
+
}
|
|
848
879
|
}
|
|
849
880
|
}
|
|
850
881
|
|