@cowwo/dsh-lookout 0.1.2 → 0.5.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 +45 -9
- package/client/client.js +2837 -1084
- package/lib/icon/index.js +54 -0
- package/lib/icon/workspace-prefs.js +129 -0
- package/lib/index.js +29 -3
- package/lib/notify/config.js +119 -153
- package/lib/notify/index.js +47 -112
- package/lib/notify/render.js +4 -18
- package/lib/notify/send/index.js +10 -13
- package/lib/notify/service.js +4 -16
- package/lib/notify/test-send.js +5 -8
- package/lib/notify/typert.host.js +2 -24
- package/lib/typert.host.js +89 -23
- package/package.json +1 -1
- package/lib/notify/state.js +0 -102
package/lib/typert.host.js
CHANGED
|
@@ -116,11 +116,41 @@ const setRunningRuleRequestSchema = z.object({
|
|
|
116
116
|
headerRing: z.boolean().optional()
|
|
117
117
|
})
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* 工作区偏好(标签 / 颜色 / 置顶 / 隐藏)。
|
|
121
|
+
*
|
|
122
|
+
* 与图标 RPC 分开的一对端点:偏好文件坏了最多丢标签,不能拖累 favicon、红点
|
|
123
|
+
* 和进行中黄点。文档整份读写(客户端每次提交完整文档,宿主整份覆盖)。
|
|
124
|
+
*/
|
|
125
|
+
const workspacePrefsEntrySchema = z.object({
|
|
126
|
+
// 按 Unicode 字符数(不是 UTF-16 长度)限 6:6 个 emoji 是合法标签。
|
|
127
|
+
tag: z.string().refine((value) => [...value].length <= 6, { message: 'tag 最多 6 个字符' }),
|
|
128
|
+
color: z.enum(['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'purple', 'gray']),
|
|
129
|
+
pinned: z.boolean(),
|
|
130
|
+
hidden: z.boolean(),
|
|
131
|
+
pinAnchor: z.string().nullable()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const workspacePrefsSchema = z.object({
|
|
135
|
+
version: z.literal(1),
|
|
136
|
+
workspaces: z.record(z.string().min(1).max(200), workspacePrefsEntrySchema)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* set 的请求 schema 故意放宽到"只验形状":端点自己会用 normalize 把非法 tag /
|
|
141
|
+
* 颜色 / 布尔值降级成默认值,所以旧客户端或手改的文档不会在网关层被整份拒绝,
|
|
142
|
+
* 从而丢掉本来还能用的那些工作区。
|
|
143
|
+
*/
|
|
144
|
+
const setWorkspacePrefsRequestSchema = z.object({
|
|
145
|
+
version: z.number().int().optional(),
|
|
146
|
+
workspaces: z.record(z.string(), z.unknown())
|
|
147
|
+
})
|
|
148
|
+
|
|
119
149
|
const setIconResultSchema = statusSchema
|
|
120
150
|
|
|
121
151
|
|
|
122
152
|
|
|
123
|
-
/**
|
|
153
|
+
/** 一个通道这一次的成败(多通道扇出后,要看得出是哪个通道)。 */
|
|
124
154
|
let channelSchema$value;
|
|
125
155
|
const channelSchema = () => (channelSchema$value ??= z.object({
|
|
126
156
|
transport: z.string(),
|
|
@@ -142,13 +172,6 @@ const deliverySchema = () => (deliverySchema$value ??= z.object({
|
|
|
142
172
|
channels: z.array(channelSchema()),
|
|
143
173
|
}));
|
|
144
174
|
|
|
145
|
-
/** 账本快照。 */
|
|
146
|
-
let stateSchema$value;
|
|
147
|
-
const stateSchema = () => (stateSchema$value ??= z.object({
|
|
148
|
-
counts: z.object({ sent: z.number(), failed: z.number() }),
|
|
149
|
-
recent: z.array(deliverySchema()),
|
|
150
|
-
}));
|
|
151
|
-
|
|
152
175
|
|
|
153
176
|
|
|
154
177
|
/** 合并后的 service 模型:A 的 iconCustom + B 的 mailNotify(B 的 model.services 为空)。 */
|
|
@@ -250,13 +273,31 @@ const services = [
|
|
|
250
273
|
summary: "Set the running-mark switch.",
|
|
251
274
|
jsDoc: "/** Set the running-mark switch. */",
|
|
252
275
|
tags: []
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
kind: 'method',
|
|
279
|
+
name: 'getWorkspacePrefs',
|
|
280
|
+
signature: 'async getWorkspacePrefs(): Promise<{ version: 1; workspaces: Record<string, { tag: string; color: string; pinned: boolean; hidden: boolean; pinAnchor: string | null }> }>',
|
|
281
|
+
description: "Read the per-workspace preferences (tag, color, pinned, hidden).",
|
|
282
|
+
summary: "Read workspace preferences.",
|
|
283
|
+
jsDoc: "/** Read workspace preferences. */",
|
|
284
|
+
tags: []
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
kind: 'method',
|
|
288
|
+
name: 'setWorkspacePrefs',
|
|
289
|
+
signature: 'async setWorkspacePrefs(request: { version?: number; workspaces: Record<string, unknown> }): Promise<{ version: 1; workspaces: Record<string, { tag: string; color: string; pinned: boolean; hidden: boolean; pinAnchor: string | null }> }>',
|
|
290
|
+
description: "Replace the whole workspace-preferences document (dirty fields degrade to defaults).",
|
|
291
|
+
summary: "Set workspace preferences.",
|
|
292
|
+
jsDoc: "/** Set workspace preferences. */",
|
|
293
|
+
tags: []
|
|
253
294
|
}
|
|
254
295
|
],
|
|
255
296
|
types: []
|
|
256
297
|
}
|
|
257
298
|
]
|
|
258
299
|
|
|
259
|
-
/** 合并后的宿主端点:A 的
|
|
300
|
+
/** 合并后的宿主端点:A 的 12 条 iconCustom/* + B 的 1 条 mailNotify/*。 */
|
|
260
301
|
const invocations = [
|
|
261
302
|
{
|
|
262
303
|
id: 'dsh-lookout#iconCustom/getStatus',
|
|
@@ -439,6 +480,45 @@ const invocations = [
|
|
|
439
480
|
},
|
|
440
481
|
sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
|
|
441
482
|
},
|
|
483
|
+
{
|
|
484
|
+
id: 'dsh-lookout#iconCustom/getWorkspacePrefs',
|
|
485
|
+
service: 'iconCustom',
|
|
486
|
+
namespace: 'iconCustom',
|
|
487
|
+
method: 'getWorkspacePrefs',
|
|
488
|
+
invocation: { kind: 'direct' },
|
|
489
|
+
parameters: [],
|
|
490
|
+
result: {
|
|
491
|
+
mode: 'strict',
|
|
492
|
+
typeSymbol: 'dsh-lookout#iconCustom/getWorkspacePrefs:result',
|
|
493
|
+
create: () => workspacePrefsSchema
|
|
494
|
+
},
|
|
495
|
+
sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
id: 'dsh-lookout#iconCustom/setWorkspacePrefs',
|
|
499
|
+
service: 'iconCustom',
|
|
500
|
+
namespace: 'iconCustom',
|
|
501
|
+
method: 'setWorkspacePrefs',
|
|
502
|
+
invocation: { kind: 'direct' },
|
|
503
|
+
parameters: [
|
|
504
|
+
{
|
|
505
|
+
name: 'request',
|
|
506
|
+
wire: 'request',
|
|
507
|
+
source: 'json',
|
|
508
|
+
codec: {
|
|
509
|
+
mode: 'strict',
|
|
510
|
+
typeSymbol: 'dsh-lookout#iconCustom/setWorkspacePrefs:request',
|
|
511
|
+
create: () => setWorkspacePrefsRequestSchema
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
],
|
|
515
|
+
result: {
|
|
516
|
+
mode: 'strict',
|
|
517
|
+
typeSymbol: 'dsh-lookout#iconCustom/setWorkspacePrefs:result',
|
|
518
|
+
create: () => workspacePrefsSchema
|
|
519
|
+
},
|
|
520
|
+
sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
|
|
521
|
+
},
|
|
442
522
|
{
|
|
443
523
|
id: 'dsh-lookout#iconCustom/setUnreadSeen',
|
|
444
524
|
service: 'iconCustom',
|
|
@@ -478,20 +558,6 @@ const invocations = [
|
|
|
478
558
|
},
|
|
479
559
|
sourceLocation: { file: 'lib/service.js', line: 1, column: 1 },
|
|
480
560
|
},
|
|
481
|
-
{
|
|
482
|
-
id: 'dsh-lookout#mailNotify/state',
|
|
483
|
-
service: 'mailNotify',
|
|
484
|
-
namespace: 'mailNotify',
|
|
485
|
-
method: 'state',
|
|
486
|
-
invocation: { kind: 'direct' },
|
|
487
|
-
parameters: [],
|
|
488
|
-
result: {
|
|
489
|
-
mode: 'strict',
|
|
490
|
-
typeSymbol: 'dsh-lookout#mailNotify/state:result',
|
|
491
|
-
create: stateSchema,
|
|
492
|
-
},
|
|
493
|
-
sourceLocation: { file: 'lib/service.js', line: 1, column: 1 },
|
|
494
|
-
},
|
|
495
561
|
]
|
|
496
562
|
|
|
497
563
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cowwo/dsh-lookout",
|
|
3
3
|
"description": "瞭望哨:盯着你的每个会话——谁跑完了、谁出错了、谁卡住了、谁在等你审批,用浏览器图标角标、红点/黄点和邮件通知你;顺带显示当前会话 ID。· Lookout: watches every session — who finished, who errored, who is stuck, and who is waiting for your approval — and notifies you through the browser favicon badge, red/yellow dots, and email, while also showing the current session ID.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|
package/lib/notify/state.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
// dsh-notify-p · 投递账本(落盘,用于"到底发没发出去"的可验证性)
|
|
2
|
-
//
|
|
3
|
-
// 为什么需要它:cordis 的控制台 exporter 默认只输出 error 级,
|
|
4
|
-
// 插件的 info/warn 日志**不会**出现在 dsh 的 stdout 里——
|
|
5
|
-
// 所以"看日志确认"是不可行的。这里把每次投递结果落成一个 JSON 文件,
|
|
6
|
-
// 出问题时 `cat $DSH_HOME/dsh-notify-p/state.json` 一眼就能看到。
|
|
7
|
-
//
|
|
8
|
-
// 只写真值,不写密钥、不写正文。
|
|
9
|
-
|
|
10
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
-
import { homedir } from 'node:os';
|
|
12
|
-
import { dirname, join } from 'node:path';
|
|
13
|
-
|
|
14
|
-
/** 账本保留的最近投递条数。 */
|
|
15
|
-
const RECENT_LIMIT = 50;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 解析账本路径:配置优先,其次 $DSH_HOME/dsh-notify-p/state.json,
|
|
19
|
-
* 最后回退 ~/.dsh/dsh-notify-p/state.json。
|
|
20
|
-
* @param {string} [configured] 配置里的 statePath
|
|
21
|
-
* @returns {string} 绝对路径
|
|
22
|
-
*/
|
|
23
|
-
export function resolveStatePath(configured) {
|
|
24
|
-
if (typeof configured === 'string' && configured.trim() !== '') return configured.trim();
|
|
25
|
-
const home = typeof process.env.DSH_HOME === 'string' && process.env.DSH_HOME !== ''
|
|
26
|
-
? process.env.DSH_HOME
|
|
27
|
-
: join(homedir(), '.dsh');
|
|
28
|
-
return join(home, 'dsh-notify-p', 'state.json');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 建一个投递账本。
|
|
33
|
-
* @param {object} [options] 参数
|
|
34
|
-
* @param {string} [options.path] 账本路径
|
|
35
|
-
* @param {object} [options.logger] 日志器
|
|
36
|
-
* @returns {{ record: (entry: object) => void, snapshot: () => object, flush: () => void, path: string }}
|
|
37
|
-
*/
|
|
38
|
-
export function createLedger(options = {}) {
|
|
39
|
-
const logger = options.logger ?? {};
|
|
40
|
-
const path = resolveStatePath(options.path);
|
|
41
|
-
const startedAt = new Date().toISOString();
|
|
42
|
-
/** @type {object[]} */
|
|
43
|
-
const recent = [];
|
|
44
|
-
const counts = { sent: 0, failed: 0 };
|
|
45
|
-
|
|
46
|
-
const persist = () => {
|
|
47
|
-
const payload = {
|
|
48
|
-
version: 1,
|
|
49
|
-
startedAt,
|
|
50
|
-
updatedAt: new Date().toISOString(),
|
|
51
|
-
counts,
|
|
52
|
-
recent,
|
|
53
|
-
};
|
|
54
|
-
try {
|
|
55
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
56
|
-
writeFileSync(path, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
57
|
-
} catch (error) {
|
|
58
|
-
// 账本写不进去不能影响发信本身
|
|
59
|
-
logger.debug?.(`dsh-notify-p: 账本写入失败 / ledger write failed: ${String(error)}`);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// 进程一起来就先落一个文件,避免"文件不存在"被误读成"插件没加载"
|
|
64
|
-
persist();
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
path,
|
|
68
|
-
/**
|
|
69
|
-
* 记一条投递结果。
|
|
70
|
-
* @param {object} entry 结果(at/ok/outcome/sessionId/subject/error/attempts)
|
|
71
|
-
* @returns {void}
|
|
72
|
-
*/
|
|
73
|
-
record(entry) {
|
|
74
|
-
if (entry && entry.ok === true) counts.sent += 1;
|
|
75
|
-
else counts.failed += 1;
|
|
76
|
-
recent.push(entry);
|
|
77
|
-
while (recent.length > RECENT_LIMIT) recent.shift();
|
|
78
|
-
persist();
|
|
79
|
-
},
|
|
80
|
-
/**
|
|
81
|
-
* 当前账本内容。
|
|
82
|
-
* @returns {object}
|
|
83
|
-
*/
|
|
84
|
-
snapshot() {
|
|
85
|
-
return { version: 1, startedAt, updatedAt: new Date().toISOString(), counts, recent: [...recent] };
|
|
86
|
-
},
|
|
87
|
-
flush: persist,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* 读一个账本文件(排错用)。
|
|
93
|
-
* @param {string} path 账本路径
|
|
94
|
-
* @returns {object|undefined}
|
|
95
|
-
*/
|
|
96
|
-
export function readLedger(path) {
|
|
97
|
-
try {
|
|
98
|
-
return JSON.parse(readFileSync(path, 'utf8'));
|
|
99
|
-
} catch {
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
102
|
-
}
|