dsh-harmonyos-hiboard 0.6.0 → 0.7.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 +9 -1
- package/package.json +2 -2
- package/src/lib/client.js +198 -205
- package/src/lib/index.js +74 -1
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
[](#)
|
|
7
7
|
[](#)
|
|
8
8
|
[](#)
|
|
9
|
+
[](https://www.npmjs.com/package/dsh-harmonyos-hiboard)
|
|
9
10
|
[](https://dsh-plugin.org/plugins/maybemeibemaybi/dsh-harmonyos-hiboard)
|
|
10
11
|
|
|
11
12
|
把 **DeepSeek Harness(dsh)** 的对话与任务结果,以 **服务卡片** 形式推送到华为鸿蒙
|
|
@@ -117,10 +118,17 @@ TypeError: Cannot read properties of undefined (reading 'validate')
|
|
|
117
118
|
前置:Windows / macOS / Linux + Node.js 18+ + dsh 已安装 + `pnpm` 已安装。
|
|
118
119
|
|
|
119
120
|
```bash
|
|
120
|
-
# 1) 安装为 dsh
|
|
121
|
+
# 1) 安装为 dsh 插件
|
|
122
|
+
# 推荐走 npm:预构建安装,免掉 allowBuilds 构建授权那一步
|
|
123
|
+
dsh plugin --profile web add dsh-harmonyos-hiboard
|
|
124
|
+
|
|
125
|
+
# 或者从 GitHub 安装
|
|
121
126
|
dsh plugin --profile web add github:MaybeMeibeMaybi/dsh-harmonyos-hiboard
|
|
122
127
|
```
|
|
123
128
|
|
|
129
|
+
> 两者装的是同一份代码。npm 上的是本仓库的构建产物,`repository` 字段指回本仓库;
|
|
130
|
+
> 想装某个具体版本就写 `dsh-harmonyos-hiboard@0.6.0`。
|
|
131
|
+
|
|
124
132
|
<details>
|
|
125
133
|
<summary>或者从本地克隆安装(改源码 / 调试时用)</summary>
|
|
126
134
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-harmonyos-hiboard",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
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.",
|
|
5
5
|
"author": "Jim Chen",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
package/src/lib/client.js
CHANGED
|
@@ -1,33 +1,36 @@
|
|
|
1
1
|
// dsh-harmonyos-hiboard 的浏览器半边:插件设置卡片。
|
|
2
2
|
//
|
|
3
|
-
// dsh 不会从设置 schema
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// dsh 不会从设置 schema 自动生成表单,所以卡片必须**作为插槽内容贡献**:
|
|
4
|
+
// 从 dsh 0.1.6-alpha.2 起挂在 `plugins.bundle.config`(主界面侧栏「插件」面板里
|
|
5
|
+
// 该 bundle 自己的页面上),更早的版本挂在设置页的 `settings.plugin.item`。
|
|
6
|
+
// 两个都挂,宿主声明哪个就填哪个。
|
|
7
7
|
//
|
|
8
|
-
// 手写在 lazy-CJS bundle 协议里(window.__ModuleLoader__.load + 返回 cordis
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// 手写在 lazy-CJS bundle 协议里(window.__ModuleLoader__.load + 返回 cordis 插件
|
|
9
|
+
// 导出的 factory),所以**没有构建步骤**。
|
|
10
|
+
//
|
|
11
|
+
// 控件一律用官方原语(Button / Switch / Input):它们的配色由
|
|
12
|
+
// `--dsw-alias-button-*` 等主题令牌驱动,深浅色主题都自动正确。
|
|
13
|
+
// 第一版这些控件是手搓样式(自己拼 background / color),这正是它们在深色模式下
|
|
14
|
+
// 不可读的原因 —— 所以这里不再自己决定控件配色。
|
|
11
15
|
//
|
|
12
16
|
// 它读写宿主半边的 `/hiboard-push/config`:授权码存进插件自己的配置文件,
|
|
13
|
-
//
|
|
17
|
+
// 浏览器永远拿不到已保存的明文(只拿打码值与来源)。
|
|
14
18
|
window.__ModuleLoader__.load({
|
|
15
19
|
id: 'dsh-harmonyos-hiboard',
|
|
16
20
|
factory: (require) => {
|
|
17
21
|
var module = { exports: {} };
|
|
18
22
|
var exports = module.exports;
|
|
19
23
|
|
|
20
|
-
//
|
|
24
|
+
// 「插件」面板按 npm 包名分发某个 bundle 的配置,这个 key 必须与包名一致。
|
|
21
25
|
var PACKAGE_NAME = 'dsh-harmonyos-hiboard';
|
|
22
26
|
var ROUTE = '/hiboard-push/config';
|
|
23
27
|
|
|
24
28
|
// 授权码的字符集与长度区间 —— 与宿主半边 src/lib/authcode.js 同一套规则。
|
|
25
29
|
// 客户端 bundle 无法 import 宿主模块,所以这里是**刻意的副本**:
|
|
26
|
-
//
|
|
30
|
+
// 两边规则必须一起改,仓库里有对齐测试盯着(test/authcode.test.mjs)。
|
|
27
31
|
var CODE_PATTERN = /^[A-Za-z0-9_-]{6,128}$/;
|
|
28
32
|
var COMMAND_HINTS = /(openclaw|config\s+set|skills\.entries|today-task|authCode)/i;
|
|
29
33
|
|
|
30
|
-
// 解析用户输入:整条关联指令或裸授权码都认(取指令的最后一个词)。
|
|
31
34
|
function parseAuthCodeInput(raw) {
|
|
32
35
|
var original = typeof raw === 'string' ? raw : '';
|
|
33
36
|
if (!original.trim()) return { ok: false, reason: 'empty' };
|
|
@@ -45,26 +48,26 @@ window.__ModuleLoader__.load({
|
|
|
45
48
|
var tokens = text.split(/\s+/).filter(Boolean);
|
|
46
49
|
var last = tokens[tokens.length - 1].replace(/^[`'"]+/, '').replace(/[`'"]+$/, '');
|
|
47
50
|
if (!CODE_PATTERN.test(last)) return { ok: false, reason: 'invalid' };
|
|
48
|
-
return {
|
|
49
|
-
ok: true,
|
|
50
|
-
code: last,
|
|
51
|
-
shape: 'command',
|
|
52
|
-
// 不像常见指令时也照取最后一个词,但让界面说清楚这是"猜"的
|
|
53
|
-
guessed: !COMMAND_HINTS.test(text)
|
|
54
|
-
};
|
|
51
|
+
return { ok: true, code: last, shape: 'command', guessed: !COMMAND_HINTS.test(text) };
|
|
55
52
|
}
|
|
56
53
|
|
|
57
54
|
function maskAuthCode(code) {
|
|
58
55
|
var s = typeof code === 'string' ? code : '';
|
|
59
56
|
if (!s) return '';
|
|
60
57
|
if (s.length <= 4) return new Array(s.length + 1).join('•');
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
return new Array(Math.min(s.length - 4, 12) + 1).join('•') + s.slice(-4);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 审计时间是 ISO 串;面板只想要「最近几次」,显示到分钟就够。
|
|
62
|
+
function shortTime(iso) {
|
|
63
|
+
var s = String(iso || '');
|
|
64
|
+
if (s.length < 16) return s;
|
|
65
|
+
return s.slice(5, 10).replace('-', '/') + ' ' + s.slice(11, 16);
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
// ------------------------------------------------------------ 文案
|
|
66
|
-
// 两组短标签而不是一套 locale bundle
|
|
67
|
-
//
|
|
69
|
+
// 两组短标签而不是一套 locale bundle:卡片上就这些字,为它们装一套
|
|
70
|
+
// 本地化机制比它标注的东西还重。
|
|
68
71
|
var TEXT = {
|
|
69
72
|
zh: {
|
|
70
73
|
section: '推送设置',
|
|
@@ -74,21 +77,32 @@ window.__ModuleLoader__.load({
|
|
|
74
77
|
source: '当前生效来源',
|
|
75
78
|
placeholder: '粘贴授权码,或整条关联指令…',
|
|
76
79
|
save: '保存',
|
|
77
|
-
saving: '保存中…',
|
|
78
80
|
clear: '清除',
|
|
79
81
|
commandHint:
|
|
80
|
-
'
|
|
82
|
+
'也可以直接粘贴整条关联指令,例如 openclaw config set skills.entries.today-task.config.authCode <授权码>,会自动从末尾取出授权码。',
|
|
81
83
|
willExtract: '将从这条指令中取出授权码:',
|
|
82
84
|
bareOk: '将按裸授权码保存:',
|
|
83
85
|
invalid: '这串内容不像授权码(只允许字母、数字、下划线、连字符,6–128 位)。',
|
|
84
86
|
emptyErr: '请先填入授权码或整条关联指令。',
|
|
85
87
|
whereHint:
|
|
86
|
-
'授权码获取:手机 负一屏 → 我的 → 动态管理 → 关联账号 → Claw
|
|
87
|
-
guardTitle: '
|
|
88
|
+
'授权码获取:手机 负一屏 → 我的 → 动态管理 → 关联账号 → Claw 智能体。不存进浏览器,字段保存后清空。',
|
|
89
|
+
guardTitle: '推送兜底',
|
|
88
90
|
guardOn: '已开启',
|
|
89
91
|
guardOff: '已关闭',
|
|
90
|
-
guardHint:
|
|
91
|
-
|
|
92
|
+
guardHint: '默认开启,建议保持开启。模型漏调用或调用失败时,它替你补上,卡片就不会悄悄丢掉。',
|
|
93
|
+
guardWhat:
|
|
94
|
+
'① 模型把调用写成了纯文本 → 替你补推 ② 模型只说“推给你”却没真调用 → 替你补推 ③ 调用失败 → 按错误类型重试',
|
|
95
|
+
guardBudget: function (g) {
|
|
96
|
+
return '重试预算:确定未送达 ' + g.retryCount + ' 次 / 可能已送达 ' + g.ambiguousRetryCount + ' 次 · schedule_id 兜底:' + g.defaultScheduleId;
|
|
97
|
+
},
|
|
98
|
+
debugTitle: '兜底战绩',
|
|
99
|
+
debugSummary: function (c) {
|
|
100
|
+
return '已救回 ' + (c.retryOk + c.rescueOk) + ' 次(重试 ' + c.retryOk + ' · 补推 ' + c.rescueOk + ')';
|
|
101
|
+
},
|
|
102
|
+
debugEmpty: '还没有需要兜底的记录 —— 每次推送都成功调用了工具。',
|
|
103
|
+
debugNone: '兜底还没有运行记录。',
|
|
104
|
+
kindRetry: '重试',
|
|
105
|
+
kindRescue: '补推',
|
|
92
106
|
loadFailed: '读取配置失败',
|
|
93
107
|
saveFailed: '保存失败',
|
|
94
108
|
unavailable: '设置路由不可用(宿主未提供)。'
|
|
@@ -101,29 +115,39 @@ window.__ModuleLoader__.load({
|
|
|
101
115
|
source: 'Effective source',
|
|
102
116
|
placeholder: 'Paste the authCode, or the whole linking command…',
|
|
103
117
|
save: 'Save',
|
|
104
|
-
saving: 'saving…',
|
|
105
118
|
clear: 'Clear',
|
|
106
119
|
commandHint:
|
|
107
|
-
'You may paste the whole linking command, e.g. openclaw config set skills.entries.today-task.config.authCode <code
|
|
120
|
+
'You may also paste the whole linking command, e.g. openclaw config set skills.entries.today-task.config.authCode <code>; the code is taken from the end.',
|
|
108
121
|
willExtract: 'Auth code taken from that command: ',
|
|
109
122
|
bareOk: 'Will be saved as a bare auth code: ',
|
|
110
123
|
invalid: 'That does not look like an auth code (letters, digits, underscore, hyphen; 6–128).',
|
|
111
124
|
emptyErr: 'Enter an auth code or the whole linking command first.',
|
|
112
125
|
whereHint:
|
|
113
|
-
'Get the code on the phone: 负一屏 → 我的 → 动态管理 → 关联账号 → Claw 智能体.
|
|
126
|
+
'Get the code on the phone: 负一屏 → 我的 → 动态管理 → 关联账号 → Claw 智能体. Never stored in the browser; the field clears after saving.',
|
|
114
127
|
guardTitle: 'Push safety net',
|
|
115
128
|
guardOn: 'on',
|
|
116
129
|
guardOff: 'off',
|
|
117
|
-
guardHint:
|
|
118
|
-
|
|
130
|
+
guardHint: 'On by default and best left on. When the model misses the call or a call fails, it steps in so a card is not silently lost.',
|
|
131
|
+
guardWhat:
|
|
132
|
+
'1) the model wrote the call as plain text → it pushes for you 2) the model promised a push but never called → it pushes for you 3) a call failed → it retries by error type',
|
|
133
|
+
guardBudget: function (g) {
|
|
134
|
+
return 'Retry budget: definitely-undelivered ' + g.retryCount + ' / possibly-delivered ' + g.ambiguousRetryCount + ' · schedule_id fallback: ' + g.defaultScheduleId;
|
|
135
|
+
},
|
|
136
|
+
debugTitle: 'Rescues so far',
|
|
137
|
+
debugSummary: function (c) {
|
|
138
|
+
return 'Saved ' + (c.retryOk + c.rescueOk) + ' (retries ' + c.retryOk + ', pushes ' + c.rescueOk + ')';
|
|
139
|
+
},
|
|
140
|
+
debugEmpty: 'Nothing needed rescuing yet — every push called the tool successfully.',
|
|
141
|
+
debugNone: 'The safety net has no run records yet.',
|
|
142
|
+
kindRetry: 'retry',
|
|
143
|
+
kindRescue: 'rescue',
|
|
119
144
|
loadFailed: 'Could not load the configuration',
|
|
120
145
|
saveFailed: 'Could not save',
|
|
121
146
|
unavailable: 'The settings route is unavailable (host did not provide it).'
|
|
122
147
|
}
|
|
123
148
|
};
|
|
124
149
|
|
|
125
|
-
|
|
126
|
-
// 其次看页面 lang,最后看浏览器语言。
|
|
150
|
+
/** dsh 的语言:优先问 locale 服务(可能晚于卡片就绪,故在渲染时读),其次页面 lang。 */
|
|
127
151
|
function pickLang(localeRef) {
|
|
128
152
|
try {
|
|
129
153
|
var locale = localeRef && localeRef.current;
|
|
@@ -144,28 +168,27 @@ window.__ModuleLoader__.load({
|
|
|
144
168
|
}
|
|
145
169
|
|
|
146
170
|
// ------------------------------------------------------------ 卡片
|
|
147
|
-
|
|
148
|
-
// 少一个需要跟随宿主版本变化的接口面。这些变量在 dsh web 的主题里都有定义,
|
|
149
|
-
// 取不到时用中性回退值,卡片在别的宿主上也不至于看不清。
|
|
150
|
-
function Card(react, localeRef) {
|
|
171
|
+
function Card(react, ui, localeRef) {
|
|
151
172
|
var h = react.createElement;
|
|
152
173
|
var useState = react.useState;
|
|
153
174
|
var useEffect = react.useEffect;
|
|
175
|
+
var Button = ui.Button;
|
|
176
|
+
var Switch = ui.Switch;
|
|
177
|
+
var Input = ui.Input;
|
|
154
178
|
|
|
179
|
+
// 只留排版用的中性令牌:**控件配色全部交给官方原语**,免得深浅色各错一次。
|
|
155
180
|
var V = {
|
|
156
|
-
label: 'var(--dsw-alias-label-primary, currentColor)',
|
|
157
181
|
sub: 'var(--dsw-alias-label-tertiary, rgba(127,127,127,0.9))',
|
|
158
182
|
border: 'var(--dsw-alias-border-l2, rgba(127,127,127,0.28))',
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
danger: 'var(--dsw-alias-label-error, #d9534f)'
|
|
183
|
+
danger: 'var(--dsw-alias-label-error, #d9534f)',
|
|
184
|
+
ok: 'var(--dsw-alias-label-success, #3ba55d)'
|
|
162
185
|
};
|
|
163
186
|
|
|
164
187
|
return function HiboardConfigCard() {
|
|
165
188
|
var t = TEXT[pickLang(localeRef)];
|
|
166
|
-
var
|
|
167
|
-
var summary =
|
|
168
|
-
var setSummary =
|
|
189
|
+
var state = useState({ loading: true, data: null, error: '' });
|
|
190
|
+
var summary = state[0];
|
|
191
|
+
var setSummary = state[1];
|
|
169
192
|
var draftState = useState('');
|
|
170
193
|
var draft = draftState[0];
|
|
171
194
|
var setDraft = draftState[1];
|
|
@@ -175,6 +198,9 @@ window.__ModuleLoader__.load({
|
|
|
175
198
|
var noteState = useState('');
|
|
176
199
|
var note = noteState[0];
|
|
177
200
|
var setNote = noteState[1];
|
|
201
|
+
var showDebugState = useState(true);
|
|
202
|
+
var showDebug = showDebugState[0];
|
|
203
|
+
var setShowDebug = showDebugState[1];
|
|
178
204
|
|
|
179
205
|
var refresh = function () {
|
|
180
206
|
return fetch(ROUTE, { headers: { accept: 'application/json' } })
|
|
@@ -182,26 +208,25 @@ window.__ModuleLoader__.load({
|
|
|
182
208
|
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
183
209
|
return response.json();
|
|
184
210
|
})
|
|
185
|
-
.then(function (data) {
|
|
186
|
-
setSummary({ loading: false, data: data, error: '' });
|
|
187
|
-
})
|
|
211
|
+
.then(function (data) { setSummary({ loading: false, data: data, error: '' }); })
|
|
188
212
|
.catch(function (error) {
|
|
189
213
|
setSummary({ loading: false, data: null, error: String((error && error.message) || error) });
|
|
190
214
|
});
|
|
191
215
|
};
|
|
192
216
|
|
|
193
217
|
useEffect(function () {
|
|
194
|
-
// 路由不存在(settingsCard: false,或非 web profile
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
218
|
+
// 路由不存在(settingsCard: false,或非 web profile)时只提示,不假装卡片生效
|
|
219
|
+
fetch(ROUTE, { method: 'GET' })
|
|
220
|
+
.then(function (response) {
|
|
221
|
+
if (response.status === 404) {
|
|
222
|
+
setSummary({ loading: false, data: null, error: t.unavailable });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
refresh();
|
|
226
|
+
})
|
|
227
|
+
.catch(function (error) {
|
|
228
|
+
setSummary({ loading: false, data: null, error: String((error && error.message) || error) });
|
|
229
|
+
});
|
|
205
230
|
}, []);
|
|
206
231
|
|
|
207
232
|
var post = function (body) {
|
|
@@ -216,10 +241,7 @@ window.__ModuleLoader__.load({
|
|
|
216
241
|
return data;
|
|
217
242
|
});
|
|
218
243
|
})
|
|
219
|
-
.then(function (data) {
|
|
220
|
-
setSummary({ loading: false, data: data, error: '' });
|
|
221
|
-
return data;
|
|
222
|
-
})
|
|
244
|
+
.then(function (data) { setSummary({ loading: false, data: data, error: '' }); return data; })
|
|
223
245
|
.catch(function (error) {
|
|
224
246
|
setSummary(function (prev) {
|
|
225
247
|
return { loading: false, data: prev.data, error: String((error && error.message) || error) };
|
|
@@ -228,18 +250,14 @@ window.__ModuleLoader__.load({
|
|
|
228
250
|
});
|
|
229
251
|
};
|
|
230
252
|
|
|
231
|
-
// 输入预览:保存前就告诉用户"会存进去什么",避免把整段说明当授权码存了
|
|
232
253
|
var parsed = draft.trim() ? parseAuthCodeInput(draft) : null;
|
|
254
|
+
var info = summary.data && summary.data.authCode;
|
|
255
|
+
var guard = summary.data && summary.data.guard;
|
|
256
|
+
var audit = summary.data && summary.data.audit;
|
|
233
257
|
|
|
234
258
|
var onSave = function () {
|
|
235
|
-
if (!draft.trim()) {
|
|
236
|
-
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
if (parsed && !parsed.ok) {
|
|
240
|
-
setNote(t.invalid);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
259
|
+
if (!draft.trim()) { setNote(t.emptyErr); return; }
|
|
260
|
+
if (parsed && !parsed.ok) { setNote(t.invalid); return; }
|
|
243
261
|
setBusy('auth');
|
|
244
262
|
setNote('');
|
|
245
263
|
post({ authCode: draft }).then(function (data) {
|
|
@@ -249,7 +267,6 @@ window.__ModuleLoader__.load({
|
|
|
249
267
|
setNote((data.notes && data.notes[0]) || '');
|
|
250
268
|
});
|
|
251
269
|
};
|
|
252
|
-
|
|
253
270
|
var onClear = function () {
|
|
254
271
|
setBusy('auth');
|
|
255
272
|
setNote('');
|
|
@@ -260,38 +277,34 @@ window.__ModuleLoader__.load({
|
|
|
260
277
|
setNote((data.notes && data.notes[0]) || '');
|
|
261
278
|
});
|
|
262
279
|
};
|
|
263
|
-
|
|
264
|
-
var onToggleGuard = function () {
|
|
265
|
-
var next = !(summary.data && summary.data.guard && summary.data.guard.enabled);
|
|
280
|
+
var onToggleGuard = function (next) {
|
|
266
281
|
setBusy('guard');
|
|
267
282
|
setNote('');
|
|
268
|
-
post({ guardEnabled: next }).then(function () {
|
|
269
|
-
setBusy('');
|
|
270
|
-
});
|
|
283
|
+
post({ guardEnabled: next }).then(function () { setBusy(''); });
|
|
271
284
|
};
|
|
272
285
|
|
|
273
|
-
var info = summary.data && summary.data.authCode;
|
|
274
|
-
var guard = summary.data && summary.data.guard;
|
|
275
|
-
|
|
276
286
|
var row = function (children, extra) {
|
|
277
287
|
return h(
|
|
278
288
|
'div',
|
|
279
|
-
{ style: Object.assign({ display: 'flex', alignItems: 'center', gap: '8px' }, extra || {}) },
|
|
289
|
+
{ style: Object.assign({ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }, extra || {}) },
|
|
280
290
|
children
|
|
281
291
|
);
|
|
282
292
|
};
|
|
293
|
+
var muted = function (text, extra) {
|
|
294
|
+
return h('div', { style: Object.assign({ fontSize: '12px', lineHeight: '1.7', color: V.sub }, extra || {}) }, text);
|
|
295
|
+
};
|
|
296
|
+
var heading = function (text) {
|
|
297
|
+
return h('div', { style: { fontWeight: 600, marginBottom: '6px' } }, text);
|
|
298
|
+
};
|
|
283
299
|
|
|
284
300
|
var statusDot = h('span', {
|
|
285
301
|
style: {
|
|
286
|
-
width: '8px',
|
|
287
|
-
|
|
288
|
-
borderRadius: '50%',
|
|
289
|
-
flex: 'none',
|
|
290
|
-
background: info && info.configured ? '#3ba55d' : V.danger
|
|
302
|
+
width: '8px', height: '8px', borderRadius: '50%', flex: 'none',
|
|
303
|
+
background: info && info.configured ? V.ok : V.danger
|
|
291
304
|
}
|
|
292
305
|
});
|
|
293
306
|
|
|
294
|
-
var input = h(
|
|
307
|
+
var input = h(Input, {
|
|
295
308
|
type: 'text',
|
|
296
309
|
value: draft,
|
|
297
310
|
placeholder: t.placeholder,
|
|
@@ -300,89 +313,37 @@ window.__ModuleLoader__.load({
|
|
|
300
313
|
disabled: busy === 'auth',
|
|
301
314
|
onChange: function (event) { setDraft(event.target.value); },
|
|
302
315
|
onKeyDown: function (event) { if (event.key === 'Enter') onSave(); },
|
|
303
|
-
style: {
|
|
304
|
-
flex: '1 1 auto',
|
|
305
|
-
minWidth: '0',
|
|
306
|
-
padding: '6px 8px',
|
|
307
|
-
fontSize: '13px',
|
|
308
|
-
fontFamily: 'inherit',
|
|
309
|
-
color: V.label,
|
|
310
|
-
background: V.bg,
|
|
311
|
-
border: '1px solid ' + V.border,
|
|
312
|
-
borderRadius: '6px',
|
|
313
|
-
outline: 'none'
|
|
314
|
-
}
|
|
316
|
+
style: { flex: '1 1 220px', minWidth: '0' }
|
|
315
317
|
});
|
|
316
318
|
|
|
317
|
-
var btn = function (label, onClick, opts) {
|
|
318
|
-
var o = opts || {};
|
|
319
|
-
return h(
|
|
320
|
-
'button',
|
|
321
|
-
{
|
|
322
|
-
type: 'button',
|
|
323
|
-
onClick: onClick,
|
|
324
|
-
disabled: Boolean(busy) || Boolean(o.disabled),
|
|
325
|
-
style: {
|
|
326
|
-
flex: 'none',
|
|
327
|
-
padding: '6px 12px',
|
|
328
|
-
fontSize: '13px',
|
|
329
|
-
fontFamily: 'inherit',
|
|
330
|
-
cursor: busy ? 'default' : 'pointer',
|
|
331
|
-
borderRadius: '6px',
|
|
332
|
-
border: o.primary ? '1px solid transparent' : '1px solid ' + V.border,
|
|
333
|
-
background: o.primary ? V.accent : 'transparent',
|
|
334
|
-
color: o.primary ? '#fff' : V.label,
|
|
335
|
-
opacity: busy || o.disabled ? 0.6 : 1
|
|
336
|
-
}
|
|
337
|
-
},
|
|
338
|
-
label
|
|
339
|
-
);
|
|
340
|
-
};
|
|
341
|
-
|
|
342
319
|
var preview = null;
|
|
343
320
|
if (parsed && parsed.ok) {
|
|
344
|
-
preview =
|
|
345
|
-
'div',
|
|
346
|
-
{ style: { marginTop: '6px', fontSize: '12px', color: V.sub } },
|
|
347
|
-
(parsed.shape === 'command' ? t.willExtract : t.bareOk) + maskAuthCode(parsed.code)
|
|
348
|
-
);
|
|
321
|
+
preview = muted((parsed.shape === 'command' ? t.willExtract : t.bareOk) + maskAuthCode(parsed.code), { marginTop: '6px' });
|
|
349
322
|
} else if (parsed && !parsed.ok) {
|
|
350
323
|
preview = h('div', { style: { marginTop: '6px', fontSize: '12px', color: V.danger } }, t.invalid);
|
|
351
324
|
}
|
|
352
325
|
|
|
353
|
-
//
|
|
354
|
-
var
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
},
|
|
376
|
-
h('span', {
|
|
377
|
-
style: {
|
|
378
|
-
width: '16px',
|
|
379
|
-
height: '16px',
|
|
380
|
-
borderRadius: '50%',
|
|
381
|
-
background: '#fff',
|
|
382
|
-
boxShadow: '0 1px 2px rgba(0,0,0,.3)'
|
|
383
|
-
}
|
|
384
|
-
})
|
|
385
|
-
);
|
|
326
|
+
// ---- 兜底战绩:把「原本没推出去、但兜底救回来了」摆出来 ----
|
|
327
|
+
var debugBody;
|
|
328
|
+
if (!audit) {
|
|
329
|
+
debugBody = muted(t.debugNone);
|
|
330
|
+
} else if (!audit.exists || audit.counts.retryOk + audit.counts.rescueOk === 0) {
|
|
331
|
+
debugBody = muted(t.debugEmpty);
|
|
332
|
+
} 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
|
+
);
|
|
346
|
+
}
|
|
386
347
|
|
|
387
348
|
return h(
|
|
388
349
|
'div',
|
|
@@ -392,7 +353,6 @@ window.__ModuleLoader__.load({
|
|
|
392
353
|
padding: '12px',
|
|
393
354
|
border: '1px solid ' + V.border,
|
|
394
355
|
borderRadius: '12px',
|
|
395
|
-
color: V.label,
|
|
396
356
|
fontSize: '13px',
|
|
397
357
|
lineHeight: '1.6'
|
|
398
358
|
}
|
|
@@ -400,47 +360,75 @@ window.__ModuleLoader__.load({
|
|
|
400
360
|
h('div', { style: { fontWeight: 600, marginBottom: '10px' } }, t.section),
|
|
401
361
|
|
|
402
362
|
// ---- 授权码 ----
|
|
403
|
-
|
|
363
|
+
heading(t.authTitle),
|
|
364
|
+
row(
|
|
365
|
+
[
|
|
366
|
+
statusDot,
|
|
367
|
+
h(
|
|
368
|
+
'span',
|
|
369
|
+
{ style: { color: V.sub } },
|
|
370
|
+
(info && info.configured ? t.configured : t.missing) +
|
|
371
|
+
(info && info.configured && info.masked ? ' · ' + info.masked : '')
|
|
372
|
+
),
|
|
373
|
+
info && info.sourceLabel
|
|
374
|
+
? h('span', { style: { color: V.sub } }, '· ' + t.source + ': ' + info.sourceLabel)
|
|
375
|
+
: null
|
|
376
|
+
],
|
|
377
|
+
{ marginBottom: '8px' }
|
|
378
|
+
),
|
|
404
379
|
row([
|
|
405
|
-
|
|
406
|
-
h(
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
(info && info.configured ? t.configured : t.missing) +
|
|
410
|
-
(info && info.configured && info.masked ? ' · ' + info.masked : '')
|
|
411
|
-
),
|
|
412
|
-
info && info.sourceLabel
|
|
413
|
-
? h('span', { style: { color: V.sub } }, '· ' + t.source + ': ' + info.sourceLabel)
|
|
414
|
-
: null
|
|
415
|
-
], { marginBottom: '8px' }),
|
|
416
|
-
|
|
417
|
-
row([input, btn(t.save, onSave, { primary: true }), btn(t.clear, onClear, { disabled: !(info && info.configured) })]),
|
|
380
|
+
input,
|
|
381
|
+
h(Button, { variant: 'primary', size: 'sm', onClick: onSave, disabled: busy === 'auth' }, t.save),
|
|
382
|
+
h(Button, { variant: 'outline', size: 'sm', onClick: onClear, disabled: busy === 'auth' || !(info && info.configured) }, t.clear)
|
|
383
|
+
]),
|
|
418
384
|
preview,
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
note ? h('div', { style: { marginTop: '8px', fontSize: '12px', color: V.accent } }, note) : null,
|
|
385
|
+
muted(t.commandHint, { marginTop: '8px' }),
|
|
386
|
+
muted(t.whereHint, { marginTop: '4px' }),
|
|
387
|
+
note ? h('div', { style: { marginTop: '8px', fontSize: '12px' } }, note) : null,
|
|
423
388
|
summary.error
|
|
424
|
-
? h(
|
|
425
|
-
|
|
389
|
+
? h(
|
|
390
|
+
'div',
|
|
391
|
+
{ style: { marginTop: '8px', fontSize: '12px', color: V.danger } },
|
|
392
|
+
(summary.data ? t.saveFailed : t.loadFailed) + ': ' + summary.error
|
|
393
|
+
)
|
|
426
394
|
: null,
|
|
427
395
|
|
|
428
|
-
// ----
|
|
429
|
-
h(
|
|
430
|
-
|
|
431
|
-
|
|
396
|
+
// ---- 推送兜底 + 调试图 ----
|
|
397
|
+
h(
|
|
398
|
+
'div',
|
|
399
|
+
{ style: { marginTop: '14px', paddingTop: '12px', borderTop: '1px solid ' + V.border } },
|
|
432
400
|
row([
|
|
433
401
|
h('span', { style: { fontWeight: 600 } }, t.guardTitle),
|
|
434
402
|
h('span', { style: { color: V.sub } }, guard && guard.enabled ? t.guardOn : t.guardOff),
|
|
435
|
-
|
|
403
|
+
h(Switch, {
|
|
404
|
+
checked: Boolean(guard && guard.enabled),
|
|
405
|
+
onChange: onToggleGuard,
|
|
406
|
+
label: t.guardTitle,
|
|
407
|
+
disabled: busy === 'guard' || !guard
|
|
408
|
+
})
|
|
436
409
|
]),
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
:
|
|
410
|
+
muted(t.guardHint, { marginTop: '6px' }),
|
|
411
|
+
muted(t.guardWhat, { marginTop: '4px' }),
|
|
412
|
+
guard ? muted(t.guardBudget(guard), { marginTop: '4px' }) : null,
|
|
413
|
+
|
|
414
|
+
h(
|
|
415
|
+
'div',
|
|
416
|
+
{ style: { marginTop: '10px' } },
|
|
417
|
+
h(
|
|
418
|
+
'div',
|
|
419
|
+
{
|
|
420
|
+
role: 'button',
|
|
421
|
+
tabIndex: 0,
|
|
422
|
+
onClick: function () { setShowDebug(!showDebug); },
|
|
423
|
+
onKeyDown: function (event) {
|
|
424
|
+
if (event.key === 'Enter' || event.key === ' ') setShowDebug(!showDebug);
|
|
425
|
+
},
|
|
426
|
+
style: { fontSize: '12px', fontWeight: 600, cursor: 'pointer', userSelect: 'none' }
|
|
427
|
+
},
|
|
428
|
+
(showDebug ? '▾ ' : '▸ ') + t.debugTitle
|
|
429
|
+
),
|
|
430
|
+
showDebug ? h('div', { style: { marginTop: '4px' } }, debugBody) : null
|
|
431
|
+
)
|
|
444
432
|
)
|
|
445
433
|
);
|
|
446
434
|
};
|
|
@@ -456,9 +444,7 @@ window.__ModuleLoader__.load({
|
|
|
456
444
|
ctx.inject(['locale'], function (scope) {
|
|
457
445
|
localeRef.current = scope.locale;
|
|
458
446
|
if (typeof scope.effect === 'function') {
|
|
459
|
-
scope.effect(function () {
|
|
460
|
-
return function () { localeRef.current = null; };
|
|
461
|
-
}, 'hiboard-push: locale handle');
|
|
447
|
+
scope.effect(function () { return function () { localeRef.current = null; }; }, 'hiboard-push: locale handle');
|
|
462
448
|
}
|
|
463
449
|
});
|
|
464
450
|
|
|
@@ -479,9 +465,16 @@ window.__ModuleLoader__.load({
|
|
|
479
465
|
console.error('[hiboard-push] settings card skipped: ' + error);
|
|
480
466
|
return;
|
|
481
467
|
}
|
|
482
|
-
var
|
|
483
|
-
|
|
484
|
-
|
|
468
|
+
var ui;
|
|
469
|
+
try {
|
|
470
|
+
ui = require('@deepseek-ai/dsh-client-ui-primitives');
|
|
471
|
+
} catch (error) {
|
|
472
|
+
// 拿不到原语就不渲染:宁可不显示,也不要退回手搓样式再错一次深浅色
|
|
473
|
+
console.error('[hiboard-push] settings card skipped (primitives unavailable): ' + error);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
var ConfigCard = Card(react, ui, localeRef);
|
|
477
|
+
// slots.inject 会等到插槽被声明为止,所以每个宿主只填它有的那个
|
|
485
478
|
ctx.slots.inject('plugins.bundle.config', function* () {
|
|
486
479
|
yield ctx.slots.register({ name: 'plugins.bundle.config', key: PACKAGE_NAME }, ConfigCard);
|
|
487
480
|
});
|
|
@@ -499,7 +492,7 @@ window.__ModuleLoader__.load({
|
|
|
499
492
|
|
|
500
493
|
exports.apply = apply;
|
|
501
494
|
// 仅供仓库测试使用,不属于插件契约。
|
|
502
|
-
exports.__card = { parseAuthCodeInput: parseAuthCodeInput, maskAuthCode: maskAuthCode, TEXT: TEXT };
|
|
495
|
+
exports.__card = { parseAuthCodeInput: parseAuthCodeInput, maskAuthCode: maskAuthCode, TEXT: TEXT, shortTime: shortTime };
|
|
503
496
|
// slots 与 locale 都是可选的,各自走 scoped inject,所以这里不声明依赖。
|
|
504
497
|
exports.inject = [];
|
|
505
498
|
return module.exports;
|
package/src/lib/index.js
CHANGED
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
* Node built-in `fetch`.
|
|
29
29
|
*/
|
|
30
30
|
import { randomUUID } from "node:crypto";
|
|
31
|
+
import { readFileSync } from "node:fs";
|
|
32
|
+
import { homedir } from "node:os";
|
|
33
|
+
import { join } from "node:path";
|
|
31
34
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
32
35
|
import z from "@deepseek-ai/schemastery";
|
|
33
36
|
import { maskAuthCode, parseAuthCodeInput } from "./authcode.js";
|
|
@@ -62,6 +65,15 @@ const WIRE_SOURCE = "OpenClaw";
|
|
|
62
65
|
/** Environment fallback for the authCode. */
|
|
63
66
|
const AUTH_CODE_ENV = "DSH_HIBOARD_AUTH_CODE";
|
|
64
67
|
|
|
68
|
+
/**
|
|
69
|
+
* 兜底的审计/状态目录。**在这里算一次并显式传给兜底**,而不是两边各写一份默认值 ——
|
|
70
|
+
* 设置页要读同一个目录来统计「原本失败、但兜底救回来」的次数,路径必须唯一。
|
|
71
|
+
*/
|
|
72
|
+
const GUARD_STATE_DIR = join(homedir(), ".dsh", "lan", "push-guard");
|
|
73
|
+
|
|
74
|
+
/** 调试图最多回溯多少条审计(文件本身有大小上限,读全量没必要)。 */
|
|
75
|
+
const AUDIT_MAX_LINES = 4000;
|
|
76
|
+
|
|
65
77
|
/** Success codes accepted by the official client (string or numeric zero). */
|
|
66
78
|
const SUCCESS_CODES = new Set(["0000000000", "0"]);
|
|
67
79
|
|
|
@@ -424,6 +436,63 @@ function buildConfigRoute({ authCodeInfo, settings, storeFile }) {
|
|
|
424
436
|
env: `环境变量 ${AUTH_CODE_ENV}`,
|
|
425
437
|
none: "未配置"
|
|
426
438
|
};
|
|
439
|
+
/**
|
|
440
|
+
* 调试图的数据源:读兜底的审计流水,统计**「原本没推出去、但兜底救回来了」**的次数,
|
|
441
|
+
* 并列出最近几次。这正是兜底存在的意义 —— 不列出来,用户只能靠猜它到底干没干活。
|
|
442
|
+
*
|
|
443
|
+
* 两类算「救回来」:
|
|
444
|
+
* retry-ok 工具真的调用了但失败,按错误类别重试后成功(L3)
|
|
445
|
+
* rescue-ok 根本没调用工具(写成纯文本 / 只承诺没做),兜底代为补推(L1 / L2)
|
|
446
|
+
*/
|
|
447
|
+
const auditSummary = () => {
|
|
448
|
+
const file = join(GUARD_STATE_DIR, "audit.jsonl");
|
|
449
|
+
const empty = { file, exists: false, counts: { retryOk: 0, rescueOk: 0, failed: 0 }, recent: [] };
|
|
450
|
+
let text;
|
|
451
|
+
try {
|
|
452
|
+
text = readFileSync(file, "utf8");
|
|
453
|
+
} catch {
|
|
454
|
+
// 还没跑过任何一轮:这是正常状态,不是错误
|
|
455
|
+
return empty;
|
|
456
|
+
}
|
|
457
|
+
const lines = text.split("\n").filter(Boolean).slice(-AUDIT_MAX_LINES);
|
|
458
|
+
const counts = { retryOk: 0, rescueOk: 0, failed: 0 };
|
|
459
|
+
const recent = [];
|
|
460
|
+
for (const line of lines) {
|
|
461
|
+
let row;
|
|
462
|
+
try {
|
|
463
|
+
row = JSON.parse(line);
|
|
464
|
+
} catch {
|
|
465
|
+
// 半行(正在写)或坏行:跳过。一条坏记录不该毁掉整块面板。
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (row?.kind === "retry-ok") {
|
|
469
|
+
counts.retryOk += 1;
|
|
470
|
+
recent.push({
|
|
471
|
+
// kind 必须带上:客户端靠它决定显示「重试」还是「补推」
|
|
472
|
+
kind: "retry-ok",
|
|
473
|
+
at: row.at ?? "",
|
|
474
|
+
level: "",
|
|
475
|
+
label: row.taskName ?? "",
|
|
476
|
+
detail: `首次失败后重试第 ${row.tries ?? "?"} 次成功`
|
|
477
|
+
});
|
|
478
|
+
} else if (row?.kind === "rescue-ok") {
|
|
479
|
+
counts.rescueOk += 1;
|
|
480
|
+
recent.push({
|
|
481
|
+
kind: "rescue-ok",
|
|
482
|
+
at: row.at ?? "",
|
|
483
|
+
level: row.level ?? "",
|
|
484
|
+
label: row.name ?? "",
|
|
485
|
+
detail: row.level === "L1" ? "模型把调用写成了纯文本" : "模型承诺了推送却没调用工具"
|
|
486
|
+
});
|
|
487
|
+
} else if (row?.kind === "push-failed" || row?.kind === "rescue-failed" || row?.kind === "rescue-error") {
|
|
488
|
+
counts.failed += 1;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// 文件是追加写的,末尾最新;面板要「最近几次」,所以倒序后取前几条
|
|
492
|
+
recent.reverse();
|
|
493
|
+
return { file, exists: true, counts, recent: recent.slice(0, 5) };
|
|
494
|
+
};
|
|
495
|
+
|
|
427
496
|
const summary = () => {
|
|
428
497
|
const info = authCodeInfo();
|
|
429
498
|
const s = settings();
|
|
@@ -444,7 +513,9 @@ function buildConfigRoute({ authCodeInfo, settings, storeFile }) {
|
|
|
444
513
|
defaultScheduleId: s.defaultScheduleId
|
|
445
514
|
},
|
|
446
515
|
endpoint: s.pushServiceUrl,
|
|
447
|
-
storeFile
|
|
516
|
+
storeFile,
|
|
517
|
+
// 调试图:兜底到底把多少次「原本失败的推送」救了回来
|
|
518
|
+
audit: auditSummary()
|
|
448
519
|
};
|
|
449
520
|
};
|
|
450
521
|
|
|
@@ -599,6 +670,8 @@ function apply(ctx, rawConfig) {
|
|
|
599
670
|
maxContentLength: settings().maxContentLength,
|
|
600
671
|
defaultResult: settings().defaultResult,
|
|
601
672
|
defaultScheduleId: settings().defaultScheduleId,
|
|
673
|
+
// 与设置页的调试图读同一个目录,路径只此一处定义
|
|
674
|
+
stateDir: GUARD_STATE_DIR,
|
|
602
675
|
source: settings().source
|
|
603
676
|
});
|
|
604
677
|
|