@aipanel/core 1.2.9 → 1.2.11

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.
@@ -2,6 +2,7 @@
2
2
  * 通用类型(Provider 无关)
3
3
  * Provider 专属类型已下沉至 @aipanel/provider-opencode。
4
4
  */
5
+ import type { SessionPendingKind, SessionStatus } from "./provider";
5
6
  /**
6
7
  * 展示模式类型
7
8
  */
@@ -123,16 +124,27 @@ export type AIPanelWidgetTheme = "light" | "dark" | "auto";
123
124
  */
124
125
  export type ServiceStatus = "idle" | "starting" | "ready" | "partial" | "failed";
125
126
  /**
126
- * Session 状态类型
127
+ * Session 状态类型(别名:以 ./provider 的 SessionStatus 为单一来源)
127
128
  */
128
- export type AIPanelSessionStatusType = "idle" | "running" | "streaming" | "completed";
129
+ export type AIPanelSessionStatusType = SessionStatus;
130
+ /**
131
+ * Session 交互状态(单一来源:./types.ts 定义,事件载荷类型见 ./provider.ts 的 SessionPendingKind)
132
+ */
133
+ export type AIPanelSessionPendingKind = SessionPendingKind;
129
134
  /**
130
135
  * Session 思考状态
131
136
  */
132
137
  export interface AIPanelSessionThinkingState {
133
138
  thinking: boolean;
134
139
  statusType: AIPanelSessionStatusType;
140
+ /** 是否存在待用户交互(审批/提问/计划评审);pending 优先级高于 thinking */
135
141
  hasPending: boolean;
142
+ /** 待交互类型(hasPending=true 时有值;单一来源 ./provider.ts) */
143
+ pendingKind?: AIPanelSessionPendingKind;
144
+ /** 是否刚运行完成(client 端由 running→idle 边沿且非当前选中推导;对齐官方 completed 提醒语义) */
145
+ completed?: boolean;
146
+ /** 进行中的子代理数(>0 时即使自身 idle 也视为"在跑";源:session.subagents) */
147
+ subagentsRunning?: number;
136
148
  }
137
149
  /**
138
150
  * 挂件会话信息
@@ -146,19 +158,9 @@ export interface AIPanelWidgetSession {
146
158
  url?: string;
147
159
  }
148
160
  /**
149
- * 挂件选中的元素
161
+ * 挂件选中的元素(别名:与 host 端 SelectedElement 同构,单一来源为本文件上方的 SelectedElement)
150
162
  */
