claude-coder 1.5.7 → 1.6.2

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 CHANGED
@@ -60,6 +60,7 @@ claude-coder run "实现用户注册和登录功能"
60
60
  | `claude-coder validate` | 手动校验 |
61
61
  | `claude-coder status` | 查看进度和成本 |
62
62
  | `claude-coder config sync` | 同步配置到 ~/.claude/ |
63
+ | `claude-coder config mcp <mode>` | 快速切换 Playwright 模式 |
63
64
 
64
65
  **选项**:`--max N` 限制 session 数(默认 50),`--pause N` 每 N 个 session 暂停确认(默认不暂停)。
65
66
 
@@ -97,10 +98,10 @@ your-project/
97
98
  progress.json # 会话历史 + 成本
98
99
  tests.json # 验证记录
99
100
  test.env # 测试凭证(API Key 等,可选)
100
- playwright-auth.json # 登录状态快照(备份参考,auth 命令生成)
101
- browser-profile/ # 持久化浏览器 Profile(MCP 实际使用)
101
+ playwright-auth.json # 登录状态快照(isolated 模式,auth 命令生成)
102
102
  .runtime/ # 临时文件
103
103
  logs/ # 每 session 独立日志(含工具调用记录)
104
+ browser-profile/ # 持久化浏览器 Profile(persistent 模式,auth 命令生成)
104
105
  requirements.md # 需求文档(可选)
105
106
  ```
106
107
 
package/bin/cli.js CHANGED
@@ -11,7 +11,7 @@ const COMMANDS = {
11
11
  auth: { desc: '导出 Playwright 登录状态', usage: 'claude-coder auth [url]' },
12
12
  validate: { desc: '手动校验上次 session', usage: 'claude-coder validate' },
13
13
  status: { desc: '查看任务进度和成本', usage: 'claude-coder status' },
14
- config: { desc: '配置管理', usage: 'claude-coder config sync' },
14
+ config: { desc: '配置管理', usage: 'claude-coder config sync | config mcp <mode>' },
15
15
  };
16
16
 
17
17
  function showHelp() {
@@ -32,6 +32,7 @@ function showHelp() {
32
32
  console.log(' claude-coder add "..." --model opus-4 指定模型追加任务');
33
33
  console.log(' claude-coder auth 导出 Playwright 登录状态');
34
34
  console.log(' claude-coder auth http://localhost:8080 指定登录 URL');
35
+ console.log(' claude-coder config mcp persistent 快速切换 Playwright 模式');
35
36
  console.log(' claude-coder status 查看进度和成本');
36
37
  console.log(`\n前置条件: npm install -g @anthropic-ai/claude-agent-sdk`);
37
38
  }
@@ -152,8 +153,32 @@ async function main() {
152
153
  const config = require('../src/config');
153
154
  if (positional[0] === 'sync') {
154
155
  config.syncToGlobal();
156
+ } else if (positional[0] === 'mcp') {
157
+ const validModes = ['persistent', 'isolated', 'extension'];
158
+ const mode = positional[1];
159
+ if (!mode || !validModes.includes(mode)) {
160
+ console.error(`用法: claude-coder config mcp <${validModes.join('|')}>`);
161
+ process.exit(1);
162
+ }
163
+ const ok = config.updateEnvVar('MCP_PLAYWRIGHT_MODE', mode);
164
+ if (!ok) {
165
+ console.error('未找到配置文件,请先运行 claude-coder setup');
166
+ process.exit(1);
167
+ }
168
+ config.updateEnvVar('MCP_PLAYWRIGHT', 'true');
169
+ const { updateMcpConfig } = require('../src/auth');
170
+ updateMcpConfig(config.paths(), mode);
171
+ const { log } = config;
172
+ log('ok', `Playwright 模式已切换为: ${mode}`);
173
+ if (mode === 'persistent') {
174
+ log('info', '运行 claude-coder auth <URL> 完成首次登录');
175
+ } else if (mode === 'isolated') {
176
+ log('info', '运行 claude-coder auth <URL> 录制登录状态');
177
+ } else {
178
+ log('info', '确保已安装 Playwright MCP Bridge 扩展并启动浏览器');
179
+ }
155
180
  } else {
156
- console.error('用法: claude-coder config sync');
181
+ console.error('用法: claude-coder config sync | config mcp <mode>');
157
182
  process.exit(1);
158
183
  }
159
184
  break;
@@ -51,8 +51,8 @@ Agent 在单次 session 中应最大化推进任务进度。**任何非致命问
51
51
  | 文件 | git 状态 | 说明 |
52
52
  |------|---------|------|
53
53
  | `test.env` | .gitignore | Agent 可写入发现的 API Key、测试账号 |
54
- | `playwright-auth.json` | .gitignore | 登录状态快照备份(`claude-coder auth` 生成) |
55
- | `browser-profile/` | .gitignore | 持久化浏览器 Profile(MCP 实际使用) |
54
+ | `playwright-auth.json` | .gitignore | cookies + localStorage 快照(isolated 模式,`claude-coder auth` 生成) |
55
+ | `.runtime/browser-profile/` | .gitignore | 持久化浏览器 Profile(persistent 模式,`claude-coder auth` 生成) |
56
56
  | `session_result.json` | git-tracked | Agent 每次 session 覆盖写入 |
57
57
  | `tasks.json` | git-tracked | Agent 修改 status 字段 |
58
58
 
@@ -208,10 +208,10 @@ templates/
208
208
  | `tasks.json` | 首次扫描 | 任务列表 + 状态跟踪 |
209
209
  | `progress.json` | 每次 session 结束 | 结构化会话日志 + 成本记录 |
210
210
  | `session_result.json` | 每次 session 结束 | 当前 session 结果(扁平格式,向后兼容旧 `current` 包装) |
211
- | `playwright-auth.json` | `claude-coder auth` | 登录状态快照(备份参考) |
212
- | `browser-profile/` | `claude-coder auth` | 持久化浏览器 Profile(MCP 通过 `--user-data-dir` 使用) |
211
+ | `playwright-auth.json` | `claude-coder auth`(isolated 模式) | 浏览器 cookies + localStorage 快照 |
212
+ | `.runtime/browser-profile/` | `claude-coder auth`(persistent 模式) | 持久化浏览器 Profile(MCP 通过 `--user-data-dir` 使用) |
213
213
  | `tests.json` | 首次测试时 | 验证记录(防止反复测试) |
214
- | `.runtime/` | 运行时 | 临时文件(phase、step、logs/);工具调用记录合并到 session log |
214
+ | `.runtime/` | 运行时 | 临时文件(logs/、browser-profile/);工具调用记录合并到 session log |
215
215
 
216
216
  ---
217
217
 
@@ -267,7 +267,7 @@ flowchart TB
267
267
  | 5 | `docsHint` | profile.existing_docs 非空或 profile 有缺陷 | Step 4:读文档后再编码;profile 缺陷时提示 Agent 在 Step 6 补全 services/docs |
268
268
  | 6 | `taskHint` | tasks.json 存在且有待办任务 | Step 1:跳过读取 tasks.json,harness 已注入当前任务上下文 + 项目绝对路径 |
269
269
  | 6b | `testEnvHint` | 始终注入(内容因 test.env 是否存在而不同) | Step 5:存在时提示加载;不存在时告知可创建 |
270
- | 6c | `playwrightAuthHint` | .claude-coder/browser-profile/ 存在 | Step 5:提示 Agent MCP 使用持久化浏览器 Profile,首次需手动登录 |
270
+ | 6c | `playwrightAuthHint` | MCP_PLAYWRIGHT=true(按 playwrightMode 动态生成) | Step 5:告知 Agent 当前 Playwright 模式(persistent/isolated/extension)及认证状态 |
271
271
  | 7 | `memoryHint` | session_result.json 存在(扁平格式) | Step 1:跳过读取 session_result.json,harness 已注入上次会话摘要 |
272
272
  | 8 | `serviceHint` | 始终注入 | Step 6:单次模式停止服务,连续模式保持服务运行 |
273
273
  | 9 | `toolGuidance` | 始终注入 | 全局:工具使用规范(Grep/Glob/Read/LS/MultiEdit/Task 替代 bash 命令),非 Claude 模型必需 |
@@ -408,8 +408,8 @@ Harness 在 `buildCodingPrompt()` 中预读 `session_result.json`,将上次会
408
408
  | `tasks.json` | Agent(仅 `status` 字段) | 修改 `status` | tracked |
409
409
  | `project_profile.json` | Agent(仅扫描阶段) | 扫描时写入 | tracked |
410
410
  | `test.env` | Agent + 用户 | 可追加写入 | .gitignore |
411
- | `playwright-auth.json` | 用户(`claude-coder auth`) | 快照备份 | .gitignore |
412
- | `browser-profile/` | 用户(`claude-coder auth`) | MCP 自动维护 | .gitignore |
411
+ | `playwright-auth.json` | 用户(`claude-coder auth`,isolated 模式) | cookies + localStorage 快照 | .gitignore |
412
+ | `.runtime/browser-profile/` | 用户(`claude-coder auth`,persistent 模式) | 持久化浏览器 Profile | .gitignore |
413
413
 
414
414
  ---
415
415
 
@@ -1,145 +1,178 @@
1
- # 测试凭证持久化方案
1
+ # Playwright MCP 浏览器模式与凭证管理
2
2
 
3
- ## 设计思想
3
+ ## 三种模式一览
4
4
 
5
- claude-coder 的核心目标是让 Agent **完全自主测试**,不因凭证缺失而中断。测试中涉及三类凭证:
5
+ | | persistent(默认推荐) | isolated | extension |
6
+ |---|---|---|---|
7
+ | **一句话** | **懒人模式** — 登录一次,永久生效 | **开发模式** — 验证登录流程的自动化测试 | 连接真实浏览器(实验性) |
8
+ | **使用的浏览器** | Chrome for Testing(Playwright 自带) | Chrome for Testing(Playwright 自带) | **用户的真实 Chrome/Edge** |
9
+ | **登录态** | 持久化,关闭后下次自动恢复 | 每次从 JSON 快照加载(auth 录制一次) | 直接复用浏览器已有登录态 |
10
+ | **典型场景** | Google SSO、企业内网 API 文档拉取、日常维护开发 | 验证登录流程本身、需要可重复的干净测试环境 | 需要浏览器插件或绕过自动化检测 |
11
+ | **状态存储** | `.claude-coder/.runtime/browser-profile/` | `.claude-coder/playwright-auth.json` | 无(浏览器自身管理) |
12
+ | **前置安装** | `npx playwright install chromium` | `npx playwright install chromium` | Playwright MCP Bridge 扩展 |
13
+ | **.mcp.json 参数** | `--user-data-dir=<path>` | `--isolated --storage-state=<path>` | `--extension` |
6
14
 
7
- | 类型 | 示例 | 特点 |
8
- |------|------|------|
9
- | 浏览器状态 | 登录 cookies、localStorage 中的用户配置 | 有过期时间,跨 session 需要持久化 |
10
- | API Key | OPENAI_API_KEY、ZHIPU_API_KEY | 长期有效,需安全存储 |
11
- | 测试账号 | 注册的测试用户名密码、生成的 token | 可能是 Agent 自己创建的,需跨 session 传递 |
15
+ ### 如何选择
12
16
 
13
- **核心原则**:
14
- 1. **Agent 可自行发现并持久化凭证** — 测试中发现需要的 API Key 或账号,直接写入 `test.env`
15
- 2. **凭证不受回滚影响** `git reset --hard` 不会摧毁已保存的凭证
16
- 3. **零手动干预** — 首次浏览器登录后,后续由持久化 profile 自动处理
17
+ - **persistent(推荐)**:适合需要 Google 登录态、内网 API 获取等场景。登录一次,后续所有 MCP 会话自动复用。
18
+ - **isolated**:适合需要验证登录流程的自动化测试,或需要每次干净环境的场景。cookies 过期后需重新 `auth`。
19
+ - **extension**(实验性):适合需要浏览器插件(VPN、广告拦截等)的场景。需安装 Chrome 扩展。
17
20
 
18
21
  ---
19
22
 
20
- ## 持久化架构
23
+ ## 前置安装
21
24
 
22
- ```
23
- .claude-coder/
24
- .env ← 模型配置(ANTHROPIC_API_KEY 等) [用户配置]
25
- test.env ← 测试凭证(API Key、测试账号等) [Agent 可写]
26
- playwright-auth.json ← 浏览器登录状态(MCP 每次会话加载) [auth 命令生成]
25
+ ### persistent / isolated 模式
26
+
27
+ ```bash
28
+ # 安装 Playwright 自带的 Chromium 浏览器(Chrome for Testing)
29
+ npx playwright install chromium
27
30
  ```
