draftgo-cli 3.0.53 → 3.0.55

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
@@ -55,6 +55,8 @@ DraftGo Skill 不能被 MCP 替代。根 `SKILL.md` 会在 Skill 触发时自动
55
55
 
56
56
  页面任务读取 `frontend.md`,按需补读 `runtime.md` / `app-api.md`;数据、自定义服务和 AIHub 任务分别读取对应 Reference。目标明确时不要全量枚举无关资源。`mcp test` 是连接诊断,不是每次资源查询的前置步骤。
57
57
 
58
+ 页面需求先结合用户意图和 MCP 实时资源判断是修改已有页面还是新增页面。已有页面在确认唯一 ID 后 checkout;独立新页面先通过实时 API 创建并取得 ID,再 checkout 完整正文。“做一个功能页面”本身不预设新建或修改,只有不同判断会产生明显不同结果时才需要向用户澄清。
59
+
58
60
  只回答无需实时状态的本地规则问题时,不必调用 MCP。静态资源须先区分:平台内置目录由 `frontend.md` 说明,指定页面依赖经 MCP 定位、checkout 后本地搜索,服务器全量文件不能凭现有 Skill、MCP 摘要或 checkout 声称已枚举。
59
61
 
60
62
  Skill 说明产品约束、内置能力和操作规则;MCP 说明当前远端状态;checkout 正文才能证明某个页面完整引用了哪些静态资源。汇报时应明确标注这三类证据,不能互相替代。
@@ -101,6 +103,8 @@ draftgo mcp test # 验证 initialize、tools/list、项目/正文
101
103
  draftgo mcp serve # 启动 stdio -> 远端 Streamable HTTP bridge
102
104
  ```
103
105
 
106
+ `mcp serve` 会保留远端 `Mcp-Session-Id`。服务重启或 session 过期后,它会自动重新 initialize,并只重试一次被明确拒绝为 session 无效的当前请求;普通工具失败、超时和 5xx 不会自动重试。`mcp test` 使用独立的新连接,只证明当前配置能够建立新 session,不能直接证明宿主此前持有的 session 仍有效。
107
+
104
108
  `setup` 不传 target 时自动检测当前项目的宿主。可用 `--target codex,cursor` 传入多个目标。
105
109
 
106
110
  当前 MCP 宿主支持:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "draftgo-cli",
3
- "version": "3.0.53",
3
+ "version": "3.0.55",
4
4
  "description": "Install and manage the DraftGo skill across AI coding agents (Claude Code, Codex, Cursor, Windsurf, Antigravity, Copilot, Gemini, Kiro).",
5
5
  "bin": {
6
6
  "draftgo": "bin/draftgo.js"
@@ -29,7 +29,7 @@ description: Use this skill to inspect, develop, debug, or deliver a DraftGo app
29
29
 
30
30
  | 任务 | 专题资料与后续动作 |
31
31
  |---|---|
32
- | 页面、导航、交互或 UI | `references/frontend.md`;涉及 iframe、路由、认证或全局层加读 `runtime.md` / `app-api.md`;MCP 定位后 checkout 完整正文 |
32
+ | 页面、导航、交互或 UI | `references/frontend.md`;涉及 iframe、路由、认证或全局层加读 `runtime.md` / `app-api.md`;先结合需求与 MCP 资源判断新增或修改,已有资源按需 checkout |
33
33
  | 动态 DB、筛选或关系 | `references/data.md`;需要关系完整示例时读 `db-relations.md`;API 调用遵循 `mcp.md` |
34
34
  | AIHub、聊天或图片 | `references/aihub.md`、`references/chat-sdk.md`;资产与 operation 以 MCP 为准 |
35
35
  | 自定义服务、权限、外部调用 | `references/custom-services.md`;管理和动态 Route operation 以 MCP 为准 |
@@ -39,6 +39,8 @@ description: Use this skill to inspect, develop, debug, or deliver a DraftGo app
39
39
 
40
40
  不要用 MCP 摘要代替完整正文,也不要把 Skill 列出的 `/assets/` 能力声称为当前服务器全部文件。业务归属、系统资源或页面类型没有明确元数据时报告证据不足,不凭标题、路径或片段猜测。
41
41
 
42
+ 页面需求不因出现“页面”二字就默认新建或 checkout。Agent 结合用户意图、现有 route、页面职责和入口自主判断:修改已有页面时定位唯一资源后 checkout;确需独立新页面时先通过 MCP 实时 API 创建并取得 ID,再 checkout 正文。证据足以判断时直接推进;多个候选会导致不同产品结果时再向用户澄清。
43
+
42
44
  ## 所有权与执行
43
45
 
44
46
  - 同一文件或 DraftGo 资源全程只能由一个 Agent 修改;其他 Agent 只读分析并回传建议。
@@ -51,8 +53,8 @@ description: Use this skill to inspect, develop, debug, or deliver a DraftGo app
51
53
 
52
54
  ### 长正文
53
55
 
54
- 1. MCP 定位 pages/nav/docs 并读取 metadata。
55
- 2. 批量 checkout;在 `.draftgo/worktree/` 按唯一 owner 编辑。
56
+ 1. 确认目标 pages/nav/docs 已存在并通过 MCP 读取 metadata;新增资源先按实时 API 契约创建并取得 ID
57
+ 2. 对需要完整分析或编辑的目标批量 checkout;在 `.draftgo/worktree/` 按唯一 owner 编辑。
56
58
  3. 主 Agent 回读全部修改,编辑过程中统一运行一次快速 `draftgo check`。交付前运行 `draftgo verify <type> <id...>`;需要远端证据时加 `--remote`,页面布局或交互变化时加对应 `--url`。
57
59
  4. 用 `draftgo diff <type> <id>` 检查 base/local 差异,再按类型运行 `draftgo commit ...`。commit 后需要验证远端页面时运行 `draftgo verify <type> <id> --remote --url <url>`。
58
60
  5. 409/412 时停止自动提交,保留 base/local/remote 冲突材料;不得 force、覆盖或自动合并。
@@ -2,7 +2,7 @@
2
2
  "schema_version": "1.0",
3
3
  "id": "draftgo",
4
4
  "name": "DraftGo 开发助手",
5
- "version": "3.0.53",
5
+ "version": "3.0.55",
6
6
  "entry": "SKILL.md",
7
7
  "description": "以 Skill/reference 任务路由、MCP 实时发现、长正文 checkout/commit、统一验证和完成日志为边界的 DraftGo 工作流。",
8
8
  "license": "MIT",
@@ -24,6 +24,8 @@ The checkout set includes `pages`, `navigations`, `docs/articles`, and `custom_s
24
24
 