151
- export interface AIPanelSelectedElement {
152
- /** 节点唯一 id(与 SelectedElement.id 同源,`@节点[n<id>]` 标记与上下文注入共用) */
153
- id?: string;
154
- filePath: string | null;
155
- line: number | null;
156
- column: number | null;
157
- innerText: string;
158
- description?: string;
159
- /** 用户选中节点时的页面 URL(AIPanel 附加;host 端上下文注入用) */
160
- previewPageUrl?: string;
161
- }
163
+ export type AIPanelSelectedElement = SelectedElement;
162
164
  /**
163
165
  * 单条代码诊断(1-based 行列坐标)——AIPanel 诊断工具(run_diagnostics 等)的
164
166
  * canonical 持久化/展示共用协议:宿主插件写 tool/result.meta,client 插件据此渲染卡片。
@@ -21,10 +21,15 @@ __export(utils_exports, {
21
21
  base64Encode: () => base64Encode,
22
22
  ensureNodeId: () => ensureNodeId,
23
23
  extractTextFromResponse: () => extractTextFromResponse,
24
+ parseNodeMentions: () => parseNodeMentions,
24
25
  sleep: () => sleep,
25
- truncate: () => truncate
26
+ toNodeMention: () => toNodeMention,
27
+ truncate: () => truncate,
28
+ widgetEnvelope: () => widgetEnvelope,
29
+ withRetries: () => withRetries
26
30
  });
27
31
  module.exports = __toCommonJS(utils_exports);
32
+ var import_constants = require("./constants.cjs");
28
33
  function ensureNodeId(element) {
29
34
  if (element.id) return element.id;
30
35
  const random = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID().replace(/-/g, "").slice(0, 8) : Math.random().toString(36).slice(2, 10);
@@ -77,12 +82,52 @@ function extractTextFromResponse(data) {
77
82
  }
78
83
  return null;
79
84
  }
85
+ const NODE_MENTION_RE = /@节点\[(n[0-9a-z]+)\]/g;
86
+ function toNodeMention(id) {
87
+ return `@\u8282\u70B9[${id}]`;
88
+ }
89
+ function parseNodeMentions(text) {
90
+ const out = [];
91
+ const seen = /* @__PURE__ */ new Set();
92
+ for (const m of text.matchAll(NODE_MENTION_RE)) {
93
+ const id = m[1];
94
+ if (!seen.has(id)) {
95
+ seen.add(id);
96
+ out.push(id);
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+ function widgetEnvelope(type, data) {
102
+ return { type, ...data };
103
+ }
104
+ async function withRetries(fn, options = {}) {
105
+ const attempts = options.attempts ?? import_constants.DEFAULT_RETRIES;
106
+ const delayMs = options.delayMs ?? import_constants.RETRY_DELAY;
107
+ let lastError;
108
+ for (let i = 0; i < attempts; i++) {
109
+ try {
110
+ return await fn(i);
111
+ } catch (e) {
112
+ lastError = e;
113
+ if (i < attempts - 1) {
114
+ options.onRetry?.(i + 1, e);
115
+ if (delayMs > 0) await sleep(delayMs);
116
+ }
117
+ }
118
+ }
119
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
120
+ }
80
121
  // Annotate the CommonJS export names for ESM import in node:
81
122
  0 && (module.exports = {
82
123
  base64Decode,
83
124
  base64Encode,
84
125
  ensureNodeId,
85
126
  extractTextFromResponse,
127
+ parseNodeMentions,
86
128
  sleep,
87
- truncate
129
+ toNodeMention,
130
+ truncate,
131
+ widgetEnvelope,
132
+ withRetries
88
133
  });
@@ -1,6 +1,3 @@
1
- /**
2
- * @fileoverview 通用工具函数
3
- */
4
1
  /**
5
2
  * 取(或生成)元素的节点唯一 id:优先复用已赋值的 id,否则生成随机 id 并写回元素。
6
3
  * 同一引用在会话标记(`@节点[n<id>]`)与上下文注入里使用同一个 id;
@@ -42,3 +39,29 @@ export declare function base64Decode(base64: string): string;
42
39
  * 支持多种常见响应格式
43
40
  */
44
41
  export declare function extractTextFromResponse(data: unknown): string | null;
42
+ /**
43
+ * 生成节点提及标记 `@节点[n<id>]`(与 ensureNodeId 分配的 n<hex> id 体系一致)
44
+ */
45
+ export declare function toNodeMention(id: string): string;
46
+ /**
47
+ * 从文本中提取全部节点提及 id(去重、保序)
48
+ */
49
+ export declare function parseNodeMentions(text: string): string[];
50
+ /**
51
+ * 构造 iframe postMessage 信封:{ type, ...data }(AIPanel 挂件与 dsh-client 共用同一形状)
52
+ */
53
+ export declare function widgetEnvelope(type: string, data?: Record<string, unknown>): Record<string, unknown>;
54
+ /** 重试配置 */
55
+ export interface RetryOptions {
56
+ /** 总尝试次数(默认 core DEFAULT_RETRIES) */
57
+ attempts?: number;
58
+ /** 失败后连续尝试间隔毫秒(默认 core RETRY_DELAY) */
59
+ delayMs?: number;
60
+ /** 每次失败、重试前回调(attempt 为 1-based) */
61
+ onRetry?: (attempt: number, error: unknown) => void;
62
+ }
63
+ /**
64
+ * 带线性重试的执行包裹:执行 fn 直到成功或尝试完毕;最后一次失败原样抛出。
65
+ * 环境无关(仅使用 setTimeout),放于 common 供浏览器与 node 共用。
66
+ */
67
+ export declare function withRetries<T>(fn: (attempt: number) => Promise<T>, options?: RetryOptions): Promise<T>;
@@ -27,6 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
28
  var diagnostics_exports = {};
29
29
  __export(diagnostics_exports, {
30
+ DIAGNOSTICS_TOOL_DESCRIPTION: () => DIAGNOSTICS_TOOL_DESCRIPTION,
30
31
  findAllTsconfigDirs: () => findAllTsconfigDirs,
31
32
  findTsconfigDir: () => findTsconfigDir,
32
33
  formatDiagnosticsSections: () => formatDiagnosticsSections,
@@ -59,6 +60,19 @@ const JS_EXTENSIONS = /* @__PURE__ */ new Set([
59
60
  function isJsFile(filePath) {
60
61
  return JS_EXTENSIONS.has(import_node_path.default.extname(filePath));
61
62
  }
63
+ const DIAGNOSTICS_TOOL_DESCRIPTION = [
64
+ "\u8FD0\u884C ESLint \u4E0E TypeScript \u7C7B\u578B\u8BCA\u65AD\uFF0C\u8FD4\u56DE\u8BCA\u65AD\u7ED3\u679C\u3002",
65
+ "",
66
+ "**\u652F\u6301\u7684\u6587\u4EF6\u7C7B\u578B**\uFF1A",
67
+ `- ESLint\uFF1AJavaScript / TypeScript / Vue \u6E90\u7801\uFF08${[...JS_EXTENSIONS].map((e) => `*${e}`).join(" ")}\uFF09`,
68
+ "- TypeScript \u7C7B\u578B\u68C0\u67E5\uFF1A*.ts *.tsx *.vue",
69
+ "",
70
+ "**\u4F55\u65F6\u4F7F\u7528\u6B64\u5DE5\u5177**\uFF1A",
71
+ "- \u521A\u5B8C\u6210\u4EE3\u7801\u4FEE\u6539\uFF0C\u60F3\u9A8C\u8BC1\u662F\u5426\u6709 ESLint \u9519\u8BEF\u6216\u7C7B\u578B\u9519\u8BEF",
72
+ "- \u5728\u63D0\u4EA4\u4EE3\u7801\u524D\u8FDB\u884C\u8D28\u91CF\u68C0\u67E5",
73
+ "- \u6392\u67E5\u7F16\u8F91\u5668\u672A\u663E\u793A\u4F46\u5B9E\u9645\u5B58\u5728\u7684\u7C7B\u578B\u95EE\u9898",
74
+ "- \u4E0D\u4F20\u53C2\u6570\u53EF\u5168\u91CF\u8BCA\u65AD\u6574\u4E2A\u9879\u76EE"
75
+ ].join("\n");
62
76
  let ESLintClass;
63
77
  function loadESLint(workspace) {
64
78
  if (ESLintClass) return;
@@ -74,7 +88,12 @@ function loadESLint(workspace) {
74
88
  }
75
89
  async function lintFiles(pattern, cwd, warnLimit = 5) {
76
90
  loadESLint(cwd);
77
- if (!ESLintClass) return {};
91
+ if (!ESLintClass) {
92
+ return {
93
+ text: `[ESLint] \u672A\u8FD0\u884C\uFF1A\u65E0\u6CD5\u5728 workspace "${cwd}" \u89E3\u6790\u5230 eslint\uFF08\u4EC5\u663E\u793A TypeScript \u8BCA\u65AD\uFF09`,
94
+ diagnostics: []
95
+ };
96
+ }
78
97
  try {
79
98
  const eslint = new ESLintClass({ cwd });
80
99
  const results = await eslint.lintFiles(pattern);
@@ -122,7 +141,10 @@ async function lintFiles(pattern, cwd, warnLimit = 5) {
122
141
  return { text: lines.length > 0 ? lines.join("\n") : void 0, diagnostics };
123
142
  } catch (err) {
124
143
  log.warn("ESLint failed", { pattern, error: err.message });
125
- return {};
144
+ return {
145
+ text: `[ESLint] \u8FD0\u884C\u5931\u8D25\uFF1A${err.message}\uFF08\u4EC5\u663E\u793A TypeScript \u8BCA\u65AD\uFF09`,
146
+ diagnostics: []
147
+ };
126
148
  }
127
149
  }
128
150
  let _vueTscBin;
@@ -296,6 +318,7 @@ function formatDiagnosticsSections(title, eslintOutput, tscOutput) {
296
318
  }
297
319
  // Annotate the CommonJS export names for ESM import in node:
298
320
  0 && (module.exports = {
321
+ DIAGNOSTICS_TOOL_DESCRIPTION,
299
322
  findAllTsconfigDirs,
300
323
  findTsconfigDir,
301
324
  formatDiagnosticsSections,
@@ -1,5 +1,10 @@
1
1
  /** 是否为可诊断的源码文件(供宿主钩子过滤 edit/write 目标) */
2
2
  export declare function isJsFile(filePath: string): boolean;
3
+ /**
4
+ * run_diagnostics 工具描述(单一来源,供 opencode / dsh 两侧插件引用):
5
+ * 只声明能力与支持的文件类型,不涉及内部使用的检查工具。
6
+ */
7
+ export declare const DIAGNOSTICS_TOOL_DESCRIPTION: string;
3
8
  /** LSP 风格诊断项(供宿主写入 metadata.diagnostics 等结构化输出) */
4
9
  export interface DiagnosticItem {
5
10
  /** 所属文件路径(相对/绝对,按来源解析);跨文件诊断(全量模式)时必有 */
@@ -49,21 +49,6 @@ const LEVEL_COLORS = {
49
49
  [import_logger_core.LogLevel.ERROR]: COLORS.red,
50
50
  [import_logger_core.LogLevel.NONE]: COLORS.reset
51
51
  };
52
- const LEVEL_NAMES = {
53
- [import_logger_core.LogLevel.DEBUG]: "DEBUG",
54
- [import_logger_core.LogLevel.INFO]: "INFO",
55
- [import_logger_core.LogLevel.WARN]: "WARN",
56
- [import_logger_core.LogLevel.ERROR]: "ERROR",
57
- [import_logger_core.LogLevel.NONE]: "NONE"
58
- };
59
- function getTimestamp() {
60
- const now = /* @__PURE__ */ new Date();
61
- const hours = String(now.getHours()).padStart(2, "0");
62
- const minutes = String(now.getMinutes()).padStart(2, "0");
63
- const seconds = String(now.getSeconds()).padStart(2, "0");
64
- const ms = String(now.getMilliseconds()).padStart(3, "0");
65
- return `${hours}:${minutes}:${seconds}.${ms}`;
66
- }
67
52
  function getCallerInfo(depth = 3) {
68
53
  const stack = new Error().stack;
69
54
  if (!stack) return "";
@@ -82,10 +67,10 @@ function log(level, message, context, ...args) {
82
67
  const parts = [];
83
68
  parts.push(`${COLORS.dim}[${process.pid}]${COLORS.reset}`);
84
69
  if ((0, import_logger_core.getConfig)().showTimestamp) {
85
- parts.push(`${COLORS.dim}${getTimestamp()}${COLORS.reset}`);
70
+ parts.push(`${COLORS.dim}${(0, import_logger_core.getTimestamp)()}${COLORS.reset}`);
86
71
  }
87
72
  const levelColor = LEVEL_COLORS[level];
88
- const levelName = LEVEL_NAMES[level].padEnd(5);
73
+ const levelName = import_logger_core.LEVEL_NAMES[level].padEnd(5);
89
74
  parts.push(`${levelColor}${levelName}${COLORS.reset}`);
90
75
  parts.push(`${COLORS.bright}${import_constants.LOG_PREFIX}${COLORS.reset}`);
91
76
  const contextStr = (0, import_logger_core.formatContext)(context);
@@ -28,15 +28,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
28
28
  var node_utils_exports = {};
29
29
  __export(node_utils_exports, {
30
30
  checkChromeDevToolsAvailable: () => checkChromeDevToolsAvailable,
31
+ checkCliInstalled: () => checkCliInstalled,
31
32
  createPackageRequire: () => createPackageRequire,
32
33
  findAvailablePort: () => findAvailablePort,
34
+ findGitRoot: () => findGitRoot,
35
+ getCliVersion: () => getCliVersion,
33
36
  isPortAvailable: () => isPortAvailable,
34
- resolvePackageDir: () => resolvePackageDir
37
+ killOrphanCliProcesses: () => killOrphanCliProcesses,
38
+ resolvePackageDir: () => resolvePackageDir,
39
+ waitForServer: () => waitForServer
35
40
  });
36
41
  module.exports = __toCommonJS(node_utils_exports);
42
+ var import_node_child_process = require("node:child_process");
37
43
  var import_node_module = require("node:module");
44
+ var import_node_fs = __toESM(require("node:fs"));
45
+ var import_node_http = __toESM(require("node:http"));
38
46
  var import_node_path = __toESM(require("node:path"));
39
47
  var import_constants = require("../common/constants.cjs");
48
+ var import_node_logger = require("./node-logger.cjs");
49
+ const log = (0, import_node_logger.createLogger)("NodeUtils");
40
50
  function createPackageRequire(baseDir = process.cwd()) {
41
51
  return (0, import_node_module.createRequire)(import_node_path.default.join(baseDir, "package.json"));
42
52
  }
@@ -83,11 +93,237 @@ async function findAvailablePort(startPort, hostname, maxTries = 100) {
83
93
  }
84
94
  throw new Error(`No available port in range ${startPort}-${startPort + maxTries}`);
85
95
  }
96
+ function waitForServer(url, timeout = 1e4, proc) {
97
+ const timer = new import_node_logger.PerformanceTimer("waitForServer", { url, timeout });
98
+ return new Promise((resolve, reject) => {
99
+ const startTime = Date.now();
100
+ let attempts = 0;
101
+ const check = () => {
102
+ attempts++;
103
+ log.debug(`Checking server availability (attempt ${attempts})`, { url });
104
+ if (proc?.exitCode !== null && proc?.exitCode !== void 0) {
105
+ timer.end(`\u2716 Process exited with code ${proc.exitCode}`);
106
+ reject(new Error(`Process exited with code ${proc.exitCode}`));
107
+ return;
108
+ }
109
+ const req = import_node_http.default.get(url, (res) => {
110
+ if (res.statusCode && res.statusCode < 500) {
111
+ timer.end(`\u2713 Server ready after ${attempts} attempts`);
112
+ resolve();
113
+ } else {
114
+ log.debug(`Server returned status ${res.statusCode}, retrying...`);
115
+ retryOrReject();
116
+ }
117
+ });
118
+ req.on("error", (err) => {
119
+ log.debug(`Server check failed: ${err.message}`);
120
+ retryOrReject();
121
+ });
122
+ };
123
+ const retryOrReject = () => {
124
+ const elapsed = Date.now() - startTime;
125
+ if (elapsed < timeout) {
126
+ setTimeout(check, import_constants.SERVER_CHECK_INTERVAL);
127
+ } else {
128
+ timer.end("\u2716 Timeout");
129
+ reject(new Error(`Server not ready after ${timeout}ms (${attempts} attempts)`));
130
+ }
131
+ };
132
+ check();
133
+ });
134
+ }
135
+ function findGitRoot(startDir, maxDepth = 10) {
136
+ const timer = new import_node_logger.PerformanceTimer("findGitRoot", { startDir, maxDepth });
137
+ let currentDir = startDir;
138
+ let depth = 0;
139
+ while (depth < maxDepth) {
140
+ const gitDir = import_node_path.default.join(currentDir, ".git");
141
+ try {
142
+ if (import_node_fs.default.existsSync(gitDir)) {
143
+ timer.end(`\u2713 Found git root at depth ${depth}: ${currentDir}`);
144
+ return currentDir;
145
+ }
146
+ } catch (err) {
147
+ log.debug(`Error checking .git directory at ${currentDir}`, {
148
+ error: err.message
149
+ });
150
+ }
151
+ const parentDir = import_node_path.default.dirname(currentDir);
152
+ if (parentDir === currentDir) {
153
+ log.debug("Reached filesystem root");
154
+ break;
155
+ }
156
+ currentDir = parentDir;
157
+ depth++;
158
+ }
159
+ timer.end(`\u2716 No git root found after ${depth} levels, using start directory`);
160
+ return startDir;
161
+ }
162
+ async function checkCliInstalled(bin) {
163
+ const timer = new import_node_logger.PerformanceTimer(`checkCliInstalled:${bin}`);
164
+ return new Promise((resolve) => {
165
+ const proc = (0, import_node_child_process.spawn)(bin, ["--version"], { stdio: "ignore", shell: true });
166
+ proc.on("close", (code) => {
167
+ const installed = code === 0;
168
+ timer.end(installed ? `\u2713 ${bin} is installed` : `\u2716 ${bin} not found`);
169
+ resolve(installed);
170
+ });
171
+ proc.on("error", (err) => {
172
+ log.debug(`Failed to check ${bin} installation`, { error: err.message });
173
+ timer.end("\u2716 Check failed");
174
+ resolve(false);
175
+ });
176
+ });
177
+ }
178
+ function getCliVersion(bin) {
179
+ return new Promise((resolve) => {
180
+ const proc = (0, import_node_child_process.spawn)(bin, ["--version"], { stdio: "pipe", shell: true });
181
+ let output = "";
182
+ proc.stdout?.on("data", (data) => {
183
+ output += data.toString();
184
+ });
185
+ proc.on("close", (code) => {
186
+ resolve(code === 0 && output.trim() ? output.trim() : null);
187
+ });
188
+ proc.on("error", () => resolve(null));
189
+ });
190
+ }
191
+ function killOrphanCliProcesses(bin, options) {
192
+ const label = options.label ?? bin;
193
+ const timeoutMs = options.timeout ?? 5e3;
194
+ const timer = new import_node_logger.PerformanceTimer(`killOrphanCliProcesses:${label}`);
195
+ log.debug(`Looking for orphan ${label} processes (PPID=1)`);
196
+ return new Promise((resolve) => {
197
+ let settled = false;
198
+ const done = (count) => {
199
+ if (!settled) {
200
+ settled = true;
201
+ resolve(count);
202
+ }
203
+ };
204
+ const timeout = setTimeout(() => {
205
+ log.warn(`Kill orphan ${label} processes timed out, skipping`);
206
+ timer.end("\u26A0 Timeout, skipped");
207
+ done(0);
208
+ }, timeoutMs);
209
+ const wrappedResolve = (count) => {
210
+ clearTimeout(timeout);
211
+ done(count);
212
+ };
213
+ if (process.platform === "win32") {
214
+ killOrphansOnWindows(wrappedResolve, options, label, timer);
215
+ } else {
216
+ killOrphansOnUnix(wrappedResolve, options, label, timer);
217
+ }
218
+ });
219
+ }
220
+ function killOrphansOnWindows(resolve, options, label, timer) {
221
+ log.debug(`Using Windows method to find orphan ${label} processes`);
222
+ const proc = (0, import_node_child_process.spawn)(
223
+ "wmic",
224
+ ["process", "where", `name="${options.winName}"`, "get", "processid,parentprocessid,commandline"],
225
+ { stdio: "pipe" }
226
+ );
227
+ let output = "";
228
+ proc.stdout?.on("data", (data) => {
229
+ output += data.toString();
230
+ });
231
+ proc.on("close", () => {
232
+ const pidsToKill = [];
233
+ output.split("\n").forEach((rawLine) => {
234
+ const line = rawLine.trim();
235
+ if (!line.includes(options.match)) return;
236
+ const parts = line.split(/\s+/);
237
+ if (parts.length >= 3) {
238
+ const ppid = parts[0];
239
+ const pid = parts[1];
240
+ if (ppid === "1" && pid && !Number.isNaN(Number(pid))) pidsToKill.push(pid);
241
+ }
242
+ });
243
+ finishOrphanKillWindows(pidsToKill, resolve, label, timer);
244
+ });
245
+ proc.on("error", (err) => {
246
+ log.debug(`Failed to find orphan ${label} processes`, { error: err.message });
247
+ timer.end("\u2716 Failed to find orphan processes");
248
+ resolve(0);
249
+ });
250
+ }
251
+ function finishOrphanKillWindows(pidsToKill, resolve, label, timer) {
252
+ if (pidsToKill.length === 0) {
253
+ log.debug("No orphan processes found");
254
+ timer.end("No orphan processes found");
255
+ resolve(0);
256
+ return;
257
+ }
258
+ log.debug(`Found ${pidsToKill.length} orphan processes`, { pids: pidsToKill });
259
+ let killedCount = 0;
260
+ let completedCount = 0;
261
+ pidsToKill.forEach((pid) => {
262
+ const killProc = (0, import_node_child_process.spawn)("taskkill", ["/F", "/PID", pid], { stdio: "ignore" });
263
+ killProc.on("close", (code) => {
264
+ completedCount++;
265
+ if (code === 0) killedCount++;
266
+ if (completedCount === pidsToKill.length) {
267
+ timer.end(`\u2713 Killed ${killedCount} orphan ${label} processes`);
268
+ resolve(killedCount);
269
+ }
270
+ });
271
+ });
272
+ }
273
+ function killOrphansOnUnix(resolve, options, label, timer) {
274
+ log.debug(`Using Unix method to find orphan ${label} processes`);
275
+ const proc = (0, import_node_child_process.spawn)("ps", ["-e", "-o", "pid,ppid,args"], { stdio: "pipe" });
276
+ let output = "";
277
+ proc.stdout?.on("data", (data) => {
278
+ output += data.toString();
279
+ });
280
+ proc.on("close", () => {
281
+ const pidsToKill = [];
282
+ output.split("\n").forEach((line) => {
283
+ if (!line.includes(options.match)) return;
284
+ const parts = line.trim().split(/\s+/);
285
+ if (parts.length >= 3) {
286
+ const pid = parts[0];
287
+ const ppid = parts[1];
288
+ if (ppid === "1" && pid && !Number.isNaN(Number(pid))) pidsToKill.push(pid);
289
+ }
290
+ });
291
+ if (pidsToKill.length === 0) {
292
+ log.debug("No orphan processes found");
293
+ timer.end("No orphan processes found");
294
+ resolve(0);
295
+ return;
296
+ }
297
+ log.debug(`Found ${pidsToKill.length} orphan processes`, { pids: pidsToKill });
298
+ const killProc = (0, import_node_child_process.spawn)("kill", ["-9", ...pidsToKill], { stdio: "ignore" });
299
+ killProc.on("close", (code) => {
300
+ const killedCount = code === 0 ? pidsToKill.length : 0;
301
+ timer.end(
302
+ killedCount > 0 ? `\u2713 Killed ${killedCount} orphan ${label} processes` : "\u2716 Failed to kill processes"
303
+ );
304
+ resolve(killedCount);
305
+ });
306
+ killProc.on("error", () => {
307
+ timer.end("\u2716 Failed to kill processes");
308
+ resolve(0);
309
+ });
310
+ });
311
+ proc.on("error", (err) => {
312
+ log.debug(`Failed to find orphan ${label} processes`, { error: err.message });
313
+ timer.end("\u2716 Failed to find orphan processes");
314
+ resolve(0);
315
+ });
316
+ }
86
317
  // Annotate the CommonJS export names for ESM import in node:
87
318
  0 && (module.exports = {
88
319
  checkChromeDevToolsAvailable,
320
+ checkCliInstalled,
89
321
  createPackageRequire,
90
322
  findAvailablePort,
323
+ findGitRoot,
324
+ getCliVersion,
91
325
  isPortAvailable,
92
- resolvePackageDir
326
+ killOrphanCliProcesses,
327
+ resolvePackageDir,
328
+ waitForServer
93
329
  });
@@ -26,3 +26,44 @@ export declare function isPortAvailable(port: number, hostname?: string): Promis
26
26
  * 从 startPort 开始寻找可用端口
27
27
  */
28
28
  export declare function findAvailablePort(startPort: number, hostname?: string, maxTries?: number): Promise<number>;
29
+ /**
30
+ * 可等待的进程接口(ResultPromise 等匹配就够)
31
+ */
32
+ export interface WaitableProcess {
33
+ exitCode: number | null | undefined;
34
+ }
35
+ /**
36
+ * 轮询等待服务准备就绪(HTTP 状态码 < 500 即当作 ready);超时或进程退出时 reject
37
+ * @param url - 检查的服务 URL
38
+ * @param timeout - 超时毫秒数
39
+ * @param proc - 可选进程,提前退出时直接失败
40
+ */
41
+ export declare function waitForServer(url: string, timeout?: number, proc?: WaitableProcess): Promise<void>;
42
+ /**
43
+ * 从指定目录往上查找 .git 根目录(找不到时返回起始目录)
44
+ */
45
+ export declare function findGitRoot(startDir: string, maxDepth?: number): string;
46
+ /**
47
+ * 检查某 CLI 是否安装(运行 <bin> --version,退出码 0 即存在)
48
+ */
49
+ export declare function checkCliInstalled(bin: string): Promise<boolean>;
50
+ /**
51
+ * 获取某 CLI 版本号(<bin> --version 的第一行,败路返回 null)
52
+ */
53
+ export declare function getCliVersion(bin: string): Promise<string | null>;
54
+ /** 孤儿进程清理配置(提供商的 check/kill 统一实现) */
55
+ export interface KillOrphanCliOptions {
56
+ /** 进程命令行/参数中需匹配的子串(如 opencode / dsh) */
57
+ match: string;
58
+ /** Windows wmic 按名称查询的进程名(如 opencode.exe / node.exe) */
59
+ winName: string;
60
+ /** 超时毫秒(默认 5000) */
61
+ timeout?: number;
62
+ /** 运行名(仅用于日志,默认等于 match) */
63
+ label?: string;
64
+ }
65
+ /**
66
+ * 清理被 reparent 到 init(PPID=1)的孤儿进程(win: wmic+taskkill;unix: ps+kill)
67
+ * @returns 被成功结束的进程数
68
+ */
69
+ export declare function killOrphanCliProcesses(bin: string, options: KillOrphanCliOptions): Promise<number>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aipanel/core",
3
- "version": "1.2.9",
3
+ "version": "1.2.11",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "main": "lib/index.cjs",