@angelky7/dsh-browser-control 1.7.3 → 1.7.5
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.en.md +3 -1
- package/README.md +3 -1
- package/lib/index.js +553 -15
- package/lib/types/audit.d.ts +72 -0
- package/lib/types/audit.js +107 -0
- package/lib/types/index.d.ts +8 -1
- package/lib/types/index.js +242 -17
- package/lib/types/journal.d.ts +45 -0
- package/lib/types/journal.js +128 -0
- package/llms.txt +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话恢复(v1.7.5):把审计账本持久化到磁盘,dsh 重启以后还查得到上一个会话做过什么。
|
|
3
|
+
*
|
|
4
|
+
* 为什么是文件而不是数据库:一条命令一行 JSONL,出问题时能直接打开看、能 grep、
|
|
5
|
+
* 写坏了最多丢一行(读到坏行跳过,不炸整本账)。上限 1000 行,超了在**启动时**
|
|
6
|
+
* 裁掉最旧的——运行中不裁剪,append 永远是纯追加。
|
|
7
|
+
*
|
|
8
|
+
* 隐私与审计同一套:写盘的就是 AuditLog 里那份脱敏后的摘要(输入值/cookie 值已
|
|
9
|
+
* 打码),journal 层不再碰原始参数。文件在 <stateDir>/journal.jsonl,stateDir
|
|
10
|
+
* 默认 ~/.dsh-browser-control,可在 profile 的 browser-bridge 配置里改。
|
|
11
|
+
*/
|
|
12
|
+
import { appendFile, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
/** 磁盘上最多保留的条数(启动时裁剪)。 */
|
|
15
|
+
export const JOURNAL_MAX_LINES = 1000;
|
|
16
|
+
/** 防御性上限:正常文件远小于它;超过就只读尾巴,别为了一本账吃掉内存。 */
|
|
17
|
+
const JOURNAL_MAX_BYTES = 8 * 1024 * 1024;
|
|
18
|
+
function isEntry(value) {
|
|
19
|
+
if (value === null || typeof value !== 'object')
|
|
20
|
+
return false;
|
|
21
|
+
const v = value;
|
|
22
|
+
return typeof v.run === 'string'
|
|
23
|
+
&& typeof v.command === 'string'
|
|
24
|
+
&& typeof v.seq === 'number'
|
|
25
|
+
&& typeof v.t === 'number'
|
|
26
|
+
&& typeof v.params === 'string'
|
|
27
|
+
&& typeof v.ok === 'boolean'
|
|
28
|
+
&& typeof v.durationMs === 'number';
|
|
29
|
+
}
|
|
30
|
+
/** 拆行并解析;坏行(截断、手改坏)跳过——一本账不能因为一行烂掉就全废。 */
|
|
31
|
+
function parseLines(text) {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const line of text.split('\n')) {
|
|
34
|
+
if (line.trim().length === 0)
|
|
35
|
+
continue;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(line);
|
|
38
|
+
if (isEntry(parsed))
|
|
39
|
+
out.push(parsed);
|
|
40
|
+
}
|
|
41
|
+
catch { /* 坏行跳过 */ }
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 打开(并按需裁剪)一个 journal 目录。
|
|
47
|
+
* @param stateDir - 目录(不存在会创建);journal 文件是 `<stateDir>/journal.jsonl`。
|
|
48
|
+
* @param log - 一行式日志;打开/裁剪的结论写进去。
|
|
49
|
+
* @returns 带 run 编号的追加器;`close()` 等所有在途写入落盘。
|
|
50
|
+
*/
|
|
51
|
+
export class Journal {
|
|
52
|
+
/** 这次运行的编号:启动时刻(毫秒精度;秒级精度下两次启动同秒会撞号)。 */
|
|
53
|
+
runId;
|
|
54
|
+
file;
|
|
55
|
+
log;
|
|
56
|
+
chain = Promise.resolve();
|
|
57
|
+
warned = false;
|
|
58
|
+
constructor(file, runId, log) {
|
|
59
|
+
this.file = file;
|
|
60
|
+
this.runId = runId;
|
|
61
|
+
this.log = log;
|
|
62
|
+
}
|
|
63
|
+
static async open(stateDir, log) {
|
|
64
|
+
await mkdir(stateDir, { recursive: true });
|
|
65
|
+
const file = path.join(stateDir, 'journal.jsonl');
|
|
66
|
+
let existing = [];
|
|
67
|
+
try {
|
|
68
|
+
const text = await readFile(file, 'utf8');
|
|
69
|
+
existing = parseLines(text.length > JOURNAL_MAX_BYTES ? text.slice(-JOURNAL_MAX_BYTES) : text);
|
|
70
|
+
}
|
|
71
|
+
catch { /* 首次运行没有文件 */ }
|
|
72
|
+
if (existing.length > JOURNAL_MAX_LINES) {
|
|
73
|
+
const kept = existing.slice(existing.length - JOURNAL_MAX_LINES);
|
|
74
|
+
const tmp = `${file}.tmp`;
|
|
75
|
+
await writeFile(tmp, kept.map((e) => `${JSON.stringify(e)}\n`).join(''), 'utf8');
|
|
76
|
+
await rename(tmp, file);
|
|
77
|
+
log(`journal: pruned ${existing.length - kept.length} old entries (${JOURNAL_MAX_LINES} kept)`);
|
|
78
|
+
}
|
|
79
|
+
else if (existing.length > 0) {
|
|
80
|
+
const last = existing[existing.length - 1];
|
|
81
|
+
log(`journal: restored context — ${existing.length} entries on disk, last from run ${last.run} (#${last.seq} ${last.command})`);
|
|
82
|
+
}
|
|
83
|
+
return new Journal(file, new Date().toISOString().slice(0, 23).replace('T', ' '), log);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 追加一条(异步、排队写;失败只告警一次,绝不阻塞命令路径)。
|
|
87
|
+
* @param entry - AuditLog 刚记下的条目(参数已脱敏)。
|
|
88
|
+
*/
|
|
89
|
+
append(entry) {
|
|
90
|
+
const line = `${JSON.stringify({ run: this.runId, ...entry })}\n`;
|
|
91
|
+
this.chain = this.chain.then(() => appendFile(this.file, line, 'utf8')).catch((error) => {
|
|
92
|
+
// 账本写不进去不是命令失败的理由:告警一次,继续跑。
|
|
93
|
+
if (!this.warned) {
|
|
94
|
+
this.warned = true;
|
|
95
|
+
this.log(`journal: write failed — ${error instanceof Error ? error.message : String(error)}`);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/** 等在途写入落盘(teardown / 测试用)。 */
|
|
100
|
+
async close() {
|
|
101
|
+
await this.chain.catch(() => { });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 读 journal 的尾部(不要求 Journal 实例——读在线程外也能工作,扩展离线也行)。
|
|
106
|
+
* @param stateDir - journal 所在目录。
|
|
107
|
+
* @param filter - tabId / 命令子串 / 条数(默认 50,最大 500;取最新)。
|
|
108
|
+
*/
|
|
109
|
+
export async function readJournal(stateDir, filter = {}) {
|
|
110
|
+
let text;
|
|
111
|
+
try {
|
|
112
|
+
text = await readFile(path.join(stateDir, 'journal.jsonl'), 'utf8');
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return { total: 0, entries: [] };
|
|
116
|
+
}
|
|
117
|
+
if (text.length > JOURNAL_MAX_BYTES)
|
|
118
|
+
text = text.slice(-JOURNAL_MAX_BYTES);
|
|
119
|
+
let matched = parseLines(text);
|
|
120
|
+
if (filter.tabId !== undefined)
|
|
121
|
+
matched = matched.filter((e) => e.tabId === filter.tabId);
|
|
122
|
+
if (filter.command !== undefined && filter.command.length > 0) {
|
|
123
|
+
const needle = filter.command.toLowerCase();
|
|
124
|
+
matched = matched.filter((e) => e.command.toLowerCase().includes(needle));
|
|
125
|
+
}
|
|
126
|
+
const limit = Math.min(500, Math.max(1, Math.trunc(filter.limit ?? 50)));
|
|
127
|
+
return { total: matched.length, entries: matched.slice(Math.max(0, matched.length - limit)) };
|
|
128
|
+
}
|
package/llms.txt
CHANGED
|
@@ -42,7 +42,7 @@ Chrome 浏览器扩展 + DeepSeek Harness 插件,让 AI Agent 通过 CDP 驱
|
|
|
42
42
|
|
|
43
43
|
## 关键文件
|
|
44
44
|
|
|
45
|
-
- `src/index.ts` -
|
|
45
|
+
- `src/index.ts` - 28 个 `browser_*` 工具注册 + settings 接线的版本兼容分支
|
|
46
46
|
- `src/policy.ts` - 三张互不相同的表:只读白名单、脚本命令、插件本地破坏性命令(`browser_cleanup` 不经过 `execute()`,必须自己站闸门)
|
|
47
47
|
- `src/server.ts` - WebSocket 服务 + HTTP 面(鉴权、来源校验、状态页)
|
|
48
48
|
- `src/ws.ts` - RFC 6455 帧编解码
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angelky7/dsh-browser-control",
|
|
3
3
|
"description": "Local WebSocket bridge between dsh and the DSH Browser Control browser extension, with model-facing browser_* tools and a Settings-managed on/off switch",
|
|
4
|
-
"version": "1.7.
|
|
4
|
+
"version": "1.7.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"build": "tsc -p tsconfig.json && tsdown",
|
|
38
38
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
39
39
|
"check": "npm run typecheck && npm run build && publint",
|
|
40
|
-
"verify": "node verify/verify_config_patch.mjs && node verify/verify_peers.mjs && node verify/verify_surface.mjs && node verify/verify_tab_groups.mjs && node verify/verify_tabs_close_many.mjs && node verify/verify_cdp_retry.mjs && node verify/verify_snapshot_ax.mjs && node verify/verify_hard_lock.mjs && node verify/verify_policy.mjs && node verify/verify_cookies.mjs && node verify/verify_extension_e2e.mjs && node verify/verify_plugin.mjs legacy && node verify/verify_plugin.mjs conflict && node verify/verify_plugin.mjs new && node verify/verify_http.mjs && node verify/verify_errors.mjs && node verify/verify_hold.mjs && node verify/verify_protocol.mjs && node verify/verify_bundle_http.mjs && node verify/verify_nav_report.mjs && node verify/verify_nav_landing.mjs && node verify/verify_render_contract.mjs && node verify/verify_press_events.mjs && node verify/verify_nav_timeout.mjs && node verify/verify_nav_waituntil.mjs && node verify/verify_archive.mjs && node verify/verify_windows.mjs && node verify/verify_history_nav.mjs && node verify/verify_metrics_throttle.mjs && node verify/verify_drag.mjs && node verify/verify_compat_matrix.mjs"
|
|
40
|
+
"verify": "node verify/verify_config_patch.mjs && node verify/verify_peers.mjs && node verify/verify_surface.mjs && node verify/verify_tab_groups.mjs && node verify/verify_tabs_close_many.mjs && node verify/verify_cdp_retry.mjs && node verify/verify_snapshot_ax.mjs && node verify/verify_hard_lock.mjs && node verify/verify_policy.mjs && node verify/verify_cookies.mjs && node verify/verify_extension_e2e.mjs && node verify/verify_plugin.mjs legacy && node verify/verify_plugin.mjs conflict && node verify/verify_plugin.mjs new && node verify/verify_http.mjs && node verify/verify_errors.mjs && node verify/verify_hold.mjs && node verify/verify_protocol.mjs && node verify/verify_bundle_http.mjs && node verify/verify_nav_report.mjs && node verify/verify_nav_landing.mjs && node verify/verify_render_contract.mjs && node verify/verify_press_events.mjs && node verify/verify_nav_timeout.mjs && node verify/verify_nav_waituntil.mjs && node verify/verify_archive.mjs && node verify/verify_windows.mjs && node verify/verify_history_nav.mjs && node verify/verify_metrics_throttle.mjs && node verify/verify_drag.mjs && node verify/verify_audit.mjs && node verify/verify_journal.mjs && node verify/verify_compat_matrix.mjs"
|
|
41
41
|
},
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"access": "public",
|