25
25
  db_meta、AIHub、system_config、roles、users、doc_categories 和普通配置使用 MCP 实时 API,不 checkout。
26
26
 
27
+ Checkout 只为已存在且已确认 ID 的资源建立本地正文与 base,不创建页面、导航或文档。新增资源先按 MCP 实时 API 契约创建并取得 ID;需要编辑完整正文时再 checkout。只需元数据或正文片段即可完成判断时,不必 checkout。
28
+
27
29
  ## 命令
28
30
 
29
31
  ```bash
@@ -79,12 +79,22 @@ version: 2.0.0
79
79
  - 系统内置页面通常不改;确需修改登录、设置、权限、用户、系统配置等页面时,先说明影响、验证方式和保留的管理员能力。
80
80
  - 页面风格不要照搬管理侧内置页面;业务前台按业务用户和品牌语境设计,管理侧按操作效率和信息密度设计。
81
81
 
82
- ### 新增页面绑定(强制)
82
+ ### 页面身份与创建
83
+
84
+ 业务新页面默认是数据库中的完整 HTML 页面。开始正文工作前,结合用户表达与 MCP 中的标题、route、用途和入口判断目标:
85
+
86
+ - 用户指定页面、ID、已有 route,或现有页面与需求职责明确一致时,按已有页面修改并 checkout 对应 ID。
87
+ - 用户明确要求新增,或需求需要独立 route/职责且定向搜索后没有合适页面时,按新页面处理。通过 `draftgo_api_search` → `draftgo_api_describe` → `draftgo_api_call` 使用实时创建契约,取得新页面 ID 后再 checkout;完整 HTML 仍只走 checkout/commit。
88
+ - “做一个功能页面”本身不等于新增,也不等于修改。证据足以形成唯一合理方案时 Agent 直接推进;若多个已有候选或新增/修改会产生明显不同的产品结果,再用一个简短问题澄清。
89
+
90
+ 搜索用于确认页面身份和避免重复,按标题、route 或业务用途逐步缩小范围即可,无需枚举无关资源。Checkout 应对应已确认的目标资源;新资源先创建并取得 ID。
91
+
92
+ ### 新增页面入口
83
93
 
84
94
  - 公开页 → 顶部/侧边导航、首页入口、相关按钮之一
85
95
  - 后台页 → 后台导航、管理菜单或现有后台入口
86
96
  - 多页面功能:列表/详情/新建/编辑/管理必须互相走通
87
- - 用户明确要求"隐藏页/草稿页"才可不绑定,并在 Task 记录原因;changelog 只记录最终完成结果
97
+ - 隐藏页、回调页、按链接直达的详情页或草稿页可按产品目的不绑定全局入口;需要入口时优先接入现有导航或业务流程
88
98
 
89
99
  ### `page_1_root.html` 首页特例(强制)
90
100
 
@@ -138,7 +148,7 @@ version: 2.0.0
138
148
  - **壳层前端**位于 DraftGo 基座的 `frontend/`,技术栈为 React 19 + Vite 8 + Tailwind CSS 4;这是维护壳层代码时使用的构建链路。
139
149
  - **业务页面和导航 HTML**存储在数据库资源中,由壳层以 iframe 运行。它们必须是完整的原生 HTML 文档,不能写入 TSX、ESM import、npm 依赖或 Vite 构建产物。
140
150
  - 数据库页面需要 React 时,只能使用下方内置的 React 18 UMD 资源和 `window.React` / `window.ReactDOM`。不要把壳层的 React 19 npm 依赖、外部 CDN 或其他 React 版本混入页面。
141
- - 新建页面先确认是“壳层源码修改”还是“数据库 HTML 页面”。二者的组件、构建和资源加载方式不同,不能混用。
151
+ - 只有用户明确要求修改 DraftGo 壳层前端时,才处理 `frontend/` 源码。
142
152
 
143
153
  ### 内置组件库清单
144
154
 
@@ -84,7 +84,9 @@ draftgo mcp serve
84
84
  - `setup` 写入项目级宿主配置,只替换 `draftgo` MCP 条目并保留其他配置。
85
85
  - `status` 检查配置是否存在、格式是否有效,以及是否误写了凭据或远端 URL。
86
86
  - `test` 从 `.draftgo/config.json` 读取连接,验证 initialize、tools/list、三类正文 resource_list,以及 db_meta 的 api_search、api_describe 和只读 api_call。
87
- - `serve` 启动 stdio bridge,把宿主请求代理到当前项目配置的远端 Streamable HTTP `/mcp`。
87
+ - `serve` 启动 stdio bridge,把宿主请求代理到当前项目配置的远端 Streamable HTTP `/mcp`。远端重启或 session 过期后,bridge 会清除旧 `Mcp-Session-Id`、重新 initialize,并对被明确拒绝为 session 无效的当前请求重试一次;普通超时、服务端错误和工具错误不会自动重试。
88
+
89
+ `draftgo mcp test` 每次使用独立的新连接,因此它证明当前 server、SAT 和协议可以建立新 session,不代表宿主已持有的旧 session 仍然有效。宿主工具报告 session 失效但 `mcp test` 成功时,bridge 应自动恢复;若恢复仍失败,再检查远端日志、代理头传递和服务可用性。
88
90
 
89
91
  `draftgo connect` 保存并验证 server/SAT、探测 `/mcp` 和关键工具,并可提示宿主 setup;`--server` 表示基础地址,完整 MCP endpoint 使用 `--mcp-url` 显式传入。它不下载业务资源,
90
92
  也不创建 pages、navigation、docs、db_meta 等本地镜像。底座仍在开发或暂不可达时,报告诊断结果即可,
@@ -8,7 +8,7 @@ read_when: 评估功能可行性时 · 选择开发路径时 · 进入新项目
8
8
 
9
9
  | 模块 | 开发方式 | 入口 |
10
10
  |---|---|---|
11
- | 页面 | 数据库 HTML(`page.value.html`) | MCP 定位,`checkout pages` / `commit pages` 编辑正文 |
11
+ | 页面 | 数据库 HTML(`page.value.html`) | 已有页面由 MCP 定位后 `checkout pages` / `commit pages`;新页面先按实时 API 契约创建并取得 ID |
12
12
  | 导航栏 | 数据库 HTML(`navigation.html`) | MCP 定位,`checkout nav` / `commit nav` 编辑正文 |
13
13
  | 动态 DB | db_meta 定义 schema 并操作结构化记录 | MCP `api_search` / `api_describe` / `api_call` |
14
14
  | 自定义服务 | Go `Register` 服务,支持 route/event/scheduled 混合注册 | MCP 实时 API;不创建本地镜像 |
package/src/mcp/client.js CHANGED
@@ -65,6 +65,44 @@ class McpRpcError extends Error {
65
65
  }
66
66
  }
67
67
 
68
+ const SESSION_INVALID_CODES = new Set([
69
+ -32002,
70
+ 'SESSION_EXPIRED',
71
+ 'SESSION_NOT_FOUND',
72
+ 'MCP_SESSION_EXPIRED',
73
+ 'MCP_SESSION_NOT_FOUND',
74
+ ]);
75
+
76
+ function isSessionInvalidError(error) {
77
+ if (!error) return false;
78
+ if (Number(error.status) === 404) return true;
79
+ const code = error.code != null
80
+ ? error.code
81
+ : error.rpc && error.rpc.error && error.rpc.error.code;
82
+ if (SESSION_INVALID_CODES.has(code)) return true;
83
+ const message = String(error.message
84
+ || error.rpc && error.rpc.error && error.rpc.error.message
85
+ || '');
86
+ return /\b(?:mcp\s+)?session(?:\s+id)?\s+(?:was\s+)?(?:not\s+found|expired|invalid|unknown|lost)\b/i.test(message)
87
+ || /\b(?:server|mcp)\s+(?:is\s+)?(?:not initialized|uninitialized)\b/i.test(message)
88
+ || /\binitialize first\b/i.test(message);
89
+ }
90
+
91
+ function requestIdKeys(message) {
92
+ const messages = Array.isArray(message) ? message : [message];
93
+ return new Set(messages
94
+ .filter((item) => isObject(item) && typeof item.method === 'string' && item.id != null)
95
+ .map((item) => `${typeof item.id}:${JSON.stringify(item.id)}`));
96
+ }
97
+
98
+ function sessionInvalidResponse(message, expectedIds) {
99
+ const messages = Array.isArray(message) ? message : [message];
100
+ return messages.find((item) => isObject(item)
101
+ && item.error
102
+ && expectedIds.has(`${typeof item.id}:${JSON.stringify(item.id)}`)
103
+ && isSessionInvalidError(item.error)) || null;
104
+ }
105
+
68
106
  function diagnosticNextCursor(result) {
69
107
  const value = diagnosticData(result);
70
108
  if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
@@ -154,6 +192,9 @@ class DraftGoMcpClient {
154
192
  this.sessionId = null;
155
193
  this.protocolVersion = options.protocolVersion || DEFAULT_PROTOCOL_VERSION;
156
194
  this.nextId = 1;
195
+ this.sessionGeneration = 0;
196
+ this.sessionRecovery = null;
197
+ this.sessionRecoveryCount = 0;
157
198
  this.onMessage = typeof options.onMessage === 'function' ? options.onMessage : null;
158
199
  }
159
200
 
@@ -163,28 +204,65 @@ class DraftGoMcpClient {
163
204
  }
164
205
 
165
206
  const initialize = findInitializeRequest(message);
207
+ const internalRecovery = options._sessionRecoveryInternal === true;
208
+ if (!initialize && !internalRecovery && this.sessionRecovery) {
209
+ await this.sessionRecovery;
210
+ }
211
+ if (initialize) this.sessionId = null;
166
212
  const requestedVersion = initialize
167
213
  && initialize.params
168
214
  && initialize.params.protocolVersion;
169
215
  const delivered = [];
216
+ const expectedIds = requestIdKeys(message);
217
+ const attemptedSessionId = initialize ? null : this.sessionId;
218
+ const attemptedGeneration = this.sessionGeneration;
219
+ const canRecover = !initialize
220
+ && !internalRecovery
221
+ && options._sessionRecoveryAttempted !== true
222
+ && attemptedSessionId != null;
223
+ let suppressedSessionError = null;
170
224
 
171
225
  try {
172
226
  await postJsonRpc(this.config, message, {
173
227
  signal: options.signal,
174
228
  timeoutMs: options.timeoutMs,
175
- sessionId: this.sessionId,
229
+ sessionId: attemptedSessionId,
176
230
  protocolVersion: requestedVersion || this.protocolVersion,
177
- onSession: (sessionId) => { this.sessionId = sessionId; },
231
+ onSession: (sessionId) => {
232
+ if (initialize || (this.sessionGeneration === attemptedGeneration
233
+ && this.sessionId === attemptedSessionId)) {
234
+ this.sessionId = sessionId;
235
+ }
236
+ },
178
237
  onMessage: (remoteMessage) => {
179
238
  const safe = redactValue(remoteMessage, this.secrets);
180
239
  delivered.push(safe);
240
+ const invalid = canRecover && sessionInvalidResponse(safe, expectedIds);
241
+ if (invalid) {
242
+ suppressedSessionError = invalid;
243
+ return;
244
+ }
245
+ if (options._suppressCallbacks === true) return;
181
246
  if (this.onMessage) this.onMessage(safe);
182
247
  if (typeof options.onMessage === 'function') options.onMessage(safe);
183
248
  },
184
249
  });
250
+ if (suppressedSessionError) {
251
+ const remoteError = suppressedSessionError.error;
252
+ const error = new McpRpcError(
253
+ remoteError.message || `MCP error ${remoteError.code}`,
254
+ { code: remoteError.code, data: remoteError.data, id: suppressedSessionError.id },
255
+ );
256
+ error.rpc = suppressedSessionError;
257
+ throw error;
258
+ }
185
259
  } catch (error) {
186
260
  if (error && error.rpc) error.rpc = redactValue(error.rpc, this.secrets);
187
261
  if (error && error.message) error.message = redactText(error.message, this.secrets);
262
+ if (canRecover && isSessionInvalidError(error)) {
263
+ await this.recoverSession(attemptedSessionId, attemptedGeneration, options);
264
+ return this.forward(message, { ...options, _sessionRecoveryAttempted: true });
265
+ }
188
266
  throw error;
189
267
  }
190
268
 
@@ -196,6 +274,54 @@ class DraftGoMcpClient {
196
274
  return delivered;
197
275
  }
198
276
 
277
+ async recoverSession(failedSessionId, failedGeneration, options = {}) {
278
+ if (this.sessionGeneration !== failedGeneration) return;
279
+ if (this.sessionId != null && this.sessionId !== failedSessionId) return;
280
+ if (this.sessionRecovery) return this.sessionRecovery;
281
+
282
+ const recoveryNumber = this.sessionRecoveryCount + 1;
283
+ this.sessionId = null;
284
+ const recovery = (async () => {
285
+ if (typeof options.onSessionRecovery === 'function') {
286
+ options.onSessionRecovery('started', { attempt: recoveryNumber });
287
+ }
288
+ try {
289
+ await this.initialize({
290
+ id: `draftgo-session-recovery-${recoveryNumber}`,
291
+ protocolVersion: this.protocolVersion,
292
+ timeoutMs: options.timeoutMs,
293
+ signal: options.signal,
294
+ _sessionRecoveryInternal: true,
295
+ _suppressCallbacks: true,
296
+ });
297
+ this.sessionRecoveryCount = recoveryNumber;
298
+ if (typeof options.onSessionRecovery === 'function') {
299
+ options.onSessionRecovery('succeeded', { attempt: recoveryNumber });
300
+ }
301
+ } catch (error) {
302
+ this.sessionId = null;
303
+ if (typeof options.onSessionRecovery === 'function') {
304
+ options.onSessionRecovery('failed', { attempt: recoveryNumber });
305
+ }
306
+ const message = redactText(error && error.message ? error.message : error, this.secrets);
307
+ const wrapped = new McpRpcError(`DraftGo MCP session recovery failed: ${message}`, {
308
+ code: error && error.code != null ? error.code : -32000,
309
+ data: error && error.data,
310
+ });
311
+ wrapped.status = Number(error && error.status) || 0;
312
+ wrapped.sessionRecovery = true;
313
+ wrapped.cause = error;
314
+ throw wrapped;
315
+ }
316
+ })();
317
+ this.sessionRecovery = recovery;
318
+ try {
319
+ await recovery;
320
+ } finally {
321
+ if (this.sessionRecovery === recovery) this.sessionRecovery = null;
322
+ }
323
+ }
324
+
199
325
  async request(method, params, options = {}) {
200
326
  const id = options.id == null ? this.nextId++ : options.id;
201
327
  const message = { jsonrpc: '2.0', id, method };
@@ -249,6 +375,7 @@ class DraftGoMcpClient {
249
375
  throw error;
250
376
  }
251
377
  if (typeof options.onStage === 'function') options.onStage('initialized', 'succeeded');
378
+ this.sessionGeneration += 1;
252
379
  return result;
253
380
  }
254
381
 
@@ -425,6 +552,7 @@ class DraftGoMcpClient {
425
552
  initialized,
426
553
  protocolVersion: this.protocolVersion,
427
554
  sessionId: this.sessionId,
555
+ sessionRecoveries: this.sessionRecoveryCount,
428
556
  tools,
429
557
  testedTool: tested[0].name,
430
558
  toolResult: tested[0].result,
@@ -457,6 +585,7 @@ module.exports = {
457
585
  diagnosticData,
458
586
  diagnosticNextCursor,
459
587
  findDBMetaListOperation,
588
+ isSessionInvalidError,
460
589
  redactValue,
461
590
  testConnection,
462
591
  callTool,