dsh-browser-playwright-codex 0.8.2 → 0.8.3
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 +29 -0
- package/lib/browser-lifecycle.js +16 -2
- package/lib/client.js +97 -0
- package/lib/config.d.ts +5 -0
- package/lib/config.js +5 -0
- package/lib/gates-patch.d.ts +29 -0
- package/lib/gates-patch.js +149 -0
- package/lib/provider.js +4 -0
- package/lib/proxy.d.ts +56 -0
- package/lib/proxy.js +131 -0
- package/lib/settings-card.js +62 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
## 0.8.3 — 2026-11-12
|
|
2
|
+
|
|
3
|
+
一句话摘要:**插件可以走代理了**——以前它启动的浏览器只能直连,凡是要代理的站点(维基、GitHub 网页、大量国外站)一律打不开。现在多一个 `launch.proxy` 配置项和设置页里的「代理」输入框;**默认留空 = 直连,行为与之前完全一样**。实测同一台机器:维基直连 31 秒超时,走代理 1 秒返回。
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`launch.proxy`**:填 `http://host:port`、`https://host:port` 或 `socks5://host:port`,只写 `host:port` 会按 Playwright 的规则补成 `http://`。代理交给**浏览器启动参数**(`launchOptions.proxy`),整个浏览器走代理——不是在某个请求上加头。
|
|
8
|
+
- **设置页「代理」输入框**:放在「已登录的站点」正前方(这张卡的最后一块),不再散落在中间。占位提示 `http://host:port`,示例用保留域名 `http://proxy.example:8080`,**不写任何本机真实地址**;默认值为空。
|
|
9
|
+
- **强制绕过本机地址**:配置代理时自动带上 `bypass: localhost,127.0.0.1`。不加这个,插件自己的本地页面(verify、本地普查、任何本地服务)会被塞进代理而连不上。
|
|
10
|
+
- **地址写错当场报错,绝不静默回退直连**:缺 scheme 时按 `host:port` 补 http;端口必须 1–65535;带路径/查询/片段、未知 scheme 都拒绝,并给出「写成 `scheme://host:port`」这句人话。静默回退会把"代理没生效"伪装成"网站打不开",比报错更难查。
|
|
11
|
+
- **不支持需要账号密码的代理**:带账号密码的地址在校验阶段就拒。两个理由——密码会明文落在用户的 `cordis.patch.yml` 里;而且 Chromium/Playwright 本身不支持 SOCKS5 认证(`Browser does not support socks5 proxy authentication`)。宁可少一个能力,不埋一个明文密码。
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **保存代理只改一行**:`playwright.launch` 下的 `proxy:` 这一行,与三个能力闸门同一个模式(已知键、按行改)。其它键(`profileDir` / `channel` / `viewport` / `maxSessions` / `toolPrefix` / `maxWaitMs` 等)**逐字节不动**;清空 = 删掉那一行(留空=直连必须是干净的,不能留空值);结构认不出就**拒绝写入**,绝不整块重渲染。
|
|
16
|
+
- **生效时机**:代理是浏览器启动参数,插件在加载时读配置,所以**改完要重启 DSH 才生效**;设置页保存成功后会直接提示这一点。
|
|
17
|
+
|
|
18
|
+
### Tests
|
|
19
|
+
|
|
20
|
+
- 新增 `tests/proxy.test.ts`(9 条):空配置不带 proxy;填了带 server + bypass;`host:port` 短写补 http;端口缺失/非数字/0/65536;认证代理拒绝;未知 scheme 与带路径拒绝;启动路径静态守卫(防止只加配置没接上);**代理不影响 `allowedDomains` 判定**。
|
|
21
|
+
- `tests/settings-card.test.ts` 新增 4 条:保存后 `launch.proxy` 出现且其它键完全不变(含 `profileDir`);清空后那一行被删;非法地址 400 且文件不变;重复保存同一值幂等无重复键。
|
|
22
|
+
- 全套现在 **390 条 / 387 过 / 0 失败 / cancelled 0 / skipped 3 / todo 0**。
|
|
23
|
+
|
|
24
|
+
### Verified
|
|
25
|
+
|
|
26
|
+
- **走代理真跑 live 三条:3/3 通过**。`example.com` 两条照常;`wikipedia REST summary` 在直连时 31 秒 `NAVIGATION_TIMEOUT` 失败,配代理后 **1.0 秒通过**——这就是这次改动的直接证据(此前 0.8.2 发布时这条是 2/3)。
|
|
27
|
+
- verify 60 项全过、preflight 0 硬错误、打包 101 个文件(新增 `lib/proxy.js` / `lib/proxy.d.ts`)。
|
|
28
|
+
- 已知限制:设置页里没有"测试代理连通性"的按钮;填错端口但格式合法时,要到实际启动浏览器才会发现连不上。
|
|
29
|
+
|
|
1
30
|
## 0.8.2 — 2026-11-12
|
|
2
31
|
|
|
3
32
|
一句话摘要:**发版门禁不再会"假绿",快照引擎第一次在真实网站上被量过**——preflight 过去只读 `ℹ fail`(而且"fail 0"是写死的字符串),被取消的用例看不出、也不报;设置页这个全仓最大的零测试模块补上了走真实 HTTP 的测试,顺手修掉"已有未标记装载行时写出第二行"的坏文件路径;智能模式(aria)的解析器与可交互集对齐后,130 个任务页的降级从 18 页降到 3 页。
|
package/lib/browser-lifecycle.js
CHANGED
|
@@ -7,6 +7,7 @@ import { chromium } from 'playwright-core';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import { AUTO_CHANNELS, HUMANIZED_LAUNCH } from "./config.js";
|
|
10
|
+
import { proxyLaunchOption } from "./proxy.js";
|
|
10
11
|
import { SNAPSHOT_SCRIPT } from "./injected.js";
|
|
11
12
|
import { BrowserError, launchFailed } from "./errors.js";
|
|
12
13
|
import { forgetOnBrowserClose, showWelcome } from "./swarm-intro.js";
|
|
@@ -72,6 +73,9 @@ async function launchContext(provider) {
|
|
|
72
73
|
// Headless mode keeps the fixed viewport (no window to track).
|
|
73
74
|
...(headless ? { viewport: provider.config.launch.viewport } : { viewport: null }),
|
|
74
75
|
ignoreHTTPSErrors: provider.config.launch.ignoreHTTPSErrors,
|
|
76
|
+
// Proxy at the browser level (launchOptions.proxy): request headers cannot route a whole
|
|
77
|
+
// browser. Empty proxy yields {} so the default stays a direct connection.
|
|
78
|
+
...proxyLaunchOption(provider.config.launch.proxy),
|
|
75
79
|
...HUMANIZED_LAUNCH,
|
|
76
80
|
args: [
|
|
77
81
|
'--no-first-run',
|
|
@@ -226,7 +230,12 @@ async function launchBrowser(provider) {
|
|
|
226
230
|
const attempts = [];
|
|
227
231
|
if (executablePath !== undefined) {
|
|
228
232
|
try {
|
|
229
|
-
provider.browser = await chromium.launch({
|
|
233
|
+
provider.browser = await chromium.launch({
|
|
234
|
+
executablePath,
|
|
235
|
+
headless,
|
|
236
|
+
...HUMANIZED_LAUNCH,
|
|
237
|
+
...proxyLaunchOption(provider.config.launch.proxy),
|
|
238
|
+
});
|
|
230
239
|
provider.launchedHeadless = headless;
|
|
231
240
|
return provider.browser;
|
|
232
241
|
}
|
|
@@ -239,7 +248,12 @@ async function launchBrowser(provider) {
|
|
|
239
248
|
const channels = channel !== undefined && channel !== '' ? [channel] : [...AUTO_CHANNELS];
|
|
240
249
|
for (const candidate of channels) {
|
|
241
250
|
try {
|
|
242
|
-
provider.browser = await chromium.launch({
|
|
251
|
+
provider.browser = await chromium.launch({
|
|
252
|
+
channel: candidate,
|
|
253
|
+
headless,
|
|
254
|
+
...HUMANIZED_LAUNCH,
|
|
255
|
+
...proxyLaunchOption(provider.config.launch.proxy),
|
|
256
|
+
});
|
|
243
257
|
provider.launchedHeadless = headless;
|
|
244
258
|
return provider.browser;
|
|
245
259
|
}
|
package/lib/client.js
CHANGED
|
@@ -403,6 +403,13 @@ window.__ModuleLoader__.load({
|
|
|
403
403
|
const [logins, setLogins] = useState(null)
|
|
404
404
|
const [headless, setHeadless] = useState(null)
|
|
405
405
|
const [headlessUserChosen, setHeadlessUserChosen] = useState(false)
|
|
406
|
+
// 代理(浏览器启动参数):'' = 直连。
|
|
407
|
+
const [proxy, setProxy] = useState('')
|
|
408
|
+
const [proxyDraft, setProxyDraft] = useState('')
|
|
409
|
+
const [proxyBusy, setProxyBusy] = useState(false)
|
|
410
|
+
const [proxyMsg, setProxyMsg] = useState(null)
|
|
411
|
+
// 输入框有焦点时,5 秒一次的轮询不许覆盖用户正在打的字。
|
|
412
|
+
const proxyFocusedRef = useRef(false)
|
|
406
413
|
const [showAll, setShowAll] = useState(false)
|
|
407
414
|
const [openGroups, setOpenGroups] = useState([])
|
|
408
415
|
const [hiddenDomains, setHiddenDomains] = useState(readHiddenDomains())
|
|
@@ -436,6 +443,10 @@ window.__ModuleLoader__.load({
|
|
|
436
443
|
if (typeof s.swarmPersistPlayed === 'boolean') setSwarmPersist(s.swarmPersistPlayed)
|
|
437
444
|
if (typeof s.headless === 'boolean') setHeadless(s.headless)
|
|
438
445
|
setHeadlessUserChosen(s.headlessUserChosen === true)
|
|
446
|
+
if (typeof s.proxy === 'string') {
|
|
447
|
+
setProxy(s.proxy)
|
|
448
|
+
if (!proxyFocusedRef.current) setProxyDraft(s.proxy)
|
|
449
|
+
}
|
|
439
450
|
setGates({
|
|
440
451
|
allowEvaluate: typeof s.allowEvaluate === 'boolean' ? s.allowEvaluate : null,
|
|
441
452
|
allowCdp: typeof s.allowCdp === 'boolean' ? s.allowCdp : null,
|
|
@@ -573,6 +584,30 @@ window.__ModuleLoader__.load({
|
|
|
573
584
|
}
|
|
574
585
|
}
|
|
575
586
|
|
|
587
|
+
// 保存代理:只改 cordis.patch.yml 里 launch.proxy 那一行;空 = 删掉(直连)。
|
|
588
|
+
const saveProxy = async () => {
|
|
589
|
+
if (proxyBusy) return
|
|
590
|
+
setProxyBusy(true)
|
|
591
|
+
setProxyMsg(null)
|
|
592
|
+
try {
|
|
593
|
+
const r = await api('/proxy', 'POST', { proxy: proxyDraft })
|
|
594
|
+
if (r && r.error) {
|
|
595
|
+
setProxyMsg({ kind: 'error', text: r.error.message || r.error.code || '保存失败' })
|
|
596
|
+
return
|
|
597
|
+
}
|
|
598
|
+
const saved = typeof r.proxy === 'string' ? r.proxy : ''
|
|
599
|
+
setProxy(saved)
|
|
600
|
+
setProxyDraft(saved)
|
|
601
|
+
setProxyMsg({
|
|
602
|
+
kind: 'ok',
|
|
603
|
+
text: (saved === '' ? '已保存:留空 = 直连。' : '已保存:' + saved + '。') + '重启 DSH 后生效。',
|
|
604
|
+
})
|
|
605
|
+
} catch (error) {
|
|
606
|
+
setProxyMsg({ kind: 'error', text: '保存失败:' + (error?.message ?? String(error)) })
|
|
607
|
+
} finally {
|
|
608
|
+
setProxyBusy(false)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
576
611
|
const chooseWindow = async (nextHeadless) => {
|
|
577
612
|
if (headless === null || busy || nextHeadless === headless) return
|
|
578
613
|
setBusy(true)
|
|
@@ -1153,6 +1188,68 @@ window.__ModuleLoader__.load({
|
|
|
1153
1188
|
)
|
|
1154
1189
|
: null,
|
|
1155
1190
|
|
|
1191
|
+
// ---- 代理(浏览器启动参数)----
|
|
1192
|
+
// 放在「已登录的站点」正前方(用户要求的最后一块),样式与其它分组一致:
|
|
1193
|
+
// 标题行 + 一整块内容。留空 = 直连;改了要重启 DSH 才生效(保存成功时会说明)。
|
|
1194
|
+
h('div', { key: 'proxyGroup', style: S.group }, ['代理']),
|
|
1195
|
+
h('div', { key: 'proxyBody', style: { ...S.list, marginTop: 8 } }, [
|
|
1196
|
+
h('div', { key: 'row', style: { display: 'flex', alignItems: 'center', gap: 8 } }, [
|
|
1197
|
+
h('input', {
|
|
1198
|
+
key: 'input',
|
|
1199
|
+
type: 'text',
|
|
1200
|
+
value: proxyDraft,
|
|
1201
|
+
placeholder: 'http://host:port',
|
|
1202
|
+
disabled: proxyBusy,
|
|
1203
|
+
spellCheck: false,
|
|
1204
|
+
onFocus: () => {
|
|
1205
|
+
proxyFocusedRef.current = true
|
|
1206
|
+
},
|
|
1207
|
+
onBlur: () => {
|
|
1208
|
+
proxyFocusedRef.current = false
|
|
1209
|
+
},
|
|
1210
|
+
onChange: (e) => {
|
|
1211
|
+
setProxyDraft(e.target.value)
|
|
1212
|
+
setProxyMsg(null)
|
|
1213
|
+
},
|
|
1214
|
+
style: {
|
|
1215
|
+
flex: '1 1 auto',
|
|
1216
|
+
minWidth: 160,
|
|
1217
|
+
height: 28,
|
|
1218
|
+
padding: '0 10px',
|
|
1219
|
+
border: '0.5px solid ' + T.borderL3,
|
|
1220
|
+
borderRadius: 14,
|
|
1221
|
+
background: 'transparent',
|
|
1222
|
+
font: 'inherit',
|
|
1223
|
+
fontSize: 12,
|
|
1224
|
+
lineHeight: '18px',
|
|
1225
|
+
color: T.primary,
|
|
1226
|
+
},
|
|
1227
|
+
}),
|
|
1228
|
+
h(
|
|
1229
|
+
Btn,
|
|
1230
|
+
{
|
|
1231
|
+
key: 'save',
|
|
1232
|
+
disabled: proxyBusy || proxyDraft === proxy,
|
|
1233
|
+
},
|
|
1234
|
+
proxyBusy ? '保存中…' : '保存',
|
|
1235
|
+
),
|
|
1236
|
+
]),
|
|
1237
|
+
h(
|
|
1238
|
+
'p',
|
|
1239
|
+
{ key: 'hint', style: S.note },
|
|
1240
|
+
'留空 = 直连。要填的话写成 http://主机:端口 或 socks5://主机:端口,例如 http://proxy.example:8080;本机页面不走代理。不支持需要账号密码的代理。',
|
|
1241
|
+
),
|
|
1242
|
+
proxyMsg
|
|
1243
|
+
? h(
|
|
1244
|
+
'p',
|
|
1245
|
+
{
|
|
1246
|
+
key: 'msg',
|
|
1247
|
+
style: { ...S.note, color: proxyMsg.kind === 'error' ? T.error : T.success },
|
|
1248
|
+
},
|
|
1249
|
+
proxyMsg.text,
|
|
1250
|
+
)
|
|
1251
|
+
: null,
|
|
1252
|
+
]),
|
|
1156
1253
|
// ---- 已登录站点(默认收起) ----
|
|
1157
1254
|
h(
|
|
1158
1255
|
'div',
|
package/lib/config.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ export interface PlaywrightConfig {
|
|
|
25
25
|
navigationTimeoutMs: number;
|
|
26
26
|
/** Ignore HTTPS certificate errors. */
|
|
27
27
|
ignoreHTTPSErrors: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Proxy for the whole browser, e.g. `http://proxy.example:8080` (http / https / socks5).
|
|
30
|
+
* Empty = connect directly — the default, and the previous behaviour.
|
|
31
|
+
*/
|
|
32
|
+
proxy?: string;
|
|
28
33
|
};
|
|
29
34
|
/** Host suffixes the browser may visit. Empty = any http(s) host. */
|
|
30
35
|
allowedDomains?: string[];
|
package/lib/config.js
CHANGED
|
@@ -27,6 +27,11 @@ export const Config = z.object({
|
|
|
27
27
|
// one second; the default stays 30s.
|
|
28
28
|
navigationTimeoutMs: z.number().min(1000).default(30000),
|
|
29
29
|
ignoreHTTPSErrors: z.boolean().default(false),
|
|
30
|
+
// Proxy for the whole browser (launchOptions.proxy), e.g. http://proxy.example:8080.
|
|
31
|
+
// Empty = direct, which is the default and keeps every existing deployment unchanged.
|
|
32
|
+
// Validation lives in src/proxy.ts so a bad address fails loudly instead of silently
|
|
33
|
+
// connecting directly (which would look like "the site is down").
|
|
34
|
+
proxy: z.string().default(''),
|
|
30
35
|
}),
|
|
31
36
|
allowedDomains: z.array(z.string()),
|
|
32
37
|
idleTimeoutMs: z.number().default(600000),
|
package/lib/gates-patch.d.ts
CHANGED
|
@@ -86,4 +86,33 @@ export declare function writeGates(allowEvaluate: boolean, allowCdp: boolean, di
|
|
|
86
86
|
allowCdp: boolean;
|
|
87
87
|
diagnostics: boolean;
|
|
88
88
|
}>;
|
|
89
|
+
/**
|
|
90
|
+
* 改写块里的 proxy 行:有就改、没有就插、空字符串就删。其它行逐字节保留。
|
|
91
|
+
*
|
|
92
|
+
* @param body - 托管块全文(含标记)
|
|
93
|
+
* @param proxy - 已经过 normalizeProxy 校验的地址;'' 表示删掉这一行
|
|
94
|
+
* @returns 改好的块
|
|
95
|
+
*/
|
|
96
|
+
export declare function patchBlockProxy(body: string, proxy: string): string;
|
|
97
|
+
/** 读托管块里的 proxy 值;没有就是 ''(= 直连)。 */
|
|
98
|
+
export declare function parseBrowserBlockProxy(text: string): string;
|
|
99
|
+
/**
|
|
100
|
+
* 把 proxy 合并进文件内容:没有托管块就先按模板建一个,再改这一行。
|
|
101
|
+
*
|
|
102
|
+
* @param original - 原文件内容
|
|
103
|
+
* @param proxy - 已校验的地址;'' = 删掉
|
|
104
|
+
* @returns 新文件内容
|
|
105
|
+
*/
|
|
106
|
+
export declare function mergeBrowserBlockProxy(original: string, proxy: string): string;
|
|
107
|
+
/**
|
|
108
|
+
* 写 proxy:备份 → 原子替换 → 读回校验;校验失败恢复原文件。
|
|
109
|
+
*
|
|
110
|
+
* @param proxy - 已校验的地址;'' 表示删掉这一行(= 直连)
|
|
111
|
+
* @param env - 环境(测试用 DSH_HOME 覆盖)
|
|
112
|
+
* @returns 写到的文件与现在的值
|
|
113
|
+
*/
|
|
114
|
+
export declare function writeProxy(proxy: string, env?: NodeJS.ProcessEnv): Promise<{
|
|
115
|
+
patch: string;
|
|
116
|
+
proxy: string;
|
|
117
|
+
}>;
|
|
89
118
|
export {};
|
package/lib/gates-patch.js
CHANGED
|
@@ -323,3 +323,152 @@ export async function writeGates(allowEvaluate, allowCdp, diagnostics, env = pro
|
|
|
323
323
|
}
|
|
324
324
|
return { patch: file, allowEvaluate, allowCdp, diagnostics };
|
|
325
325
|
}
|
|
326
|
+
/** 在块的文本里找 `launch:` 与它的 `proxy:` 子行。认不出结构就抛错(拒绝写入)。 */
|
|
327
|
+
function findLaunchSection(lines) {
|
|
328
|
+
let launchAt = -1;
|
|
329
|
+
let launchIndent = '';
|
|
330
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
331
|
+
const m = /^(\s*)launch\s*:\s*(#.*)?$/.exec(lines[i] ?? '');
|
|
332
|
+
if (m !== null) {
|
|
333
|
+
launchAt = i;
|
|
334
|
+
launchIndent = m[1] ?? '';
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (launchAt === -1) {
|
|
339
|
+
throw new Error('the managed block has no `playwright.launch:` section, so launch.proxy cannot be edited safely; ' +
|
|
340
|
+
'nothing was written. Fix the block by hand (or remove it and save a gate once to rebuild it).');
|
|
341
|
+
}
|
|
342
|
+
const baseIndent = launchIndent.length;
|
|
343
|
+
let childIndent = '';
|
|
344
|
+
let proxy;
|
|
345
|
+
for (let i = launchAt + 1; i < lines.length; i += 1) {
|
|
346
|
+
const line = lines[i] ?? '';
|
|
347
|
+
const trimmed = line.trim();
|
|
348
|
+
if (trimmed === '')
|
|
349
|
+
continue;
|
|
350
|
+
const indent = (/^\s*/.exec(line)?.[0] ?? '').length;
|
|
351
|
+
if (indent <= baseIndent)
|
|
352
|
+
break; // 出了 launch 这一节
|
|
353
|
+
if (trimmed.startsWith('#'))
|
|
354
|
+
continue;
|
|
355
|
+
if (childIndent === '')
|
|
356
|
+
childIndent = ' '.repeat(indent);
|
|
357
|
+
const p = /^(\s*)proxy\s*:/.exec(line);
|
|
358
|
+
if (p !== null)
|
|
359
|
+
proxy = { at: i, indent: p[1] ?? '' };
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
at: launchAt,
|
|
363
|
+
indent: baseIndent,
|
|
364
|
+
childIndent: childIndent === '' ? ' '.repeat(baseIndent + 2) : childIndent,
|
|
365
|
+
...(proxy === undefined ? {} : { proxy }),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* 改写块里的 proxy 行:有就改、没有就插、空字符串就删。其它行逐字节保留。
|
|
370
|
+
*
|
|
371
|
+
* @param body - 托管块全文(含标记)
|
|
372
|
+
* @param proxy - 已经过 normalizeProxy 校验的地址;'' 表示删掉这一行
|
|
373
|
+
* @returns 改好的块
|
|
374
|
+
*/
|
|
375
|
+
export function patchBlockProxy(body, proxy) {
|
|
376
|
+
const nl = body.includes('\r\n') ? '\r\n' : '\n';
|
|
377
|
+
const lines = body.split(/\r?\n/);
|
|
378
|
+
const section = findLaunchSection(lines);
|
|
379
|
+
const value = proxy.trim();
|
|
380
|
+
const rendered = section.childIndent + 'proxy: ' + JSON.stringify(value);
|
|
381
|
+
if (section.proxy !== undefined) {
|
|
382
|
+
if (value === '')
|
|
383
|
+
lines.splice(section.proxy.at, 1);
|
|
384
|
+
else
|
|
385
|
+
lines[section.proxy.at] = rendered;
|
|
386
|
+
return lines.join(nl);
|
|
387
|
+
}
|
|
388
|
+
if (value === '')
|
|
389
|
+
return lines.join(nl); // 本来就没有,删空是幂等的
|
|
390
|
+
lines.splice(section.at + 1, 0, rendered);
|
|
391
|
+
return lines.join(nl);
|
|
392
|
+
}
|
|
393
|
+
/** 读托管块里的 proxy 值;没有就是 ''(= 直连)。 */
|
|
394
|
+
export function parseBrowserBlockProxy(text) {
|
|
395
|
+
const found = findBlock(text);
|
|
396
|
+
if (found === undefined)
|
|
397
|
+
return '';
|
|
398
|
+
const body = text.slice(found.start, found.after);
|
|
399
|
+
const lines = body.split(/\r?\n/);
|
|
400
|
+
let section;
|
|
401
|
+
try {
|
|
402
|
+
section = findLaunchSection(lines);
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
return '';
|
|
406
|
+
}
|
|
407
|
+
if (section?.proxy === undefined)
|
|
408
|
+
return '';
|
|
409
|
+
const raw = (lines[section.proxy.at] ?? '').replace(/^\s*proxy\s*:\s*/, '');
|
|
410
|
+
const withoutComment = raw.replace(/\s+#.*$/, '').trim();
|
|
411
|
+
if (withoutComment === '')
|
|
412
|
+
return '';
|
|
413
|
+
if ((withoutComment.startsWith('"') && withoutComment.endsWith('"')) ||
|
|
414
|
+
(withoutComment.startsWith("'") && withoutComment.endsWith("'"))) {
|
|
415
|
+
return withoutComment.slice(1, -1);
|
|
416
|
+
}
|
|
417
|
+
return withoutComment;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* 把 proxy 合并进文件内容:没有托管块就先按模板建一个,再改这一行。
|
|
421
|
+
*
|
|
422
|
+
* @param original - 原文件内容
|
|
423
|
+
* @param proxy - 已校验的地址;'' = 删掉
|
|
424
|
+
* @returns 新文件内容
|
|
425
|
+
*/
|
|
426
|
+
export function mergeBrowserBlockProxy(original, proxy) {
|
|
427
|
+
const value = proxy.trim();
|
|
428
|
+
let base = original;
|
|
429
|
+
let found = findBlock(base);
|
|
430
|
+
if (found === undefined) {
|
|
431
|
+
base = (base === '' || base.endsWith('\n') ? base : base + '\n') + '\n' + renderBrowserBlock(false, false, false);
|
|
432
|
+
found = findBlock(base);
|
|
433
|
+
}
|
|
434
|
+
if (found === undefined)
|
|
435
|
+
throw new Error('could not create the managed block; nothing was written');
|
|
436
|
+
const edited = patchBlockProxy(base.slice(found.start, found.after), value);
|
|
437
|
+
if (parseBrowserBlockProxy(edited) !== value) {
|
|
438
|
+
throw new Error('proxy line self-check failed; nothing was written');
|
|
439
|
+
}
|
|
440
|
+
return base.slice(0, found.start) + edited + base.slice(found.after);
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* 写 proxy:备份 → 原子替换 → 读回校验;校验失败恢复原文件。
|
|
444
|
+
*
|
|
445
|
+
* @param proxy - 已校验的地址;'' 表示删掉这一行(= 直连)
|
|
446
|
+
* @param env - 环境(测试用 DSH_HOME 覆盖)
|
|
447
|
+
* @returns 写到的文件与现在的值
|
|
448
|
+
*/
|
|
449
|
+
export async function writeProxy(proxy, env = process.env) {
|
|
450
|
+
const file = homePatchPath(env);
|
|
451
|
+
const value = proxy.trim();
|
|
452
|
+
let original = '';
|
|
453
|
+
try {
|
|
454
|
+
original = await readFile(file, 'utf8');
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
original = '';
|
|
458
|
+
}
|
|
459
|
+
const next = mergeBrowserBlockProxy(original, value);
|
|
460
|
+
if (original !== '')
|
|
461
|
+
await copyFile(file, file + '.bak-browser-toggle').catch(() => { });
|
|
462
|
+
const tmp = file + '.tmp-browser-toggle';
|
|
463
|
+
await writeFile(tmp, next, 'utf8');
|
|
464
|
+
await rename(tmp, file);
|
|
465
|
+
const check = await readFile(file, 'utf8');
|
|
466
|
+
if (parseBrowserBlockProxy(check) !== value) {
|
|
467
|
+
if (original !== '') {
|
|
468
|
+
await writeFile(file, original, 'utf8');
|
|
469
|
+
throw new Error('proxy self-check failed after write; previous content restored');
|
|
470
|
+
}
|
|
471
|
+
throw new Error('proxy self-check failed after write');
|
|
472
|
+
}
|
|
473
|
+
return { patch: file, proxy: value };
|
|
474
|
+
}
|
package/lib/provider.js
CHANGED
|
@@ -17,6 +17,7 @@ import { swarmBootScript } from "./swarm-curtain.js";
|
|
|
17
17
|
import { forgetOnBrowserClose } from "./swarm-intro.js";
|
|
18
18
|
import { isWindowMinimized } from "./window-focus.js";
|
|
19
19
|
import { installHostGuard } from "./navigation-guard.js";
|
|
20
|
+
import { normalizeProxy } from "./proxy.js";
|
|
20
21
|
export class PlaywrightProvider {
|
|
21
22
|
config;
|
|
22
23
|
id = 'playwright';
|
|
@@ -62,6 +63,9 @@ export class PlaywrightProvider {
|
|
|
62
63
|
blockedNavigation = undefined;
|
|
63
64
|
constructor(config) {
|
|
64
65
|
this.config = config;
|
|
66
|
+
// Fail at load time, not at launch time: a bad proxy address must produce the one
|
|
67
|
+
// message that explains it (src/proxy.ts), not a wrapped "tried channel …" failure.
|
|
68
|
+
normalizeProxy(config.launch.proxy);
|
|
65
69
|
// Tell the settings card which page-reading mode this deployment was
|
|
66
70
|
// configured with, so it can show "当前:X(默认)" truthfully instead of
|
|
67
71
|
// assuming the built-in default.
|
package/lib/proxy.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy support for the browser launch (2026-11-12).
|
|
3
|
+
*
|
|
4
|
+
* The plugin starts its own Chromium-family browser, so a proxy has to go into the
|
|
5
|
+
* **launch** options (`launchOptions.proxy`) — request headers cannot route a whole
|
|
6
|
+
* browser. Measured here: Wikipedia through a proxy answered in 4.4 s; direct it timed
|
|
7
|
+
* out at 15 s, which is what every proxied site looked like before this.
|
|
8
|
+
*
|
|
9
|
+
* Three rules, each with a reason:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Empty means direct.** No configured proxy keeps today's behaviour exactly.
|
|
12
|
+
* 2. **`bypass` always includes the local addresses.** Without it the plugin's own
|
|
13
|
+
* `127.0.0.1` pages (verify, the MiniWoB sweep, any local server) would be pushed
|
|
14
|
+
* through the proxy and fail. Playwright passes `bypass` straight to Chromium's
|
|
15
|
+
* proxy bypass list.
|
|
16
|
+
* 3. **A bad address is an error, never a silent fallback to direct.** Quietly going
|
|
17
|
+
* direct would look like "the site is down" and send the user debugging the wrong thing.
|
|
18
|
+
*
|
|
19
|
+
* Authentication is deliberately **not** supported in this version: a password in the
|
|
20
|
+
* config would be stored in plain text in the user's `cordis.patch.yml`, and Playwright
|
|
21
|
+
* refuses SOCKS5 authentication outright (`Browser does not support socks5 proxy
|
|
22
|
+
* authentication`). Rather than ship a plaintext password path, credentials are rejected
|
|
23
|
+
* here with a message that says why.
|
|
24
|
+
*
|
|
25
|
+
* @module dsh-browser-playwright-codex/proxy
|
|
26
|
+
*/
|
|
27
|
+
/** The shape Playwright accepts in `launchOptions.proxy`. */
|
|
28
|
+
export interface LaunchProxy {
|
|
29
|
+
/** `http://host:port`, `https://host:port` or `socks5://host:port`. */
|
|
30
|
+
server: string;
|
|
31
|
+
/** Local addresses bypass the proxy, so the plugin's own local pages keep working. */
|
|
32
|
+
bypass: string;
|
|
33
|
+
}
|
|
34
|
+
/** Hosts that must never be proxied (the plugin's own local pages live there). */
|
|
35
|
+
export declare const PROXY_BYPASS = "localhost,127.0.0.1";
|
|
36
|
+
/**
|
|
37
|
+
* Turn a configured proxy address into Playwright's proxy object.
|
|
38
|
+
*
|
|
39
|
+
* Accepted shapes: `host:port` (treated as HTTP, like Playwright does), `http://host:port`,
|
|
40
|
+
* `https://host:port`, `socks5://host:port`. The port is required and must be 1–65535.
|
|
41
|
+
*
|
|
42
|
+
* @param raw - the configured value; empty/undefined means "connect directly"
|
|
43
|
+
* @returns the proxy settings (server + local bypass), or undefined for a direct connection
|
|
44
|
+
* @throws Error with an actionable message when the address cannot be used
|
|
45
|
+
*/
|
|
46
|
+
export declare function normalizeProxy(raw: string | undefined | null): LaunchProxy | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* The spread-in fragment for Playwright's launch options: `{}` when direct,
|
|
49
|
+
* `{ proxy: … }` when configured. Every launch path uses this so they cannot drift.
|
|
50
|
+
*
|
|
51
|
+
* @param raw - the configured proxy value
|
|
52
|
+
* @returns an object to spread into the launch options
|
|
53
|
+
*/
|
|
54
|
+
export declare function proxyLaunchOption(raw: string | undefined | null): {
|
|
55
|
+
proxy?: LaunchProxy;
|
|
56
|
+
};
|
package/lib/proxy.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy support for the browser launch (2026-11-12).
|
|
3
|
+
*
|
|
4
|
+
* The plugin starts its own Chromium-family browser, so a proxy has to go into the
|
|
5
|
+
* **launch** options (`launchOptions.proxy`) — request headers cannot route a whole
|
|
6
|
+
* browser. Measured here: Wikipedia through a proxy answered in 4.4 s; direct it timed
|
|
7
|
+
* out at 15 s, which is what every proxied site looked like before this.
|
|
8
|
+
*
|
|
9
|
+
* Three rules, each with a reason:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Empty means direct.** No configured proxy keeps today's behaviour exactly.
|
|
12
|
+
* 2. **`bypass` always includes the local addresses.** Without it the plugin's own
|
|
13
|
+
* `127.0.0.1` pages (verify, the MiniWoB sweep, any local server) would be pushed
|
|
14
|
+
* through the proxy and fail. Playwright passes `bypass` straight to Chromium's
|
|
15
|
+
* proxy bypass list.
|
|
16
|
+
* 3. **A bad address is an error, never a silent fallback to direct.** Quietly going
|
|
17
|
+
* direct would look like "the site is down" and send the user debugging the wrong thing.
|
|
18
|
+
*
|
|
19
|
+
* Authentication is deliberately **not** supported in this version: a password in the
|
|
20
|
+
* config would be stored in plain text in the user's `cordis.patch.yml`, and Playwright
|
|
21
|
+
* refuses SOCKS5 authentication outright (`Browser does not support socks5 proxy
|
|
22
|
+
* authentication`). Rather than ship a plaintext password path, credentials are rejected
|
|
23
|
+
* here with a message that says why.
|
|
24
|
+
*
|
|
25
|
+
* @module dsh-browser-playwright-codex/proxy
|
|
26
|
+
*/
|
|
27
|
+
const ALLOWED_SCHEMES = new Set(['http:', 'https:', 'socks5:']);
|
|
28
|
+
/** Hosts that must never be proxied (the plugin's own local pages live there). */
|
|
29
|
+
export const PROXY_BYPASS = 'localhost,127.0.0.1';
|
|
30
|
+
/** One clear sentence for every rejection; the value is echoed so a typo is visible. */
|
|
31
|
+
function reject(value, why) {
|
|
32
|
+
throw new Error('launch.proxy is not a usable proxy address (' +
|
|
33
|
+
why +
|
|
34
|
+
'): "' +
|
|
35
|
+
value +
|
|
36
|
+
'". ' +
|
|
37
|
+
'Write it as scheme://host:port (or just host:port) — e.g. http://proxy.example:8080, ' +
|
|
38
|
+
'https://proxy.example:8443 or socks5://proxy.example:1080. ' +
|
|
39
|
+
'Leave it empty to connect directly. Authenticated proxies are not supported in this version.');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Turn a configured proxy address into Playwright's proxy object.
|
|
43
|
+
*
|
|
44
|
+
* Accepted shapes: `host:port` (treated as HTTP, like Playwright does), `http://host:port`,
|
|
45
|
+
* `https://host:port`, `socks5://host:port`. The port is required and must be 1–65535.
|
|
46
|
+
*
|
|
47
|
+
* @param raw - the configured value; empty/undefined means "connect directly"
|
|
48
|
+
* @returns the proxy settings (server + local bypass), or undefined for a direct connection
|
|
49
|
+
* @throws Error with an actionable message when the address cannot be used
|
|
50
|
+
*/
|
|
51
|
+
export function normalizeProxy(raw) {
|
|
52
|
+
const value = (raw ?? '').trim();
|
|
53
|
+
if (value === '')
|
|
54
|
+
return undefined;
|
|
55
|
+
// Credentials would have to live in plain text in the user's patch file, and SOCKS5
|
|
56
|
+
// authentication is not supported by the browser anyway — refuse before launch.
|
|
57
|
+
if (/^[a-z][a-z0-9+.-]*:\/\/[^/?#]*@/i.test(value) || /^[^/?#]*@/.test(value)) {
|
|
58
|
+
throw new Error('launch.proxy must not carry a username or password (found a credential in "' +
|
|
59
|
+
value +
|
|
60
|
+
'"). ' +
|
|
61
|
+
'This version supports proxies WITHOUT authentication only: a password in the config ' +
|
|
62
|
+
'would be stored in plain text in your patch file, and SOCKS5 authentication is not ' +
|
|
63
|
+
'supported by Chromium/Playwright at all. Configure an unauthenticated proxy, or leave ' +
|
|
64
|
+
'this empty to connect directly.');
|
|
65
|
+
}
|
|
66
|
+
// Short form: Playwright itself reads `host:port` as HTTP, so accept it the same way.
|
|
67
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : 'http://' + value;
|
|
68
|
+
// Check the port by hand first: `new URL('http://host:abc')` throws a generic message,
|
|
69
|
+
// and "the port is not a number" is what the user needs to read.
|
|
70
|
+
const hostPart = withScheme.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '').split('/')[0] ?? '';
|
|
71
|
+
const hostOnly = hostPart.split('?')[0] ?? '';
|
|
72
|
+
const portText = (() => {
|
|
73
|
+
if (hostOnly.startsWith('[')) {
|
|
74
|
+
const close = hostOnly.indexOf(']');
|
|
75
|
+
if (close === -1)
|
|
76
|
+
return '';
|
|
77
|
+
const rest = hostOnly.slice(close + 1);
|
|
78
|
+
return rest.startsWith(':') ? rest.slice(1) : '';
|
|
79
|
+
}
|
|
80
|
+
const colon = hostOnly.lastIndexOf(':');
|
|
81
|
+
return colon === -1 ? '' : hostOnly.slice(colon + 1);
|
|
82
|
+
})();
|
|
83
|
+
if (portText === '')
|
|
84
|
+
reject(value, 'the port is missing');
|
|
85
|
+
if (!/^\d+$/.test(portText ?? ''))
|
|
86
|
+
reject(value, 'the port is not a number');
|
|
87
|
+
// Check the range before `new URL` does: an out-of-range port makes the constructor
|
|
88
|
+
// throw a generic error, and the useful sentence is "the port must be 1–65535".
|
|
89
|
+
const portNumber = Number(portText);
|
|
90
|
+
if (!Number.isInteger(portNumber) || portNumber < 1 || portNumber > 65535) {
|
|
91
|
+
reject(value, 'the port must be 1–65535');
|
|
92
|
+
}
|
|
93
|
+
let url;
|
|
94
|
+
try {
|
|
95
|
+
url = new URL(withScheme);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return reject(value, 'it does not parse as a URL');
|
|
99
|
+
}
|
|
100
|
+
if (!ALLOWED_SCHEMES.has(url.protocol.toLowerCase())) {
|
|
101
|
+
reject(value, 'only http, https and socks5 are supported');
|
|
102
|
+
}
|
|
103
|
+
if (url.hostname === '')
|
|
104
|
+
reject(value, 'the host is empty');
|
|
105
|
+
if (url.port === '' || !/^\d+$/.test(url.port))
|
|
106
|
+
reject(value, 'the port is not a number');
|
|
107
|
+
const port = Number(url.port);
|
|
108
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
109
|
+
reject(value, 'the port must be 1–65535');
|
|
110
|
+
// A non-special scheme (socks5) parses with an EMPTY pathname while http/https give '/',
|
|
111
|
+
// so both are "no path". Anything else means the user appended something.
|
|
112
|
+
if ((url.pathname !== '' && url.pathname !== '/') || url.search !== '' || url.hash !== '') {
|
|
113
|
+
reject(value, 'a proxy address has no path, query or fragment');
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
server: url.protocol + '//' + url.hostname + ':' + url.port,
|
|
117
|
+
// Never proxy the plugin's own local pages.
|
|
118
|
+
bypass: PROXY_BYPASS,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The spread-in fragment for Playwright's launch options: `{}` when direct,
|
|
123
|
+
* `{ proxy: … }` when configured. Every launch path uses this so they cannot drift.
|
|
124
|
+
*
|
|
125
|
+
* @param raw - the configured proxy value
|
|
126
|
+
* @returns an object to spread into the launch options
|
|
127
|
+
*/
|
|
128
|
+
export function proxyLaunchOption(raw) {
|
|
129
|
+
const proxy = normalizeProxy(raw);
|
|
130
|
+
return proxy === undefined ? {} : { proxy };
|
|
131
|
+
}
|
package/lib/settings-card.js
CHANGED
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
import * as runtimeState from "./runtime-state.js";
|
|
63
63
|
// The patch writer lives in its own dependency-free module so it can be tested
|
|
64
64
|
// without a DSH host (a half-written patch layer would break every plugin load).
|
|
65
|
-
import { homePatchPath, parseGatesBlock, writeGates } from "./gates-patch.js";
|
|
65
|
+
import { homePatchPath, parseBrowserBlockProxy, parseGatesBlock, writeGates, writeProxy } from "./gates-patch.js";
|
|
66
|
+
import { normalizeProxy } from "./proxy.js";
|
|
66
67
|
import { onPersistPlayedChanged, resetPlayed } from "./swarm-intro.js";
|
|
67
68
|
import { readFileSync } from 'node:fs';
|
|
68
69
|
import { clean, isTargetId, resolvePaths, scan } from "./cache-clean.js";
|
|
@@ -90,6 +91,14 @@ function readGates() {
|
|
|
90
91
|
* shows the *effective* gates from readGates(); this is the file-level view used
|
|
91
92
|
* for diagnostics when the two disagree (i.e. the reload has not landed yet).
|
|
92
93
|
*/
|
|
94
|
+
function managedBlockProxy() {
|
|
95
|
+
try {
|
|
96
|
+
return parseBrowserBlockProxy(readFileSync(homePatchPath(), 'utf8'));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return '';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
93
102
|
function managedBlockGates() {
|
|
94
103
|
try {
|
|
95
104
|
const text = readFileSync(homePatchPath(), 'utf8');
|
|
@@ -391,6 +400,8 @@ export function apply(ctx) {
|
|
|
391
400
|
// What the user's own layer says right now. Differs from the above
|
|
392
401
|
// between a write and the live reload that applies it.
|
|
393
402
|
patchGates: managedBlockGates() ?? null,
|
|
403
|
+
// launch.proxy(设置页的「代理」输入框)。空 = 直连。
|
|
404
|
+
proxy: managedBlockProxy(),
|
|
394
405
|
});
|
|
395
406
|
},
|
|
396
407
|
}));
|
|
@@ -523,5 +534,55 @@ export function apply(ctx) {
|
|
|
523
534
|
sendJson(res, 200, { logins });
|
|
524
535
|
},
|
|
525
536
|
}));
|
|
537
|
+
// ---- launch.proxy:设置页里的「代理」输入框 ----
|
|
538
|
+
// 只改托管块里 `playwright.launch.proxy` 这一行(与三个闸门同一个模式),
|
|
539
|
+
// 其它键逐字节不动;空字符串 = 删掉那一行 = 直连。
|
|
540
|
+
ctx.effect(() => ctx.webServer.register({
|
|
541
|
+
kind: 'exact',
|
|
542
|
+
path: `${PREFIX}/proxy`,
|
|
543
|
+
handler: async (req, res) => {
|
|
544
|
+
if (req.method !== 'POST')
|
|
545
|
+
return sendJson(res, 405, { error: { code: 'METHOD', message: 'POST only' } });
|
|
546
|
+
if (req.headers[HEADER] !== 'browser-toggle')
|
|
547
|
+
return sendJson(res, 403, { error: { code: 'CSRF', message: 'missing plugin header' } });
|
|
548
|
+
const body = await readBody(req);
|
|
549
|
+
if (!body.ok)
|
|
550
|
+
return sendJson(res, 400, { error: { code: 'BODY_READ', message: body.error } });
|
|
551
|
+
let payload;
|
|
552
|
+
try {
|
|
553
|
+
payload = JSON.parse(body.text || '{}');
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
return sendJson(res, 400, { error: { code: 'INVALID_JSON', message: 'body must be JSON' } });
|
|
557
|
+
}
|
|
558
|
+
if (typeof payload.proxy !== 'string')
|
|
559
|
+
return sendJson(res, 400, { error: { code: 'INVALID_BODY', message: '`proxy` must be a string' } });
|
|
560
|
+
// 校验直接用 proxy.ts 的同一份实现:非法地址当场报错,文件一个字节不动。
|
|
561
|
+
let server = '';
|
|
562
|
+
try {
|
|
563
|
+
const normalized = normalizeProxy(payload.proxy);
|
|
564
|
+
server = normalized === undefined ? '' : normalized.server;
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
return sendJson(res, 400, {
|
|
568
|
+
error: { code: 'INVALID_PROXY', message: String(error?.message ?? error) },
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
const written = await writeProxy(server);
|
|
573
|
+
sendJson(res, 200, {
|
|
574
|
+
proxy: written.proxy,
|
|
575
|
+
patch: written.patch,
|
|
576
|
+
// 代理是浏览器启动参数:插件在加载时读配置,所以下次启动 DSH 才生效。
|
|
577
|
+
appliedOn: 'restart',
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
sendJson(res, 500, {
|
|
582
|
+
error: { code: 'PATCH_WRITE', message: String(error?.message ?? error) },
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
}));
|
|
526
587
|
}
|
|
527
588
|
export default { name, inject, apply };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-browser-playwright-codex",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
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",
|