28
31
 
29
- ### 文件生命周期
30
-
31
- | 文件 | 创建方 | 写入方 | 回滚保护 | 生命周期 |
32
- |------|--------|--------|----------|----------|
33
- | `.env` | `claude-coder setup` | 用户 | 是 | 长期 |
34
- | `test.env` | Agent 或用户 | Agent + 用户 | 是 | 长期,按需更新 |
35
- | `playwright-auth.json` | `claude-coder auth` | auth 命令 | 是 | 长期,MCP 每次会话自动加载;如需更新重新运行 auth |
36
-
37
- ### 技术实现:为什么用 `--isolated --storage-state`
38
-
39
- | 维度 | `--user-data-dir`(persistent) | `--isolated --storage-state`(当前方案) |
40
- |------|--------------------------|--------------------------------------|
41
- | 上下文类型 | 持久化上下文 | 隔离上下文 |
42
- | localStorage | **已知 Bug #14949:`launchPersistentContext` 不注入 localStorage** | 从 JSON 可靠注入 |
43
- | Cookies | Profile 自动续期 | 每次从 JSON 加载(静态) |
44
- | 状态保持 | 跨会话自动保持 | 每次会话从 JSON 重新加载 |
45
- | 适用场景 | 需要 cookie 自动续期(Google OAuth) | 需要 localStorage 注入(API Key 等) |
46
-
47
- > **选择 `--isolated --storage-state` 的原因**:
48
- > 经实测验证,Playwright 的 `launchPersistentContext` + `storageState` 存在已知缺陷
49
- >(Issue #14949):localStorage 完全不注入。而 `--isolated` 模式使用 `newContext({ storageState })`,
50
- > localStorage 可靠注入。claude-coder 的典型场景是注入 API Key(存储在 localStorage),
51
- > 因此选择 `--isolated --storage-state` 作为默认方案。
52
- >
53
- > 如需 cookie 持久化(Google OAuth/SSO),可手动修改 `.mcp.json` 为 `--user-data-dir` 模式,
54
- > 但需在 MCP 浏览器中手动登录一次。
32
+ 安装后位于 `~/Library/Caches/ms-playwright/chromium-*/`(macOS)。这不是你的真实 Chrome,是 Playwright 专用的自动化浏览器。
33
+
34
+ ### extension 模式
35
+
36
+ 1. 在真实 Chrome/Edge 中安装 [Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm) 扩展
37
+ 2. 确保扩展已启用
38
+ 3. 无需安装 Chromium
55
39
 
56
40
  ---
57
41
 
58
- ## 核心流程
42
+ ## 配置流程
59
43
 
60
- ### 流程 1:Agent 自动发现凭证
44
+ ### Step 1:选择模式
61
45
 
62
- ```
63
- Agent 测试 → 发现需要 API Key → 写入 test.env → 下次 session 自动加载
46
+ ```bash
47
+ claude-coder setup
48
+ # → 启用 Playwright MCP → 选择模式(persistent / isolated / extension)
49
+ # → 模式写入 .claude-coder/.env 的 MCP_PLAYWRIGHT_MODE
64
50
  ```
65
51
 
66
- ### 流程 2:用户预配置浏览器登录态
52
+ ### Step 2:运行认证
67
53
 
68
- ```
69
- 用户运行 claude-coder auth url
70
- playwright codegen 打开浏览器 → 手动登录 → 关闭浏览器
71
- → cookies + localStorage 保存到 playwright-auth.json
72
- 更新 .mcp.json(--isolated --storage-state 指向 playwright-auth.json)
73
- 每次 MCP 会话自动从 JSON 加载状态(无需手动登录)
74
- → 如需更新状态,重新运行 claude-coder auth
54
+ ```bash
55
+ # persistent / isolated:打开浏览器,手动登录后关闭
56
+ claude-coder auth http://your-target-url.com
57
+
58
+ # extension:不开浏览器,只生成 .mcp.json 配置
59
+ claude-coder auth
75
60
  ```
76
61
 
77
- ### 流程 3:用户预配置 API Key
62
+ `auth` 命令会自动完成:
63
+ 1. 根据当前模式执行对应的认证流程
64
+ 2. 生成/更新 `.mcp.json`(Claude Code SDK 读取此文件启动 MCP 服务)
65
+ 3. 更新 `.gitignore`
66
+ 4. 启用 `.env` 中 `MCP_PLAYWRIGHT=true`
78
67
 
79
- ```
80
- 用户编辑 test.env → 填入 API Key → Agent 测试前 source 加载
68
+ ### Step 3:开始使用
69
+
70
+ ```bash
71
+ claude-coder run "你的需求"
72
+ # Agent 自动通过 Playwright MCP 工具操作浏览器
81
73
  ```
82
74
 
83
75
  ---
84
76
 
85
- ## CLI 命令
77
+ ## 各模式详细说明
86
78
 
87
- ### `claude-coder auth [url]`
79
+ ### persistent 模式(默认)
88
80
 
89
- 配置持久化浏览器认证:
81
+ **原理**:使用 `--user-data-dir` 创建持久化浏览器配置文件(类似 Chrome Profile)。所有 cookies、localStorage、IndexedDB、Service Worker 状态都保留在磁盘上。
90
82
 
91
- ```bash
92
- # 默认打开 http://localhost:3000
93
- claude-coder auth
83
+ ```
84
+ .claude-coder/.runtime/browser-profile/ ← 持久化浏览器配置(~20-50MB)
85
+ ```
86
+
87
+ **优点**:
88
+ - 登录状态完整保留,包括 Google SSO、OAuth 回调等复杂流程
89
+ - 无需重复登录
90
+ - cookies 自动续期
91
+
92
+ **缺点**:
93
+ - 配置目录较大
94
+ - 不同 session 共享状态(非隔离)
94
95
 
95
- # 指定 URL(如内部 API 文档平台)
96
- claude-coder auth http://testyapi.example.com/group/2245
96
+ ### isolated 模式
97
+
98
+ **原理**:使用 `--isolated --storage-state` 将 cookies + localStorage 快照注入新的隔离上下文。每次 MCP 会话从 JSON 文件重新加载。
99
+
100
+ ```
101
+ .claude-coder/playwright-auth.json ← cookies + localStorage 快照(~10-20KB)
97
102
  ```
98
103
 
99
- **自动完成**:
100
- 1. 启动 `playwright codegen`,用户手动登录后关闭浏览器
101
- 2. cookies + localStorage 保存到 `.claude-coder/playwright-auth.json`
102
- 3. 创建/更新 `.mcp.json`,配置 `--isolated --storage-state=.claude-coder/playwright-auth.json`
103
- 4. 添加 `.gitignore` 条目(`playwright-auth.json`)
104
- 5. 启用 `.claude-coder/.env` `MCP_PLAYWRIGHT=true`
104
+ **优点**:
105
+ - 每次 session 从相同状态开始,可重复性好
106
+ - 状态文件小,可版本控制(脱敏后)
107
+
108
+ **缺点**:
109
+ - cookies 过期后需重新 `claude-coder auth`
110
+ - Google SSO 等复杂登录可能无法完整恢复(缺少 IndexedDB/Service Worker 状态)
111
+
112
+ ### extension 模式
113
+
114
+ **原理**:使用 `--extension` 通过 Chrome 扩展(WebSocket CDP relay)连接到用户正在运行的真实浏览器。
115
+
116
+ **优点**:
117
+ - 直接复用浏览器已有登录态,无需额外认证
118
+ - 可使用浏览器已安装的扩展(VPN、广告拦截等)
119
+ - 绕过自动化检测
120
+
121
+ **缺点**:
122
+ - 需要安装 [Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm) 扩展
123
+ - Agent 操作会影响用户正在使用的浏览器
124
+ - 首次连接需要用户审批(可用 Token 自动跳过)
105
125
 
106
126
  ---
107
127
 
108
- ## 场景示例
128
+ ## 测试凭证管理
109
129
 
110
- ### 场景 1:全栈项目首次测试
130
+ 除浏览器登录态外,Agent 还需要 API Key、测试账号等凭证:
131
+
132
+ | 文件 | 创建方 | 写入方 | 用途 |
133
+ |------|--------|--------|------|
134
+ | `.claude-coder/.env` | `setup` | 用户 | 模型配置、MCP 开关 |
135
+ | `.claude-coder/test.env` | Agent 或用户 | Agent + 用户 | 测试凭证(API Key、测试账号) |
111
136
 
112
137
  ```bash
113
- claude-coder setup
138
+ # 用户预配置
114
139
  cat >> .claude-coder/test.env << 'EOF'
115
140
  OPENAI_API_KEY=sk-xxx
141
+ TEST_USER=testuser@example.com
142
+ TEST_PASSWORD=xxx
116
143
  EOF
117
- claude-coder auth http://localhost:3000
118
- claude-coder run
119
- # MCP 每次会话自动从 playwright-auth.json 加载 localStorage 和 cookies
120
144
  ```
121
145
 
122
- ### 场景 2:内部系统(Google OAuth / SSO)
146
+ Agent 在测试前会 `source .claude-coder/test.env` 加载凭证。发现新凭证需求时也会自动追加写入。
123
147
 
124
- ```bash
125
- claude-coder auth http://testyapi.example.com/group/2245
126
- # 在弹出的浏览器中完成登录,关闭后状态保存到 JSON
127
- # MCP 每次会话自动加载此状态
128
- ```
129
-
130
- > **关于 Google OAuth**:`--isolated` 模式每次创建新上下文,Google 可能要求重新验证。
131
- > 如需 cookie 持久化,可手动修改 `.mcp.json` 为 `--user-data-dir` 模式(但 localStorage 不会注入)。
148
+ ---
132
149
 
133
- ### 场景 3:更新登录状态
150
+ ## 切换模式
134
151
 
135
152
  ```bash
136
- claude-coder auth http://localhost:3000
137
- # 重新登录,覆盖 playwright-auth.json
153
+ # 推荐:一行命令切换(同时更新 .env 和 .mcp.json)
154
+ claude-coder config mcp persistent
155
+ claude-coder config mcp isolated
156
+ claude-coder config mcp extension
157
+
158
+ # 如果新模式需要登录(persistent / isolated),还需运行 auth
159
+ claude-coder auth <URL>
138
160
  ```
139
161
 
140
- ### 场景 4:清除登录状态
162
+ > **说明**:`config mcp` 会同时更新 `.env` 中的 `MCP_PLAYWRIGHT_MODE` 和重新生成 `.mcp.json`,无需重走 `setup` 全流程。如果之前已经 auth 过对应模式(如 persistent 的 browser-profile 还在),切换回去后无需重新 auth。
163
+
164
+ ---
165
+
166
+ ## 清理
141
167
 
142
168
  ```bash
169
+ # 清除 persistent 模式配置
170
+ rm -rf .claude-coder/.runtime/browser-profile
171
+
172
+ # 清除 isolated 模式登录状态
143
173
  rm .claude-coder/playwright-auth.json
144
- # 下次运行 claude-coder auth 重新配置
174
+
175
+ # 完全重置 Playwright 配置
176
+ rm .mcp.json
177
+ # 然后重新运行 claude-coder auth
145
178
  ```
package/docs/README.en.md CHANGED
@@ -60,6 +60,7 @@ Each session, the agent autonomously follows 6 steps: restore context → env ch
60
60
  | `claude-coder validate` | Manually validate last session |
61
61
  | `claude-coder status` | View progress and costs |
62
62
  | `claude-coder config sync` | Sync config to ~/.claude/ |
63
+ | `claude-coder config mcp <mode>` | Switch Playwright mode (persistent/isolated/extension) |
63
64
 
64
65
  **Options**: `--max N` limit sessions (default 50), `--pause N` pause every N sessions (default: no pause).
65
66
 
@@ -85,10 +86,10 @@ your-project/
85
86
  progress.json # Session history + costs
86
87
  tests.json # Verification records
87
88
  test.env # Test credentials (API keys, optional)
88
- playwright-auth.json # Login state snapshot (backup, via auth command)
89
- browser-profile/ # Persistent browser profile (used by MCP)
89
+ playwright-auth.json # Login state snapshot (isolated mode, via auth command)
90
90
  .runtime/ # Temp files
91
91
  logs/ # Per-session logs (with tool call traces)
92
+ browser-profile/ # Persistent browser profile (persistent mode, via auth command)
92
93
  requirements.md # Requirements (optional)
93
94
  ```
94
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-coder",
3
- "version": "1.5.7",
3
+ "version": "1.6.2",
4
4
  "description": "Claude Coder — Autonomous coding agent harness powered by Claude Code SDK. Scan, plan, code, validate, git-commit in a loop.",
5
5
  "bin": {
6
6
  "claude-coder": "bin/cli.js"
@@ -38,5 +38,7 @@
38
38
  "peerDependencies": {
39
39
  "@anthropic-ai/claude-agent-sdk": ">=0.1.0"
40
40
  },
41
- "dependencies": {}
41
+ "optionalDependencies": {
42
+ "playwright": "^1.58.2"
43
+ }
42
44
  }
package/src/auth.js CHANGED
@@ -1,9 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
+ const os = require('os');
4
5
  const path = require('path');
5
6
  const { execSync } = require('child_process');
6
- const { paths, log, getProjectRoot, ensureLoopDir } = require('./config');
7
+ const { paths, loadConfig, log, getProjectRoot, ensureLoopDir } = require('./config');
7
8
 
8
9
  function updateGitignore(entry) {
9
10
  const gitignorePath = path.join(getProjectRoot(), '.gitignore');
@@ -18,31 +19,35 @@ function updateGitignore(entry) {
18
19
  log('ok', `.gitignore 已添加: ${entry}`);
19
20
  }
20
21
 
21
- function updateMcpConfig() {
22
- const p = paths();
22
+ function updateMcpConfig(p, mode) {
23
23
  let mcpConfig = {};
24
24
  if (fs.existsSync(p.mcpConfig)) {
25
- try {
26
- mcpConfig = JSON.parse(fs.readFileSync(p.mcpConfig, 'utf8'));
27
- } catch {
28
- log('warn', '.mcp.json 解析失败,将覆盖');
29
- }
25
+ try { mcpConfig = JSON.parse(fs.readFileSync(p.mcpConfig, 'utf8')); } catch {}
30
26
  }
31
27
 
32
28
  if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
33
29
 
34
- const relAuthPath = path.relative(getProjectRoot(), p.playwrightAuth).split(path.sep).join('/');
35
- mcpConfig.mcpServers.playwright = {
36
- command: 'npx',
37
- args: [
38
- '@playwright/mcp@latest',
39
- '--isolated',
40
- `--storage-state=${relAuthPath}`,
41
- ],
42
- };
30
+ const args = ['@playwright/mcp@latest'];
31
+
32
+ switch (mode) {
33
+ case 'persistent': {
34
+ const relProfile = path.relative(getProjectRoot(), p.browserProfile).split(path.sep).join('/');
35
+ args.push(`--user-data-dir=${relProfile}`);
36
+ break;
37
+ }
38
+ case 'isolated': {
39
+ const relAuth = path.relative(getProjectRoot(), p.playwrightAuth).split(path.sep).join('/');
40
+ args.push('--isolated', `--storage-state=${relAuth}`);
41
+ break;
42
+ }
43
+ case 'extension':
44
+ args.push('--extension');
45
+ break;
46
+ }
43
47
 
48
+ mcpConfig.mcpServers.playwright = { command: 'npx', args };
44
49
  fs.writeFileSync(p.mcpConfig, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf8');
45
- log('ok', `.mcp.json 已配置 Playwright MCP (isolated + storage-state: ${relAuthPath})`);
50
+ log('ok', `.mcp.json 已配置 Playwright MCP (${mode} 模式)`);
46
51
  }
47
52
 
48
53
  function enableMcpPlaywrightEnv() {
@@ -60,30 +65,108 @@ function enableMcpPlaywrightEnv() {
60
65
  log('ok', '.claude-coder/.env 已设置 MCP_PLAYWRIGHT=true');
61
66
  }
62
67
 
63
- async function auth(url) {
64
- ensureLoopDir();
65
- const p = paths();
66
- const targetUrl = url || 'http://localhost:3000';
68
+ // ── persistent 模式:启动持久化浏览器让用户登录 ──
69
+
70
+ async function authPersistent(url, p) {
71
+ const profileDir = p.browserProfile;
72
+ if (!fs.existsSync(profileDir)) fs.mkdirSync(profileDir, { recursive: true });
73
+
74
+ const lockFile = path.join(profileDir, 'SingletonLock');
75
+ if (fs.existsSync(lockFile)) {
76
+ fs.unlinkSync(lockFile);
77
+ log('warn', '已清理残留的 SingletonLock(上次浏览器未正常关闭)');
78
+ }
79
+
80
+ console.log('操作步骤:');
81
+ console.log(' 1. 浏览器将自动打开,请手动完成登录');
82
+ console.log(' 2. 登录成功后关闭浏览器窗口');
83
+ console.log(' 3. 登录状态将保存在持久化配置中');
84
+ console.log(' 4. MCP 后续会话自动复用此登录状态');
85
+ console.log('');
86
+
87
+ const scriptContent = [
88
+ `let chromium;`,
89
+ `try { chromium = require('playwright').chromium; } catch {`,
90
+ ` try { chromium = require('@playwright/test').chromium; } catch {`,
91
+ ` console.error('错误: 未找到 playwright 模块');`,
92
+ ` console.error('请安装: npx playwright install chromium');`,
93
+ ` process.exit(1);`,
94
+ ` }`,
95
+ `}`,
96
+ `(async () => {`,
97
+ ` const ctx = await chromium.launchPersistentContext(${JSON.stringify(profileDir)}, { headless: false });`,
98
+ ` const page = ctx.pages()[0] || await ctx.newPage();`,
99
+ ` try { await page.goto(${JSON.stringify(url)}); } catch {}`,
100
+ ` console.log('请在浏览器中完成登录后关闭窗口...');`,
101
+ ` await new Promise(r => {`,
102
+ ` ctx.on('close', r);`,
103
+ ` const t = setInterval(() => { try { if (!ctx.pages().length) { clearInterval(t); r(); } } catch { clearInterval(t); r(); } }, 2000);`,
104
+ ` });`,
105
+ ` try { await ctx.close(); } catch {}`,
106
+ `})().then(() => process.exit(0)).catch(() => process.exit(0));`,
107
+ ].join('\n');
108
+
109
+ const tmpScript = path.join(os.tmpdir(), `pw-auth-${Date.now()}.js`);
110
+ fs.writeFileSync(tmpScript, scriptContent);
111
+
112
+ const helperModules = path.join(__dirname, '..', 'node_modules');
113
+ const existingNodePath = process.env.NODE_PATH || '';
114
+ const nodePath = existingNodePath ? `${helperModules}:${existingNodePath}` : helperModules;
115
+
116
+ let scriptOk = false;
117
+ try {
118
+ execSync(`node "${tmpScript}"`, {
119
+ stdio: 'inherit',
120
+ cwd: getProjectRoot(),
121
+ env: { ...process.env, NODE_PATH: nodePath },
122
+ });
123
+ scriptOk = true;
124
+ } catch {
125
+ // 浏览器关闭时可能返回非零退出码,只要 profile 目录有内容就认为成功
126
+ const profileFiles = fs.readdirSync(profileDir);
127
+ scriptOk = profileFiles.length > 2;
128
+ if (!scriptOk) {
129
+ log('error', 'Playwright 启动失败,且未检测到有效的浏览器配置');
130
+ log('info', '请确保已安装 Chromium: npx playwright install chromium');
131
+ try { fs.unlinkSync(tmpScript); } catch {}
132
+ return;
133
+ }
134
+ log('warn', '浏览器退出码非零,但已检测到有效配置,继续...');
135
+ }
136
+
137
+ try { fs.unlinkSync(tmpScript); } catch {}
138
+
139
+ log('ok', '登录状态已保存到持久化配置');
140
+ updateMcpConfig(p, 'persistent');
141
+ updateGitignore('.claude-coder/.runtime/browser-profile');
142
+ enableMcpPlaywrightEnv();
67
143
 
68
- log('info', '启动 Playwright 浏览器,请手动登录...');
69
- log('info', `目标 URL: ${targetUrl}`);
70
144
  console.log('');
145
+ log('ok', '配置完成!');
146
+ const relProfile = path.relative(getProjectRoot(), profileDir);
147
+ log('info', `MCP 使用 persistent 模式 (user-data-dir: ${relProfile})`);
148
+ log('info', '如需更新登录状态,重新运行 claude-coder auth');
149
+ }
150
+
151
+ // ── isolated 模式:使用 codegen 录制 storage-state ──
152
+
153
+ async function authIsolated(url, p) {
71
154
  console.log('操作步骤:');
72
155
  console.log(' 1. 浏览器将自动打开,请手动完成登录');
73
156
  console.log(' 2. 登录成功后关闭浏览器窗口');
74
157
  console.log(' 3. 登录状态(cookies + localStorage)将保存到 playwright-auth.json');
75
- console.log(' 4. MCP 每次会话自动加载此状态(isolated 模式)');
158
+ console.log(' 4. MCP 每次会话自动从此文件加载初始状态');
76
159
  console.log('');
77
160
 
78
161
  try {
79
162
  execSync(
80
- `npx playwright codegen --save-storage="${p.playwrightAuth}" "${targetUrl}"`,
163
+ `npx playwright codegen --save-storage="${p.playwrightAuth}" "${url}"`,
81
164
  { stdio: 'inherit', cwd: getProjectRoot() }
82
165
  );
83
166
  } catch (err) {
84
167
  if (!fs.existsSync(p.playwrightAuth)) {
85
168
  log('error', `Playwright 登录状态导出失败: ${err.message}`);
86
- log('info', '请确保已安装 playwright: npx playwright install');
169
+ log('info', '请确保已安装: npx playwright install chromium');
87
170
  return;
88
171
  }
89
172
  }
@@ -94,16 +177,69 @@ async function auth(url) {
94
177
  }
95
178
 
96
179
  log('ok', '登录状态已保存到 playwright-auth.json');
97
-
98
- updateMcpConfig();
180
+ updateMcpConfig(p, 'isolated');
99
181
  updateGitignore('.claude-coder/playwright-auth.json');
100
182
  enableMcpPlaywrightEnv();
101
183
 
102
184
  console.log('');
103
185
  log('ok', '配置完成!');
104
- log('info', 'MCP 使用 --isolated --storage-state 模式');
105
- log('info', 'localStoragecookies 每次会话自动从 playwright-auth.json 加载');
186
+ log('info', 'MCP 使用 isolated 模式 (storage-state)');
187
+ log('info', 'cookieslocalStorage 每次会话自动从 playwright-auth.json 加载');
106
188
  log('info', '如需更新登录状态,重新运行 claude-coder auth');
107
189
  }
108
190
 
109
- module.exports = { auth };
191
+ // ── extension 模式:连接真实浏览器 ──
192
+
193
+ function authExtension(p) {
194
+ console.log('Extension 模式说明:');
195
+ console.log('');
196
+ console.log(' 此模式通过 Chrome 扩展连接到您正在运行的浏览器。');
197
+ console.log(' MCP 将直接使用浏览器中已有的登录态和扩展。');
198
+ console.log('');
199
+ console.log(' 前置条件:');
200
+ console.log(' 1. 安装 "Playwright MCP Bridge" Chrome/Edge 扩展');
201
+ console.log(' https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm');
202
+ console.log(' 2. 确保浏览器已启动且扩展已启用');
203
+ console.log(' 3. 无需额外认证操作,您的浏览器登录态将自动可用');
204
+ console.log('');
205
+
206
+ updateMcpConfig(p, 'extension');
207
+ enableMcpPlaywrightEnv();
208
+
209
+ console.log('');
210
+ log('ok', '配置完成!');
211
+ log('info', 'MCP 使用 extension 模式(连接真实浏览器)');
212
+ log('info', '确保 Chrome/Edge 已运行且 Playwright MCP Bridge 扩展已启用');
213
+ }
214
+
215
+ // ── 主入口 ──
216
+
217
+ async function auth(url) {
218
+ ensureLoopDir();
219
+ const config = loadConfig();
220
+ const p = paths();
221
+ const mode = config.playwrightMode;
222
+ const targetUrl = url || 'http://localhost:3000';
223
+
224
+ log('info', `Playwright 模式: ${mode}`);
225
+ log('info', `目标 URL: ${targetUrl}`);
226
+ console.log('');
227
+
228
+ switch (mode) {
229
+ case 'persistent':
230
+ await authPersistent(targetUrl, p);
231
+ break;
232
+ case 'isolated':
233
+ await authIsolated(targetUrl, p);
234
+ break;
235
+ case 'extension':
236
+ authExtension(p);
237
+ break;
238
+ default:
239
+ log('error', `未知的 Playwright 模式: ${mode}`);
240
+ log('info', '请运行 claude-coder setup 重新配置');
241
+ return;
242
+ }
243
+ }
244
+
245
+ module.exports = { auth, updateMcpConfig };
package/src/config.js CHANGED
@@ -57,6 +57,7 @@ function paths() {
57
57
  testsFile: path.join(loopDir, 'tests.json'),
58
58
  testEnvFile: path.join(loopDir, 'test.env'),
59
59
  playwrightAuth: path.join(loopDir, 'playwright-auth.json'),
60
+ browserProfile: path.join(runtime, 'browser-profile'),
60
61
  mcpConfig: path.join(getProjectRoot(), '.mcp.json'),
61
62
  claudeMd: getTemplatePath('CLAUDE.md'),
62
63
  scanProtocol: getTemplatePath('SCAN_PROTOCOL.md'),
@@ -97,7 +98,7 @@ function loadConfig() {
97
98
  timeoutMs: parseInt(env.API_TIMEOUT_MS, 10) || 3000000,
98
99
  mcpToolTimeout: parseInt(env.MCP_TOOL_TIMEOUT, 10) || 30000,
99
100
  mcpPlaywright: env.MCP_PLAYWRIGHT === 'true',
100
- debug: env.CLAUDE_DEBUG || '',
101
+ playwrightMode: env.MCP_PLAYWRIGHT_MODE || 'persistent',
101
102
  disableNonessential: env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC || '',
102
103
  effortLevel: env.CLAUDE_CODE_EFFORT_LEVEL || '',
103
104
  smallFastModel: env.ANTHROPIC_SMALL_FAST_MODEL || '',
@@ -190,6 +191,21 @@ function syncToGlobal() {
190
191
  log('ok', `已同步配置到 ${settingsPath}`);
191
192
  }
192
193
 
194
+ function updateEnvVar(key, value) {
195
+ const p = paths();
196
+ if (!fs.existsSync(p.envFile)) return false;
197
+ let content = fs.readFileSync(p.envFile, 'utf8');
198
+ const regex = new RegExp(`^${key}=.*$`, 'm');
199
+ if (regex.test(content)) {
200
+ content = content.replace(regex, `${key}=${value}`);
201
+ } else {
202
+ const suffix = content.endsWith('\n') ? '' : '\n';
203
+ content += `${suffix}${key}=${value}\n`;
204
+ }
205
+ fs.writeFileSync(p.envFile, content, 'utf8');
206
+ return true;
207
+ }
208
+
193
209
  module.exports = {
194
210
  COLOR,
195
211
  log,
@@ -203,4 +219,5 @@ module.exports = {
203
219
  buildEnvVars,
204
220
  getAllowedTools,
205
221
  syncToGlobal,
222
+ updateEnvVar,
206
223
  };
package/src/hooks.js CHANGED
@@ -74,7 +74,16 @@ function createSessionHooks(indicator, logStream, options = {}) {
74
74
 
75
75
  return {};
76
76
  }]
77
- }]
77
+ }],
78
+ PostToolUse: [{
79
+ matcher: '*',
80
+ hooks: [async () => {
81
+ indicator.updatePhase('thinking');
82
+ indicator.updateStep('');
83
+ indicator.toolTarget = '';
84
+ return {};
85
+ }]
86
+ }],
78
87
  };
79
88
 
80
89
  return {
package/src/indicator.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { COLOR } = require('./config');
4
4
 
5
- const SPINNERS = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
5
+ const SPINNERS = ['⠋', '⠙', '⠸', '⠴', '⠦', '⠇'];
6
6
 
7
7
  class Indicator {
8
8
  constructor() {
@@ -15,12 +15,14 @@ class Indicator {
15
15
  this.lastToolTime = Date.now();
16
16
  this.sessionNum = 0;
17
17
  this.startTime = Date.now();
18
+ this.stallTimeoutMin = 30;
18
19
  }
19
20
 
20
- start(sessionNum) {
21
+ start(sessionNum, stallTimeoutMin) {
21
22
  this.sessionNum = sessionNum;
22
23
  this.startTime = Date.now();
23
- this.timer = setInterval(() => this._render(), 500);
24
+ if (stallTimeoutMin > 0) this.stallTimeoutMin = stallTimeoutMin;
25
+ this.timer = setInterval(() => this._render(), 1000);
24
26
  }
25
27
 
26
28
  stop() {
@@ -64,7 +66,7 @@ class Indicator {
64
66
 
65
67
  let line = `${spinner} [Session ${this.sessionNum}] ${clock} ${phaseLabel} ${mm}:${ss}`;
66
68
  if (idleMin >= 2) {
67
- line += ` | ${COLOR.red}${idleMin}分无工具调用${COLOR.reset}`;
69
+ line += ` | ${COLOR.red}${idleMin}分无工具调用(等待模型响应, ${this.stallTimeoutMin}分钟超时自动中断)${COLOR.reset}`;
68
70
  }
69
71
  if (this.step) {
70
72
  line += ` | ${this.step}`;
@@ -83,8 +85,7 @@ class Indicator {
83
85
 
84
86
  _render() {
85
87
  this.spinnerIndex++;
86
- const line = this.getStatusLine();
87
- process.stderr.write(`\r\x1b[K${line}`);
88
+ process.stderr.write(`\r\x1b[K${this.getStatusLine()}`);
88
89
  }
89
90
  }
90
91
 
@@ -107,7 +108,7 @@ function extractBashLabel(cmd) {
107
108
  function extractBashTarget(cmd) {
108
109
  let clean = cmd.replace(/^(?:cd\s+\S+\s*&&\s*)+/g, '').trim();
109
110
  clean = clean.split(/\s*(?:\|{1,2}|;|&&|2>&1|>\s*\/dev\/null)\s*/)[0].trim();
110
- return clean.slice(0, 40);
111
+ return clean;
111
112
  }
112
113
 
113
114
  function inferPhaseStep(indicator, toolName, toolInput) {
package/src/prompts.js CHANGED
@@ -98,10 +98,23 @@ function buildCodingPrompt(sessionNum, opts = {}) {
98
98
  testEnvHint = `如需持久化测试凭证(API Key、测试账号密码等),写入 ${projectRoot}/.claude-coder/test.env(KEY=value 格式,每行一个)。后续 session 会自动感知。`;
99
99
  }
100
100
 
101
- // Hint 6c: Playwright auth state
101
+ // Hint 6c: Playwright mode awareness
102
102
  let playwrightAuthHint = '';
103
- if (fs.existsSync(p.playwrightAuth)) {
104
- playwrightAuthHint = `已检测到 Playwright 登录状态(${projectRoot}/.claude-coder/playwright-auth.json),MCP 使用 --isolated --storage-state 模式,每次会话自动加载 localStorage 和 cookies。`;
103
+ if (config.mcpPlaywright) {
104
+ const mode = config.playwrightMode;
105
+ switch (mode) {
106
+ case 'persistent':
107
+ playwrightAuthHint = 'Playwright MCP 使用 persistent 模式(user-data-dir),浏览器登录状态持久保存在本地配置中,无需额外登录操作。';
108
+ break;
109
+ case 'isolated':
110
+ playwrightAuthHint = fs.existsSync(p.playwrightAuth)
111
+ ? `Playwright MCP 使用 isolated 模式,已检测到登录状态文件(playwright-auth.json),每次会话自动加载 cookies 和 localStorage。`
112
+ : 'Playwright MCP 使用 isolated 模式,但未检测到登录状态文件。如目标页面需要登录,请先运行 claude-coder auth <URL>。';
113
+ break;
114
+ case 'extension':
115
+ playwrightAuthHint = 'Playwright MCP 使用 extension 模式,已连接用户真实浏览器,直接复用浏览器已有的登录态和扩展。注意:操作会影响用户正在使用的浏览器。';
116
+ break;
117
+ }
105
118
  }
106
119
 
107
120
  // Hint 7: Session memory (read flat session_result.json)
package/src/session.js CHANGED
@@ -135,7 +135,8 @@ async function runCodingSession(sessionNum, opts = {}) {
135
135
  editThreshold: config.editThreshold,
136
136
  });
137
137
 
138
- indicator.start(sessionNum);
138
+ const stallTimeoutMin = Math.floor(stallTimeoutMs / 60000);
139
+ indicator.start(sessionNum, stallTimeoutMin);
139
140
 
140
141
  try {
141
142
  const queryOpts = buildQueryOptions(config, opts);
@@ -203,7 +204,7 @@ async function runScanSession(requirement, opts = {}) {
203
204
  stallTimeoutMs,
204
205
  });
205
206
 
206
- indicator.start(0);
207
+ indicator.start(0, Math.floor(stallTimeoutMs / 60000));
207
208
  log('info', `正在调用 Claude Code 执行项目扫描(${projectType}项目)...`);
208
209
 
209
210
  try {
@@ -264,7 +265,7 @@ async function runAddSession(instruction, opts = {}) {
264
265
  stallTimeoutMs,
265
266
  });
266
267
 
267
- indicator.start(0);
268
+ indicator.start(0, Math.floor(stallTimeoutMs / 60000));
268
269
  log('info', '正在追加任务...');
269
270
 
270
271
  try {
package/src/setup.js CHANGED
@@ -90,7 +90,7 @@ async function setup() {
90
90
  console.log('============================================');
91
91
  console.log('');
92
92
  console.log(' 第一步: 模型提供商配置');
93
- console.log(' 第二步: MCP 工具 + 调试输出(可选)');
93
+ console.log(' 第二步: MCP 工具配置(可选)');
94
94
  console.log('');
95
95
 
96
96
  // Detect existing config
@@ -336,34 +336,45 @@ async function setup() {
336
336
  if (mcpChoice === 1) {
337
337
  configLines.push('MCP_PLAYWRIGHT=true');
338
338
  log('ok', 'Playwright MCP 已启用');
339
+
340
+ console.log('');
341
+ console.log('请选择 Playwright MCP 浏览器模式:');
342
+ console.log('');
343
+ console.log(' 1) persistent - 懒人模式(默认,推荐)');
344
+ console.log(' 登录一次永久生效,适合 Google SSO、企业内网 API 拉取等日常开发');
345
+ console.log('');
346
+ console.log(' 2) isolated - 开发模式');
347
+ console.log(' 每次会话从快照加载,适合验证登录流程的自动化测试');
348
+ console.log('');
349
+ console.log(' 3) extension - 连接真实浏览器(实验性)');
350
+ console.log(' 通过 Chrome 扩展复用已有登录态和插件');
351
+ console.log(' 需要安装 "Playwright MCP Bridge" 扩展');
339
352
  console.log('');
340
- console.log(' 请确保已安装 Playwright MCP:');
341
- console.log(` ${COLOR.blue}npx @anthropic-ai/claude-code mcp add playwright -- npx @anthropic-ai/playwright-mcp${COLOR.reset}`);
342
- console.log(` ${COLOR.blue}详见: https://github.com/microsoft/playwright-mcp${COLOR.reset}`);
343
- } else {
344
- configLines.push('MCP_PLAYWRIGHT=false');
345
- log('info', '已跳过 Playwright MCP');
346
- }
347
353
 
348
- // Debug output
349
- console.log('');
350
- console.log('是否开启 Claude 调试输出(便于排查问题,输出较多)?');
351
- console.log('');
352
- console.log(' 1) 否 - 静默(默认,推荐)');
353
- console.log(' 2) 是 - verbose(完整每轮输出)');
354
- console.log(' 3) 是 - mcp(MCP 调用,如 Playwright Click)');
355
- console.log('');
354
+ const modeChoice = await askChoice(rl, '选择 [1-3,默认 1]: ', 1, 3, 1);
355
+ const modeMap = { 1: 'persistent', 2: 'isolated', 3: 'extension' };
356
+ const mode = modeMap[modeChoice];
357
+ configLines.push(`MCP_PLAYWRIGHT_MODE=${mode}`);
356
358
 
357
- const debugChoice = await askChoice(rl, '选择 [1-3,默认 1]: ', 1, 3, 1);
358
- configLines.push('', '# Claude 调试(可随时修改)');
359
- if (debugChoice === 2) {
360
- configLines.push('CLAUDE_DEBUG=verbose');
361
- log('info', '已启用 CLAUDE_DEBUG=verbose');
362
- } else if (debugChoice === 3) {
363
- configLines.push('CLAUDE_DEBUG=mcp');
364
- log('info', '已启用 CLAUDE_DEBUG=mcp');
359
+ console.log('');
360
+ if (mode === 'extension') {
361
+ console.log(` ${COLOR.yellow}⚠ 前置条件:安装 Playwright MCP Bridge 浏览器扩展${COLOR.reset}`);
362
+ console.log(` ${COLOR.blue} https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm${COLOR.reset}`);
363
+ console.log('');
364
+ console.log(' 安装扩展后,运行 claude-coder auth 生成 .mcp.json 配置');
365
+ } else if (mode === 'persistent') {
366
+ console.log(' 使用 claude-coder auth <URL> 打开浏览器完成首次登录');
367
+ console.log(' 登录状态将持久保存,后续 MCP 会话自动复用');
368
+ console.log('');
369
+ console.log(' 请确保已安装 Playwright:');
370
+ console.log(` ${COLOR.blue}npx playwright install chromium${COLOR.reset}`);
371
+ } else {
372
+ console.log(' 使用 claude-coder auth <URL> 录制登录状态到 playwright-auth.json');
373
+ console.log(' MCP 每次会话从此文件加载初始 cookies/localStorage');
374
+ }
365
375
  } else {
366
- configLines.push('# CLAUDE_DEBUG=verbose # 取消注释可开启');
376
+ configLines.push('MCP_PLAYWRIGHT=false');
377
+ log('info', '已跳过 Playwright MCP');
367
378
  }
368
379
 
369
380
  // Write config
@@ -378,6 +389,7 @@ async function setup() {
378
389
  console.log(` 配置文件: ${p.envFile}`);
379
390
  console.log(' 使用方式: claude-coder run "你的需求"');
380
391
  console.log(' 详细需求: 创建 requirements.md 后运行 claude-coder run');
392
+ console.log(' 切换模式: claude-coder config mcp <persistent|isolated|extension>');
381
393
  console.log(' 重新配置: claude-coder setup');
382
394
  console.log('');
383
395
  }
package/src/tasks.js CHANGED
@@ -48,7 +48,11 @@ function findNextTask(data) {
48
48
  });
49
49
  })
