dsh-issue2pr 0.1.0 → 0.3.0

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/index.js CHANGED
@@ -15,6 +15,7 @@ import { makeLlm, routeInfo } from "./lib/llm.js";
15
15
  import { buildExecutors } from "./lib/stages/index.js";
16
16
  import { rollbackLedger } from "./lib/stages/p7-patch.js";
17
17
  import { killExternal, resolveClaudeBin } from "./lib/stages/p6-coder.js";
18
+ import { discoverAgents, testAgentGate, realRunWhich, realRunNpmPrefix, realRunVersion } from "./lib/agents.js";
18
19
  import { readTriggerText, logEvent } from "./lib/stages/helpers.js";
19
20
  import {
20
21
  loadConnections, upsertConnection, deleteConnection, normalizeConnection,
@@ -129,6 +130,19 @@ export function failRun(runDir, stageId, message) {
129
130
  return run;
130
131
  }
131
132
 
133
+ // —— 失败分析(P10)结果写回 run.json:此前只落 09-failure-analysis.json 产物,run.json 不记,
134
+ // UI 轮询状态机无从得知"失败已分析/建议动作"。此处读产物合并进 run.failureAnalysis(尽力而为,不阻断主流程)。
135
+ function recordFailureAnalysis(runDir) {
136
+ try {
137
+ const run = loadRun(runDir);
138
+ if (!run || run.status !== "failed") return;
139
+ const out = JSON.parse(readFileSync(join(runDir, "09-failure-analysis.json"), "utf8"));
140
+ if (!out || !out.category) return;
141
+ run.failureAnalysis = { category: out.category, detail: out.detail || "", action: out.action || "", at: new Date().toISOString() };
142
+ saveRun(runDir, run);
143
+ } catch { /* 产物缺失/损坏时静默跳过 */ }
144
+ }
145
+
132
146
  // 推进循环:仅 run.status==="running" 时调用 advance(awaiting_review 停手等 applyReview);
133
147
  // 阶段失败自动调 P10 executor 写分类产物后停(v1:不自动 replan)。
134
148
  function drive(ctx, root, runDir) {
@@ -151,6 +165,7 @@ function drive(ctx, root, runDir) {
151
165
  rcx.run = run;
152
166
  try {
153
167
  await rcx.executors.P10({ ...rcx, failure: { stage: run.current, error: msg } });
168
+ recordFailureAnalysis(runDir);
154
169
  } catch { /* P10 自身失败不阻断主流程 */ }
155
170
  }
156
171
  ctx.logger?.warn?.("issue2pr: " + msg);
@@ -174,6 +189,7 @@ function drive(ctx, root, runDir) {
174
189
  if (run.status === "failed") {
175
190
  try {
176
191
  await rcx.executors.P10({ ...rcx, failure: { stage: run.current, error: run.stages[run.current]?.error } });
192
+ recordFailureAnalysis(runDir);
177
193
  } catch { /* P10 自身失败不阻断主流程 */ }
178
194
  return;
179
195
  }
@@ -238,11 +254,22 @@ async function handleApi(ctx, root, req, res) {
238
254
  const prev = loadUiState(root);
239
255
  // lastProject:只认本字段;slug 走既有白名单形态,null/空 = 清除
240
256
  const lp = body && Object.prototype.hasOwnProperty.call(body, "lastProject") ? body.lastProject : prev.lastProject;
257
+ // lastRunBySlug:按项目记最近选中的 Run(宿主标签切换会销毁重建插件 webview,选中现场以此恢复);
258
+ // 按键合并:值合法则覆盖,null = 清除该项目的记忆,非法条目忽略
259
+ const lrPrev = (prev.lastRunBySlug && typeof prev.lastRunBySlug === "object") ? { ...prev.lastRunBySlug } : {};
260
+ if (body && body.lastRunBySlug && typeof body.lastRunBySlug === "object") {
261
+ for (const [s, rid] of Object.entries(body.lastRunBySlug)) {
262
+ if (!/^[a-z0-9-]+$/.test(s)) continue;
263
+ if (rid === null) delete lrPrev[s];
264
+ else if (/^\d{8}-\d{6}-[a-z0-9-]+$/.test(String(rid))) lrPrev[s] = String(rid);
265
+ }
266
+ }
241
267
  // 智能助手面板尺寸(跨软件重启兜底):整数且在合法范围才更新,非法值忽略保留旧值
242
268
  const intIn = (v, lo, hi) => (Number.isInteger(v) && v >= lo && v <= hi) ? v : null;
243
269
  const state = {
244
270
  ...prev,
245
271
  lastProject: (typeof lp === "string" && /^[a-z0-9-]+$/.test(lp)) ? lp : null,
272
+ lastRunBySlug: lrPrev,
246
273
  aiW: (body ? intIn(body.aiW, 240, 760) : null) ?? prev.aiW,
247
274
  aiH: (body ? intIn(body.aiH, 200, 1800) : null) ?? prev.aiH,
248
275
  aiR: (body ? intIn(body.aiR, 0, 4000) : null) ?? prev.aiR,
@@ -322,6 +349,31 @@ async function handleApi(ctx, root, req, res) {
322
349
  }
323
350
  return sendJson(res, 404, { ok: false, message: "not found" });
324
351
  }
352
+ // —— /issue2pr/api/agents/discover:委外智能体(claude CLI)多方式发现 ——
353
+ // 五种来源去重合并:项目配置 > 环境变量 > 常见安装位置 > npm 全局目录 > PATH 查找;
354
+ // ?slug= 带项目时把该项目配置的 claudeBin 列为首位候选。测试钩子环境未注入 runNpmPrefix
355
+ // 时跳过 npm 来源(避免单测真跑 npm config get prefix)。
356
+ if (parts[2] === "agents" && parts[3] === "discover" && !parts[4] && m === "GET") {
357
+ const slugQ = url.searchParams.get("slug");
358
+ const project = (slugQ && /^[a-z0-9-]+$/.test(slugQ)) ? loadProject(root, slugQ) : null;
359
+ const out = await discoverAgents({
360
+ cfgBin: (project && project.stageConfig && project.stageConfig.P6 && project.stageConfig.P6.params && project.stageConfig.P6.params.claudeBin) || "",
361
+ runWhich: __testHooks?.runWhich || realRunWhich,
362
+ runNpmPrefix: __testHooks ? __testHooks.runNpmPrefix : realRunNpmPrefix,
363
+ });
364
+ return sendJson(res, 200, { ok: true, agents: out.agents, resolved: out.resolved });
365
+ }
366
+ // —— /issue2pr/api/agents/test:委外智能体测试门禁 ——
367
+ // 三步:定位 → --version → headless 认证微任务(真实极小调用,403 IP 白名单/未登录在此拦截)。
368
+ // body {bin?: string, timeoutMs?: 10000..180000};ok=false 也回 200(与 connections/test 同约定)。
369
+ if (parts[2] === "agents" && parts[3] === "test" && !parts[4] && m === "POST") {
370
+ const body = await readBody(req);
371
+ const bin = typeof body?.bin === "string" ? body.bin.trim() : "";
372
+ const t = Number(body?.timeoutMs);
373
+ const timeoutMs = Number.isFinite(t) && t >= 10000 && t <= 180000 ? t : 120000;
374
+ const gate = await testAgentGate({ bin, timeoutMs, runners: (__testHooks && __testHooks.agentProbes) || {} });
375
+ return sendJson(res, 200, { ok: gate.ok, gate });
376
+ }
325
377
  // —— /issue2pr/api/preflight:环境健康探测(git 二进制 / claude CLI / LLM 默认路由与来源) ——
326
378
  // 纯只读探测:git --version、claudeBin 解析 + 存在性/PATH 校验、resolveRoute 现算;不发真实 LLM 请求。
327
379
  if (parts[2] === "preflight" && !parts[3] && m === "GET") {
@@ -423,6 +475,17 @@ async function handleApi(ctx, root, req, res) {
423
475
  if (m === "GET") return sendJson(res, 200, { ok: true, projects: listProjects(root) });
424
476
  if (m === "POST") {
425
477
  const body = await readBody(req);
478
+ // —— 委外智能体保存门禁(兜底):p6Mode=claude 时 claude CLI 必须可运行(--version 快检) ——
479
+ // 认证级校验(403 IP 白名单等)由项目页「测试门禁」真实微任务完成;此处拦路径写错/未安装。
480
+ // 测试钩子环境未注入 agentProbes 时跳过(既有单测不依赖本机 claude)。
481
+ if (body && body.p6Mode === "claude" && (!__testHooks || __testHooks.agentProbes)) {
482
+ const bin = resolveClaudeBin((body.stageConfig && body.stageConfig.P6 && body.stageConfig.P6.params && body.stageConfig.P6.params.claudeBin) || "");
483
+ const runV = __testHooks?.agentProbes?.runVersion || realRunVersion;
484
+ const r = await new Promise((res2) => runV(bin, {}, (err) => res2({ err })));
485
+ if (r.err) {
486
+ return sendJson(res, 400, { ok: false, message: "委外智能体门禁未通过:claude CLI 无法运行(" + String((r.err && r.err.message) || r.err).slice(0, 200) + ")。请在「项目」页 P6 执行模式选「委托 Claude Code」,用委外智能体卡片选择可用安装或修正路径,通过测试门禁后再保存。" });
487
+ }
488
+ }
426
489
  try { saveProject(root, body); }
427
490
  catch (e) { return sendJson(res, 400, { ok: false, message: (e && e.message) || String(e) }); }
428
491
  return sendJson(res, 200, { ok: true, project: body });
package/lib/agents.js ADDED
@@ -0,0 +1,204 @@
1
+ // lib/agents.js — 委外智能体(Claude Code CLI)发现与测试门禁
2
+ // 背景:claude CLI 只在 Run 的 P6 阶段才真实调用,认证类错误(403 IP 白名单等)
3
+ // 到那时才暴露,浪费一轮委托。发现 + 门禁都前置到「绑定」时:
4
+ // 发现:项目配置 > 环境变量 > 常见安装位置 > npm 全局目录 > PATH 查找(五种来源,去重合并)
5
+ // 门禁:定位 → --version(可运行)→ headless 微任务(真实过一遍认证,403 在此拦截)
6
+ // 全部探测函数可注入(index.js 传 __testHooks 版本),单测不依赖本机 claude。
7
+ import { execFile, spawn } from "node:child_process";
8
+ import { existsSync } from "node:fs";
9
+ import { resolveClaudeBin, claudeCommonCandidates } from "./stages/p6-coder.js";
10
+
11
+ export const AGENT_SOURCE_LABELS = {
12
+ configured: "项目配置",
13
+ env: "环境变量",
14
+ common: "常见安装位置",
15
+ npm: "npm 全局目录",
16
+ path: "PATH 查找",
17
+ };
18
+
19
+ // 去重键:Windows 路径不分大小写,分隔符归一
20
+ function pathKey(p) {
21
+ return String(p).replace(/[\\/]+/g, "/").replace(/\/+$/, "").toLowerCase();
22
+ }
23
+
24
+ // —— 真实探测 runner(生产路径;测试经 __testHooks 注入替换) ——
25
+ // Windows 上 claude 通常是 .cmd,execFile 无 shell 会 EINVAL,统一走 shell:true 的 spawn。
26
+ // 与 p6-coder runClaude 同策略:路径含空格属既有已知限制。
27
+ function shCollect(bin, args, timeoutMs) {
28
+ return new Promise((resolve) => {
29
+ let child;
30
+ try { child = spawn(bin, args, { shell: true, windowsHide: true }); }
31
+ catch (e) { return resolve({ code: -1, error: String((e && e.message) || e), stdout: "", stderr: "" }); }
32
+ let stdout = "", stderr = "", done = false;
33
+ const finish = (r) => { if (done) return; done = true; clearTimeout(timer); resolve(r); };
34
+ const timer = setTimeout(() => finish({ code: -2, timeout: true, stdout, stderr }), timeoutMs);
35
+ child.on("error", (e) => finish({ code: -1, error: String((e && e.message) || e), stdout, stderr }));
36
+ child.stdout?.on("data", (d) => { stdout += d; });
37
+ child.stderr?.on("data", (d) => { stderr += d; });
38
+ child.on("close", (code) => finish({ code: code == null ? -1 : code, stdout, stderr }));
39
+ });
40
+ }
41
+
42
+ export function realRunVersion(bin, opts, cb) {
43
+ shCollect(bin, ["--version"], 15000).then((r) => {
44
+ if (r.code !== 0) return cb(new Error(String(r.stderr || r.error || ("退出码 " + r.code)).slice(0, 300)), "", r.stderr);
45
+ cb(null, r.stdout, r.stderr);
46
+ });
47
+ }
48
+
49
+ // 认证微任务:-p headless 一次极小真实调用(一轮、几十 token,费用可忽略)。
50
+ // prompt 走 stdin(shell:true 下参数不转义,与 p6 委托同策略)。
51
+ export function realRunPrompt(bin, opts, cb) {
52
+ const timeoutMs = Number(opts && opts.timeoutMs) > 0 ? Number(opts.timeoutMs) : 120000;
53
+ let child;
54
+ try { child = spawn(bin, ["-p", "--output-format", "json"], { shell: true, windowsHide: true }); }
55
+ catch (e) { return void cb(new Error(String((e && e.message) || e))); }
56
+ let stdout = "", stderr = "", done = false;
57
+ const finish = (r) => { if (done) return; done = true; clearTimeout(timer); cb(null, r); };
58
+ const timer = setTimeout(() => finish({ code: -2, timeout: true, stdout, stderr }), timeoutMs);
59
+ child.on("error", (e) => finish({ code: -1, error: String((e && e.message) || e), stdout, stderr }));
60
+ child.stdout?.on("data", (d) => { if (stdout.length < 1e6) stdout += d; });
61
+ child.stderr?.on("data", (d) => { if (stderr.length < 1e6) stderr += d; });
62
+ child.on("close", (code) => finish({ code: code == null ? -1 : code, stdout, stderr }));
63
+ try { child.stdin.write("Reply with exactly: OK"); child.stdin.end(); }
64
+ catch (e) { finish({ code: -1, error: "stdin 写入失败: " + String((e && e.message) || e), stdout, stderr }); }
65
+ }
66
+
67
+ export function realRunWhich(args, opts, cb) {
68
+ execFile(process.platform === "win32" ? "where" : "which", args, { ...opts, windowsHide: true }, cb);
69
+ }
70
+
71
+ export function realRunNpmPrefix(args, opts, cb) {
72
+ // Windows 的 npm 是 npm.cmd,须 shell 才能拉起
73
+ execFile("npm", args, { ...opts, shell: process.platform === "win32" }, cb);
74
+ }
75
+
76
+ // —— 发现:五种来源合并去重,逐条标注来源与存在性 ——
77
+ export async function discoverAgents({ cfgBin = "", envBin = process.env.ISSUE2PR_CLAUDE_BIN || "", runWhich, runNpmPrefix } = {}) {
78
+ const agents = [];
79
+ const seen = new Set();
80
+ const push = (path, source) => {
81
+ const t = String(path || "").trim();
82
+ if (!t) return;
83
+ const k = pathKey(t);
84
+ if (seen.has(k)) return;
85
+ seen.add(k);
86
+ agents.push({ path: t, source, label: AGENT_SOURCE_LABELS[source] || source, exists: existsSync(t) });
87
+ };
88
+ if (cfgBin) push(cfgBin, "configured");
89
+ if (envBin) push(envBin, "env");
90
+ for (const p of claudeCommonCandidates()) push(p, "common");
91
+ // npm 全局前缀:自定义 npm 全局目录(如 D:\npm-global)的第一手来源
92
+ if (runNpmPrefix) {
93
+ const prefix = await new Promise((r) => runNpmPrefix(["config", "get", "prefix"], { timeout: 8000 }, (err, stdout) =>
94
+ r(err ? "" : String(stdout || "").trim().split(/\r?\n/).filter(Boolean).pop() || "")));
95
+ if (prefix) push(prefix + (process.platform === "win32" ? "\\claude.cmd" : "/bin/claude"), "npm");
96
+ }
97
+ // PATH 查找:where/which 可能多条命中,全部收录
98
+ if (runWhich) {
99
+ await new Promise((r) => runWhich(["claude"], { timeout: 5000 }, (err, stdout) => {
100
+ if (!err && stdout) String(stdout).split(/\r?\n/).map((s) => s.trim()).filter(Boolean).forEach((p) => push(p, "path"));
101
+ r();
102
+ }));
103
+ }
104
+ return { agents, resolved: resolveClaudeBin(cfgBin) };
105
+ }
106
+
107
+ // —— 认证类错误 → 可操作提示(用户实测:403 IP access denied by API-Key restrictions) ——
108
+ function authHintOf(text) {
109
+ const t = String(text || "");
110
+ if (/403|ip access denied|api[- ]?key restriction/i.test(t)) {
111
+ return "API Key 有 IP 访问限制:当前出口 IP 不在白名单(常见于公司代理 / VPN)。请到 Anthropic Console 调整该 Key 的 IP 白名单,或更换网络出口 / 无限制 Key 后重测。";
112
+ }
113
+ if (/401|authenticat|not logged in|please log ?in|login required/i.test(t)) {
114
+ return "claude CLI 未登录或凭据失效:在终端运行 claude 完成登录(或检查 ANTHROPIC_API_KEY 环境变量),完成后重测。";
115
+ }
116
+ if (/429|rate limit/i.test(t)) return "触发限流:请稍后重测。";
117
+ if (/timeout|etimedout|econnrefused|enotfound|network/i.test(t)) {
118
+ return "网络不可达:检查代理 / 防火墙对 api.anthropic.com 的放行情况后重测。";
119
+ }
120
+ return "";
121
+ }
122
+
123
+ // 从输出里挑出最像错误的一行(claude 的报错通常在 stderr 或 stdout 尾部)
124
+ function errorLineOf({ stderr, stdout, code }) {
125
+ const lines = String(stderr || "").split(/\r?\n/).concat(String(stdout || "").split(/\r?\n/));
126
+ const hit = lines.find((l) => /error|failed|denied|invalid|not logged|40[134]/i.test(l) && l.trim());
127
+ return (hit || lines.find((l) => l.trim()) || ("退出码 " + code)).trim().slice(0, 300);
128
+ }
129
+
130
+ // —— 测试门禁:定位 → 版本 → 认证微任务,三步全绿才算通过 ——
131
+ // runners 可注入(单测):{ runWhich, runVersion, runPrompt }
132
+ export async function testAgentGate({ bin = "", runners = {}, timeoutMs = 120000 } = {}) {
133
+ const R = {
134
+ runWhich: runners.runWhich || realRunWhich,
135
+ runVersion: runners.runVersion || realRunVersion,
136
+ runPrompt: runners.runPrompt || realRunPrompt,
137
+ };
138
+ const steps = [];
139
+ const t0 = Date.now();
140
+ const fail = (message, hint) => ({ ok: false, bin, steps, message, hint: hint || "", ms: Date.now() - t0 });
141
+
142
+ const target = String(bin || "").trim() || resolveClaudeBin("");
143
+ let path = target;
144
+
145
+ // 1) 定位:显式路径查存在性;裸名 claude 走 PATH
146
+ {
147
+ const t1 = Date.now();
148
+ let detail = "";
149
+ let ok = true;
150
+ if (!/^claude(\.cmd|\.exe)?$/i.test(target) || existsSync(target)) {
151
+ if (existsSync(target)) detail = "已定位 " + target;
152
+ else { ok = false; detail = "文件不存在: " + target; }
153
+ } else {
154
+ const hits = await new Promise((r) => R.runWhich([target], { timeout: 5000 }, (err, stdout) =>
155
+ r(err ? null : String(stdout || "").split(/\r?\n/).map((s) => s.trim()).filter(Boolean))));
156
+ if (hits && hits.length) { path = hits[0]; detail = "PATH 命中 " + path; }
157
+ else { ok = false; detail = "PATH 中未找到 claude 命令(可改用完整路径,或确认安装)"; }
158
+ }
159
+ steps.push({ name: "定位", ok, detail, ms: Date.now() - t1 });
160
+ if (!ok) return fail(detail);
161
+ }
162
+
163
+ // 2) 版本:--version 可运行(拦路径写错 / 损坏的安装)
164
+ let version = "";
165
+ {
166
+ const t1 = Date.now();
167
+ const r = await new Promise((res) => R.runVersion(path, {}, (err, stdout, stderr) => res({ err, stdout, stderr })));
168
+ version = String(r.stdout || "").trim().split(/\r?\n/)[0] || "";
169
+ const ok = !r.err;
170
+ steps.push({ name: "版本", ok, detail: ok ? (version || "无版本输出") : String((r.err && r.err.message) || r.stderr || "运行失败").slice(0, 300), ms: Date.now() - t1 });
171
+ if (!ok) return fail("claude CLI 无法运行(--version 失败): " + steps[steps.length - 1].detail);
172
+ }
173
+
174
+ // 3) 认证:headless 微任务真实过一遍凭据(403 IP 白名单 / 未登录在此拦截)
175
+ {
176
+ const t1 = Date.now();
177
+ const r = await new Promise((res) => R.runPrompt(path, { timeoutMs }, (err, out) => res({ err, out })));
178
+ if (r.err) {
179
+ const msg = String((r.err && r.err.message) || r.err);
180
+ steps.push({ name: "认证", ok: false, detail: msg.slice(0, 300), ms: Date.now() - t1 });
181
+ return fail("认证微任务未能执行: " + msg.slice(0, 300), authHintOf(msg));
182
+ }
183
+ const out = r.out || {};
184
+ let ok = out.code === 0;
185
+ let result = "";
186
+ if (ok) {
187
+ try {
188
+ const j = JSON.parse(String(out.stdout || "").trim());
189
+ if (j && typeof j === "object" && j.is_error) { ok = false; result = String(j.result || ""); }
190
+ else result = String((j && j.result) || "");
191
+ } catch { /* 非 JSON 输出但退出 0:视为通过(版本差异) */ }
192
+ }
193
+ const text = (out.stderr || "") + "\n" + (out.stdout || "");
194
+ steps.push({
195
+ name: "认证", ok,
196
+ detail: ok ? ("模型回执: " + (result || "(空)").slice(0, 80)) : (out.timeout ? "微任务超时" : errorLineOf(out)),
197
+ ms: Date.now() - t1,
198
+ });
199
+ if (!ok) {
200
+ return fail("claude 认证/调用未通过: " + steps[steps.length - 1].detail, authHintOf(text + " " + (out.timeout ? "timeout" : "")));
201
+ }
202
+ return { ok: true, bin, path, version, steps, message: "门禁通过(" + (version || "版本未知") + " · 认证正常)", ms: Date.now() - t0 };
203
+ }
204
+ }
@@ -29,15 +29,18 @@ export function killExternal(runDir) {
29
29
 
30
30
  // claude 可执行文件解析:阶段配置(params.claudeBin)> 环境变量 ISSUE2PR_CLAUDE_BIN > 常见安装位置探测。
31
31
  // 开源环境安装路径各异,UI「配置」可显式指定;PATH 里未必有 claude(如 npm 全局目录不在系统 PATH)。
32
- // preflight 端点复用本函数做健康探测(项目页显示 claude CLI 是否就绪)。
33
- export function resolveClaudeBin(cfgBin) {
34
- if (cfgBin) return cfgBin;
35
- if (process.env.ISSUE2PR_CLAUDE_BIN) return process.env.ISSUE2PR_CLAUDE_BIN;
32
+ // preflight 端点与 lib/agents.js 的发现/门禁复用本函数做健康探测。
33
+ export function claudeCommonCandidates() {
36
34
  const home = homedir();
37
- const cands = process.platform === "win32"
35
+ return process.platform === "win32"
38
36
  ? [join(home, ".npm-global", "claude.cmd"), join(home, ".npm_global", "claude.cmd"), join(home, "AppData", "Roaming", "npm", "claude.cmd"), join(home, ".local", "bin", "claude.exe")]
39
37
  : [join(home, ".local", "bin", "claude"), "/usr/local/bin/claude", "/opt/homebrew/bin/claude"];
40
- return cands.find((p) => existsSync(p)) || "claude";
38
+ }
39
+
40
+ export function resolveClaudeBin(cfgBin) {
41
+ if (cfgBin) return cfgBin;
42
+ if (process.env.ISSUE2PR_CLAUDE_BIN) return process.env.ISSUE2PR_CLAUDE_BIN;
43
+ return claudeCommonCandidates().find((p) => existsSync(p)) || "claude";
41
44
  }
42
45
 
43
46
  // 无人值守跑 claude:-p headless;skip-permissions 无交互放行;--add-dir 允许写 run 产物目录(cwd 是仓库)。
@@ -168,7 +171,11 @@ async function delegateClaude(rcx, graph) {
168
171
  return { artifact: "06-implementation/", summary: "Claude Code 已执行任务包:" + countPatches(runDir) + " 份 patch + report,等待人工复核" + statsSummary(stats) };
169
172
  }
170
173
 
171
- const reason = r.error || (r.timeout ? "超时被终止" : "退出码 " + r.code + (r.stderr ? ":" + String(r.stderr).slice(0, 300) : ""));
174
+ // 认证类失败(403 IP 白名单 / 未登录)给出可操作引导——到项目页委外智能体卡片重跑门禁
175
+ const rawReason = r.error || (r.timeout ? "超时被终止" : "退出码 " + r.code + (r.stderr ? ":" + String(r.stderr).slice(0, 300) : ""));
176
+ const authHit = /40[13]|authenticat|api[- ]?key|ip access|not logged in/i.test(String(r.stderr || "") + "\n" + String(r.stdout || ""));
177
+ const reason = rawReason + (authHit
178
+ ? ";疑似认证/授权问题——请到「项目」页委外智能体卡片重跑测试门禁(检查 Claude 登录状态、API Key 的 IP 白名单或代理出口)" : "");
172
179
  setExec({ status: r.error ? "skipped" : "failed", exitCode: r.code, stats, error: String(reason).slice(0, 500), finishedAt: new Date().toISOString() });
173
180
  logEvent(rcx, { kind: "tool", name: "Claude Code 执行未产出补丁", detail: String(reason).slice(0, 500), ok: false });
174
181
  // 回退等人工:保持 external 语义(UI 显示"等外部执行"并拦截空产物 approve),人可接管 session-task.md
package/package.json CHANGED
@@ -1,25 +1,54 @@
1
1
  {
2
2
  "name": "dsh-issue2pr",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Issue-to-PR 可验证交付链:项目配置 → 11 阶段流水线 → 人工复核 → PR 说明(DSH 全局插件)",
5
5
  "type": "module",
6
6
  "main": "./index.js",
7
- "exports": { ".": "./index.js", "./client": "./client.js", "./package.json": "./package.json" },
8
- "scripts": { "test": "node --test \"tests/**/*.test.js\"" },
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./client": "./client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "scripts": {
13
+ "test": "node --test \"tests/**/*.test.js\""
14
+ },
9
15
  "repository": {
10
16
  "type": "git",
11
17
  "url": "git+https://github.com/LONGSASASASASA/dsh-issue2pr.git"
12
18
  },
13
- "keywords": ["dsh", "dsh-plugin", "deepseek-harness", "issue", "pull-request", "pipeline", "code-review", "cordis"],
19
+ "keywords": [
20
+ "dsh",
21
+ "dsh-plugin",
22
+ "deepseek-harness",
23
+ "issue",
24
+ "pull-request",
25
+ "pipeline",
26
+ "code-review",
27
+ "cordis"
28
+ ],
14
29
  "author": "LONGSASASASASA",
15
30
  "license": "MIT",
16
- "files": ["index.js", "client.js", "lib/", "cordis.patch.yml", "docs/assets/"],
17
- "publishConfig": { "registry": "https://registry.npmjs.org/" },
31
+ "files": [
32
+ "index.js",
33
+ "client.js",
34
+ "lib/",
35
+ "cordis.patch.yml",
36
+ "docs/assets/"
37
+ ],
38
+ "publishConfig": {
39
+ "registry": "https://registry.npmjs.org/"
40
+ },
18
41
  "dsh": {
19
- "bundle": { "patch": "./cordis.patch.yml" },
42
+ "bundle": {
43
+ "patch": "./cordis.patch.yml"
44
+ },
20
45
  "client": {
21
46
  "platform": "web",
22
- "inject": ["@deepseek-ai/dsh-client-runtime", "@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-ui-settings"]
47
+ "inject": [
48
+ "@deepseek-ai/dsh-client-runtime",
49
+ "@deepseek-ai/dsh-client-locale",
50
+ "@deepseek-ai/dsh-client-ui-settings"
51
+ ]
23
52
  }
24
53
  },
25
54
  "peerDependencies": {