dsh-browser-playwright-codex 0.7.1 → 0.7.4
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/CHANGELOG.md +50 -0
- package/README.md +12 -0
- package/SECURITY.md +4 -0
- package/lib/browser-lifecycle.js +9 -2
- package/lib/cache-clean.d.ts +90 -0
- package/lib/cache-clean.js +386 -0
- package/lib/challenge.d.ts +8 -0
- package/lib/challenge.js +16 -0
- package/lib/client.js +250 -91
- package/lib/playwright.js +2 -1
- package/lib/provider.d.ts +14 -0
- package/lib/provider.js +38 -5
- package/lib/runtime-state.d.ts +20 -0
- package/lib/runtime-state.js +53 -0
- package/lib/settings-card.js +118 -0
- package/lib/tool-render.js +3 -2
- package/package.json +1 -1
package/lib/runtime-state.d.ts
CHANGED
|
@@ -33,6 +33,26 @@ export declare function getEnabled(): boolean;
|
|
|
33
33
|
export declare function setEnabled(next: boolean): boolean;
|
|
34
34
|
/** Subscribe to enable-state transitions. Returns an unsubscribe function. */
|
|
35
35
|
export declare function subscribe(fn: (enabled: boolean) => void): () => void;
|
|
36
|
+
/**
|
|
37
|
+
* The window mode the user picked (true = headless), or undefined when they
|
|
38
|
+
* never picked one — the configured default then applies.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getHeadless(): boolean | undefined;
|
|
41
|
+
/** Persist the window-mode choice. Returns true when the value changed. */
|
|
42
|
+
export declare function setHeadless(next: boolean): boolean;
|
|
43
|
+
/** Publish the configured default (launch.headless) for display. */
|
|
44
|
+
export declare function publishConfiguredHeadless(value: boolean): void;
|
|
45
|
+
/** The configured default, when the provider has published it. */
|
|
46
|
+
export declare function getConfiguredHeadless(): boolean | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Effective window mode: the user's choice, else the configured default, else
|
|
49
|
+
* the shipped default (headful).
|
|
50
|
+
*
|
|
51
|
+
* @param configuredFallback - config default when the provider has not published one
|
|
52
|
+
*/
|
|
53
|
+
export declare function getEffectiveHeadless(configuredFallback?: boolean): boolean;
|
|
54
|
+
/** Did the user pick the window mode themselves? (The card shows "你选的".) */
|
|
55
|
+
export declare function isHeadlessUserChosen(): boolean;
|
|
36
56
|
/**
|
|
37
57
|
* The page-reading mode the user picked, or undefined when they never picked
|
|
38
58
|
* one — the configured default then applies.
|
package/lib/runtime-state.js
CHANGED
|
@@ -12,6 +12,10 @@ function stateFile() {
|
|
|
12
12
|
}
|
|
13
13
|
let _enabled = true;
|
|
14
14
|
let _engine = undefined;
|
|
15
|
+
/** User's window-mode choice: true = headless, undefined = follow the config. */
|
|
16
|
+
let _headless = undefined;
|
|
17
|
+
/** Config default pushed in by the provider (display only). */
|
|
18
|
+
let _configuredHeadless = undefined;
|
|
15
19
|
/** Default pushed in by the provider that is actually loaded (display only). */
|
|
16
20
|
let _configuredEngine = undefined;
|
|
17
21
|
/** Effective tool gates, pushed in by the tool family when it is applied (display only). */
|
|
@@ -33,6 +37,9 @@ function load() {
|
|
|
33
37
|
// unknown value means "follow the configured default" (undefined here).
|
|
34
38
|
// Files written before this setting existed therefore stay valid.
|
|
35
39
|
_engine = obj.engine === 'legacy' || obj.engine === 'aria' ? obj.engine : undefined;
|
|
40
|
+
// Same contract as engine: absent/unknown means "follow the configured
|
|
41
|
+
// default", so files written before this setting existed stay valid.
|
|
42
|
+
_headless = typeof obj.headless === 'boolean' ? obj.headless : undefined;
|
|
36
43
|
}
|
|
37
44
|
}
|
|
38
45
|
catch {
|
|
@@ -60,6 +67,8 @@ function persist() {
|
|
|
60
67
|
// Keep the historical shape until the user actually picks a page-reading
|
|
61
68
|
// mode: an absent `engine` means "use the configured default".
|
|
62
69
|
const payload = _engine === undefined ? { enabled: _enabled } : { enabled: _enabled, engine: _engine };
|
|
70
|
+
if (_headless !== undefined)
|
|
71
|
+
payload.headless = _headless;
|
|
63
72
|
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2));
|
|
64
73
|
fs.renameSync(tmp, target);
|
|
65
74
|
}
|
|
@@ -99,6 +108,50 @@ export function subscribe(fn) {
|
|
|
99
108
|
_subscribers.delete(fn);
|
|
100
109
|
};
|
|
101
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* The window mode the user picked (true = headless), or undefined when they
|
|
113
|
+
* never picked one — the configured default then applies.
|
|
114
|
+
*/
|
|
115
|
+
export function getHeadless() {
|
|
116
|
+
load();
|
|
117
|
+
return _headless;
|
|
118
|
+
}
|
|
119
|
+
/** Persist the window-mode choice. Returns true when the value changed. */
|
|
120
|
+
export function setHeadless(next) {
|
|
121
|
+
load();
|
|
122
|
+
if (typeof next !== 'boolean')
|
|
123
|
+
return false;
|
|
124
|
+
if (_headless === next)
|
|
125
|
+
return false;
|
|
126
|
+
_headless = next;
|
|
127
|
+
persist();
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
/** Publish the configured default (launch.headless) for display. */
|
|
131
|
+
export function publishConfiguredHeadless(value) {
|
|
132
|
+
_configuredHeadless = value === true;
|
|
133
|
+
}
|
|
134
|
+
/** The configured default, when the provider has published it. */
|
|
135
|
+
export function getConfiguredHeadless() {
|
|
136
|
+
return _configuredHeadless;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Effective window mode: the user's choice, else the configured default, else
|
|
140
|
+
* the shipped default (headful).
|
|
141
|
+
*
|
|
142
|
+
* @param configuredFallback - config default when the provider has not published one
|
|
143
|
+
*/
|
|
144
|
+
export function getEffectiveHeadless(configuredFallback = false) {
|
|
145
|
+
load();
|
|
146
|
+
if (_headless !== undefined)
|
|
147
|
+
return _headless;
|
|
148
|
+
return _configuredHeadless !== undefined ? _configuredHeadless : configuredFallback === true;
|
|
149
|
+
}
|
|
150
|
+
/** Did the user pick the window mode themselves? (The card shows "你选的".) */
|
|
151
|
+
export function isHeadlessUserChosen() {
|
|
152
|
+
load();
|
|
153
|
+
return _headless !== undefined;
|
|
154
|
+
}
|
|
102
155
|
/**
|
|
103
156
|
* The page-reading mode the user picked, or undefined when they never picked
|
|
104
157
|
* one — the configured default then applies.
|
package/lib/settings-card.js
CHANGED
|
@@ -64,6 +64,7 @@ import * as runtimeState from "./runtime-state.js";
|
|
|
64
64
|
// without a DSH host (a half-written patch layer would break every plugin load).
|
|
65
65
|
import { homePatchPath, parseGatesBlock, writeGates } from "./gates-patch.js";
|
|
66
66
|
import { readFileSync } from 'node:fs';
|
|
67
|
+
import { clean, isTargetId, resolvePaths, scan } from "./cache-clean.js";
|
|
67
68
|
const { getEnabled, setEnabled, getEffectiveEngine, setSnapshotEngine, isEngineUserChosen } = runtimeState;
|
|
68
69
|
export const name = 'browser-toggle';
|
|
69
70
|
export const inject = ['webServer'];
|
|
@@ -114,7 +115,121 @@ async function readBody(req) {
|
|
|
114
115
|
}
|
|
115
116
|
return { ok: true, text: Buffer.concat(chunks).toString('utf8') };
|
|
116
117
|
}
|
|
118
|
+
/** The plugin's own version, used to refuse other versions' installers. */
|
|
119
|
+
function installedVersion() {
|
|
120
|
+
try {
|
|
121
|
+
const raw = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
122
|
+
return typeof raw.version === 'string' ? raw.version : '0.0.0';
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return '0.0.0';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Resolved disk locations; never taken from the request body. */
|
|
129
|
+
function diskPaths() {
|
|
130
|
+
return resolvePaths(process.env, installedVersion());
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* GET /disk/stat — sizes per bucket; POST /disk/clean — preview or execute.
|
|
134
|
+
* Both require the CSRF header, both answer with per-target detail so partial
|
|
135
|
+
* failures are visible instead of being folded into one number.
|
|
136
|
+
*/
|
|
137
|
+
function registerDiskRoutes(ctx) {
|
|
138
|
+
ctx.effect(() => ctx.webServer.register({
|
|
139
|
+
kind: 'exact',
|
|
140
|
+
path: PREFIX + '/disk/stat',
|
|
141
|
+
handler: async (req, res) => {
|
|
142
|
+
if (req.method !== 'GET')
|
|
143
|
+
return sendJson(res, 405, { error: { code: 'METHOD', message: 'GET only' } });
|
|
144
|
+
if (req.headers[HEADER] !== 'browser-toggle')
|
|
145
|
+
return sendJson(res, 403, { error: { code: 'CSRF', message: 'missing plugin header' } });
|
|
146
|
+
try {
|
|
147
|
+
const report = await scan(diskPaths());
|
|
148
|
+
sendJson(res, 200, { ...report, browserActive: getEnabled() === true });
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
sendJson(res, 500, { error: { code: 'DISK_SCAN', message: String(error && error.message ? error.message : error) } });
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
}));
|
|
155
|
+
ctx.effect(() => ctx.webServer.register({
|
|
156
|
+
kind: 'exact',
|
|
157
|
+
path: PREFIX + '/disk/clean',
|
|
158
|
+
handler: async (req, res) => {
|
|
159
|
+
if (req.method !== 'POST')
|
|
160
|
+
return sendJson(res, 405, { error: { code: 'METHOD', message: 'POST only' } });
|
|
161
|
+
if (req.headers[HEADER] !== 'browser-toggle')
|
|
162
|
+
return sendJson(res, 403, { error: { code: 'CSRF', message: 'missing plugin header' } });
|
|
163
|
+
const body = await readBody(req);
|
|
164
|
+
if (!body.ok)
|
|
165
|
+
return sendJson(res, 400, { error: { code: 'BODY_READ', message: body.error } });
|
|
166
|
+
let payload;
|
|
167
|
+
try {
|
|
168
|
+
payload = JSON.parse(body.text || '{}');
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return sendJson(res, 400, { error: { code: 'INVALID_JSON', message: 'body must be JSON' } });
|
|
172
|
+
}
|
|
173
|
+
if (typeof payload.dryRun !== 'boolean')
|
|
174
|
+
return sendJson(res, 400, { error: { code: 'INVALID_BODY', message: '`dryRun` must be a boolean' } });
|
|
175
|
+
if (!Array.isArray(payload.targets) || payload.targets.length === 0)
|
|
176
|
+
return sendJson(res, 400, { error: { code: 'INVALID_BODY', message: '`targets` must be a non-empty array' } });
|
|
177
|
+
const targets = [];
|
|
178
|
+
for (const value of payload.targets) {
|
|
179
|
+
if (!isTargetId(value))
|
|
180
|
+
return sendJson(res, 400, { error: { code: 'UNKNOWN_TARGET', message: 'unknown target: ' + String(value) } });
|
|
181
|
+
targets.push(value);
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const report = await clean({ ...diskPaths(), targets, dryRun: payload.dryRun, browserActive: getEnabled() === true });
|
|
185
|
+
sendJson(res, 200, report);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
sendJson(res, 500, { error: { code: 'DISK_CLEAN', message: String(error && error.message ? error.message : error) } });
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* POST /window { headless } — the user's window-mode switch.
|
|
195
|
+
*
|
|
196
|
+
* The change is persisted immediately and applied by the provider on its next
|
|
197
|
+
* acquire (it compares the mode it launched with against the desired one and
|
|
198
|
+
* relaunches, exporting login state first). Nothing is killed mid-operation.
|
|
199
|
+
*/
|
|
200
|
+
function registerWindowRoute(ctx) {
|
|
201
|
+
ctx.effect(() => ctx.webServer.register({
|
|
202
|
+
kind: 'exact',
|
|
203
|
+
path: PREFIX + '/window',
|
|
204
|
+
handler: async (req, res) => {
|
|
205
|
+
if (req.method !== 'POST')
|
|
206
|
+
return sendJson(res, 405, { error: { code: 'METHOD', message: 'POST only' } });
|
|
207
|
+
if (req.headers[HEADER] !== 'browser-toggle')
|
|
208
|
+
return sendJson(res, 403, { error: { code: 'CSRF', message: 'missing plugin header' } });
|
|
209
|
+
const body = await readBody(req);
|
|
210
|
+
if (!body.ok)
|
|
211
|
+
return sendJson(res, 400, { error: { code: 'BODY_READ', message: body.error } });
|
|
212
|
+
let payload;
|
|
213
|
+
try {
|
|
214
|
+
payload = JSON.parse(body.text || '{}');
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return sendJson(res, 400, { error: { code: 'INVALID_JSON', message: 'body must be JSON' } });
|
|
218
|
+
}
|
|
219
|
+
if (typeof payload.headless !== 'boolean')
|
|
220
|
+
return sendJson(res, 400, { error: { code: 'INVALID_BODY', message: '`headless` must be a boolean' } });
|
|
221
|
+
const changed = runtimeState.setHeadless(payload.headless);
|
|
222
|
+
sendJson(res, 200, {
|
|
223
|
+
headless: runtimeState.getEffectiveHeadless(),
|
|
224
|
+
changed,
|
|
225
|
+
appliedOn: 'next-operation',
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
117
230
|
export function apply(ctx) {
|
|
231
|
+
registerDiskRoutes(ctx);
|
|
232
|
+
registerWindowRoute(ctx);
|
|
118
233
|
ctx.effect(() => ctx.webServer.register({
|
|
119
234
|
kind: 'exact',
|
|
120
235
|
path: `${PREFIX}/state`,
|
|
@@ -136,6 +251,9 @@ export function apply(ctx) {
|
|
|
136
251
|
allowEvaluate: gates ? gates.allowEvaluate : null,
|
|
137
252
|
allowCdp: gates ? gates.allowCdp : null,
|
|
138
253
|
diagnostics: gates ? gates.diagnostics : null,
|
|
254
|
+
headless: runtimeState.getEffectiveHeadless(),
|
|
255
|
+
headlessUserChosen: runtimeState.isHeadlessUserChosen(),
|
|
256
|
+
headlessConfigured: runtimeState.getConfiguredHeadless(),
|
|
139
257
|
// What the user's own layer says right now. Differs from the above
|
|
140
258
|
// between a write and the live reload that applies it.
|
|
141
259
|
patchGates: managedBlockGates() ?? null,
|
package/lib/tool-render.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { challengeNote, detectChallenge, detectSilentPage } from "./challenge.js";
|
|
2
2
|
import { renderSnapshot, renderSnapshotDiff } from "./snapshot-render.js";
|
|
3
|
+
import * as runtimeState from "./runtime-state.js";
|
|
3
4
|
/**
|
|
4
5
|
* Render one diagnostics payload as one line per entry. Diagnostics are read when
|
|
5
6
|
* something already looks wrong, so the compact form matters more than the rich
|
|
@@ -134,7 +135,7 @@ export function snapshotValue(snapshot) {
|
|
|
134
135
|
']: ' +
|
|
135
136
|
challenge.hint +
|
|
136
137
|
'.\n' +
|
|
137
|
-
|
|
138
|
+
challengeNote(runtimeState.getEffectiveHeadless(true)) +
|
|
138
139
|
'\n\n' +
|
|
139
140
|
tree) +
|
|
140
141
|
diffBlock,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-browser-playwright-codex",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Browser automation for DeepSeek Harness, one plugin: fill in forms, scrape lists and work across tabs in real pages, using the logins you already have. Runs in a visible window that stays quiet when minimised; native dialogs are parked instead of confirmed, CAPTCHA pages are flagged with a note that says stop and hand the page back, password / one-time-code / card fields are refused by browser_fill, and the Settings card (enable/disable, page-reading mode, capability gates) now lives inside this package.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|