50
50
  .sort((a, b) => (a.priority || 999) - (b.priority || 999));
51
- return pending[0] || null;
51
+ if (pending.length > 0) return pending[0];
52
+
53
+ const inProgress = features.filter(f => f.status === 'in_progress')
54
+ .sort((a, b) => (a.priority || 999) - (b.priority || 999));
55
+ return inProgress[0] || null;
52
56
  }
53
57
 
54
58
  function setStatus(data, taskId, newStatus) {
package/src/validator.js CHANGED
@@ -3,6 +3,26 @@
3
3
  const fs = require('fs');
4
4
  const { execSync } = require('child_process');
5
5
  const { paths, log, getProjectRoot } = require('./config');
6
+ const { loadTasks, getFeatures } = require('./tasks');
7
+
8
+ function tryExtractFromBroken(text) {
9
+ const result = {};
10
+ const srMatch = text.match(/"session_result"\s*:\s*"(success|failed)"/);
11
+ if (srMatch) result.session_result = srMatch[1];
12
+ const saMatch = text.match(/"status_after"\s*:\s*"(\w+)"/);
13
+ if (saMatch) result.status_after = saMatch[1];
14
+ const sbMatch = text.match(/"status_before"\s*:\s*"(\w+)"/);
15
+ if (sbMatch) result.status_before = sbMatch[1];
16
+ return Object.keys(result).length > 0 ? result : null;
17
+ }
18
+
19
+ function inferFromTasks(taskId) {
20
+ if (!taskId) return null;
21
+ const data = loadTasks();
22
+ if (!data) return null;
23
+ const task = getFeatures(data).find(f => f.id === taskId);
24
+ return task ? task.status : null;
25
+ }
6
26
 
7
27
  function validateSessionResult() {
8
28
  const p = paths();
@@ -12,12 +32,18 @@ function validateSessionResult() {
12
32
  return { valid: false, fatal: true, recoverable: false, reason: 'session_result.json 不存在' };
13
33
  }
14
34
 
35
+ const raw = fs.readFileSync(p.sessionResult, 'utf8');
15
36
  let data;
16
37
  try {
17
- data = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
38
+ data = JSON.parse(raw);
18
39
  } catch (err) {
19
- log('error', `session_result.json 解析失败: ${err.message}`);
20
- return { valid: false, fatal: true, recoverable: false, reason: `JSON 解析失败: ${err.message}` };
40
+ log('warn', `session_result.json 解析失败: ${err.message}`);
41
+ const extracted = tryExtractFromBroken(raw);
42
+ if (extracted) {
43
+ log('info', `从截断 JSON 中提取到关键字段: ${JSON.stringify(extracted)}`);
44
+ return { valid: false, fatal: false, recoverable: true, reason: 'JSON 截断但提取到关键字段', data: extracted };
45
+ }
46
+ return { valid: false, fatal: false, recoverable: true, reason: `JSON 解析失败: ${err.message}` };
21
47
  }
22
48
 
23
49
  // Backward compat: unwrap legacy { current: {...} } format
@@ -33,14 +59,14 @@ function validateSessionResult() {
33
59
  }
34
60
 
35
61
  if (!['success', 'failed'].includes(data.session_result)) {
36
- log('error', `session_result 必须是 success 或 failed,实际是: ${data.session_result}`);
37
- return { valid: false, fatal: true, recoverable: false, reason: `无效 session_result: ${data.session_result}` };
62
+ log('warn', `session_result 必须是 success 或 failed,实际是: ${data.session_result}`);
63
+ return { valid: false, fatal: false, recoverable: true, reason: `无效 session_result: ${data.session_result}`, data };
38
64
  }
39
65
 
40
66
  const validStatuses = ['pending', 'in_progress', 'testing', 'done', 'failed'];
41
67
  if (!validStatuses.includes(data.status_after)) {
42
- log('error', `status_after 不合法: ${data.status_after}`);
43
- return { valid: false, fatal: true, recoverable: false, reason: `无效 status_after: ${data.status_after}` };
68
+ log('warn', `status_after 不合法: ${data.status_after}`);
69
+ return { valid: false, fatal: false, recoverable: true, reason: `无效 status_after: ${data.status_after}`, data };
44
70
  }
45
71
 
46
72
  if (data.session_result === 'success') {
@@ -79,25 +105,22 @@ function checkGitProgress(headBefore) {
79
105
  return { hasCommit: true, warning: false };
80
106
  }
81
107
 
82
- function checkTestCoverage(taskId) {
108
+ function checkTestCoverage(taskId, statusAfter) {
83
109
  const p = paths();
84
110
 
85
- if (!fs.existsSync(p.testsFile) || !fs.existsSync(p.sessionResult)) return;
111
+ if (!fs.existsSync(p.testsFile)) return;
112
+ if (statusAfter !== 'done' || !taskId) return;
86
113
 
87
114
  try {
88
- const sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
89
115
  const tests = JSON.parse(fs.readFileSync(p.testsFile, 'utf8'));
90
116
  const testCases = tests.test_cases || [];
91
-
92
- if (sr.status_after === 'done' && taskId) {
93
- const taskTests = testCases.filter(t => t.feature_id === taskId);
94
- if (taskTests.length > 0) {
95
- const failed = taskTests.filter(t => t.last_result === 'fail');
96
- if (failed.length > 0) {
97
- log('warn', `tests.json 中有失败的验证记录: ${failed.map(t => t.id).join(', ')}`);
98
- } else {
99
- log('ok', `${taskTests.length} 条验证记录覆盖任务 ${taskId}`);
100
- }
117
+ const taskTests = testCases.filter(t => t.feature_id === taskId);
118
+ if (taskTests.length > 0) {
119
+ const failed = taskTests.filter(t => t.last_result === 'fail');
120
+ if (failed.length > 0) {
121
+ log('warn', `tests.json 中有失败的验证记录: ${failed.map(t => t.id).join(', ')}`);
122
+ } else {
123
+ log('ok', `${taskTests.length} 条验证记录覆盖任务 ${taskId}`);
101
124
  }
102
125
  }
103
126
  } catch { /* ignore */ }
@@ -106,21 +129,32 @@ function checkTestCoverage(taskId) {
106
129
  async function validate(headBefore, taskId) {
107
130
  log('info', '========== 开始校验 ==========');
108
131
 
109
- let srResult = validateSessionResult();
132
+ const srResult = validateSessionResult();
110
133
  const gitResult = checkGitProgress(headBefore);
111
134
 
112
- // Tiered: has commit + session_result issue → warn, don't rollback good code
113
- if (srResult.recoverable && gitResult.hasCommit) {
114
- log('warn', 'session_result.json 格式异常,但有新提交,降级为警告(不回滚代码)');
115
- } else if (srResult.recoverable && !gitResult.hasCommit) {
116
- log('error', '无新提交且 session_result.json 格式错误,视为致命');
117
- srResult.fatal = true;
118
- }
135
+ let fatal = false;
136
+ let hasWarnings = false;
119
137
 
120
- checkTestCoverage(taskId);
138
+ if (srResult.valid) {
139
+ hasWarnings = gitResult.warning;
140
+ } else {
141
+ // session_result.json has issues — cross-validate with git + tasks.json
142
+ if (gitResult.hasCommit) {
143
+ const taskStatus = inferFromTasks(taskId);
144
+ if (taskStatus === 'done' || taskStatus === 'testing') {
145
+ log('warn', `session_result.json 异常,但 tasks.json 显示 ${taskId} 已 ${taskStatus},且有新提交,降级为警告`);
146
+ } else {
147
+ log('warn', 'session_result.json 异常,但有新提交,降级为警告(不回滚代码)');
148
+ }
149
+ hasWarnings = true;
150
+ } else {
151
+ log('error', '无新提交且 session_result.json 异常,视为致命');
152
+ fatal = true;
153
+ }
154
+ }
121
155
 
122
- const fatal = srResult.fatal;
123
- const hasWarnings = gitResult.warning || srResult.recoverable;
156
+ const statusAfter = srResult.data?.status_after || inferFromTasks(taskId) || null;
157
+ checkTestCoverage(taskId, statusAfter);
124
158
 
125
159
  if (fatal) {
126
160
  log('error', '========== 校验失败 (致命) ==========');
@@ -51,7 +51,8 @@
51
51
  | `.claude-coder/session_result.json` | 本次会话的结构化输出 | 每次会话结束时覆盖写入 |
52
52
  | `.claude-coder/tests.json` | 功能验证记录(轻量) | 可新增和更新;仅当功能涉及 API 或核心逻辑时记录 |
53
53
  | `.claude-coder/test.env` | 测试凭证(API Key、测试账号等) | **可追加写入**;发现测试需要的凭证时持久化到此文件 |
54
- | `.claude-coder/playwright-auth.json` | 浏览器登录状态(cookies + localStorage) | 只读;由 `claude-coder auth` 生成,MCP 每次会话自动加载 |
54
+ | `.claude-coder/playwright-auth.json` | 浏览器登录状态快照(isolated 模式时由 `claude-coder auth` 生成) | 只读;persistent/extension 模式下此文件不存在 |
55
+ | `.mcp.json` | MCP 服务配置(由 `claude-coder auth` 自动生成) | **只读,绝对不得修改** |
55
56
 
56
57
  ### requirements.md 处理原则
57
58
 
@@ -110,9 +110,10 @@ P0(核心流程)必测 → P1(错误处理)必测 → P2(次要功能
110
110
 
111
111
  ## 六、凭证管理
112
112
 
113
- `.mcp.json` 配置 `--isolated --storage-state=path/to/auth.json`。
114
-
115
- **关键**: `--storage-state` **必须**配合 `--isolated`,否则 localStorage 不注入。
113
+ `.mcp.json` `claude-coder auth` 自动生成,根据配置模式使用不同参数:
114
+ - persistent(默认):`--user-data-dir=<path>`,登录态自动保持
115
+ - isolated:`--isolated --storage-state=<path>`,每次从快照加载
116
+ - extension:`--extension`,连接真实浏览器
116
117
 
117
118
  凭证失效时:不修改 auth 文件,报告中标注,提示用户运行 `claude-coder auth [URL]`。
118
119