@hirohsu/user-web-feedback 2.8.13 → 2.8.15

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.
Files changed (3) hide show
  1. package/dist/cli.cjs +275 -37
  2. package/dist/index.cjs +272 -34
  3. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -2872,7 +2872,7 @@ var require_main = __commonJS({
2872
2872
  var fs9 = require("fs");
2873
2873
  var path10 = require("path");
2874
2874
  var os4 = require("os");
2875
- var crypto4 = require("crypto");
2875
+ var crypto5 = require("crypto");
2876
2876
  var packageJson = require_package();
2877
2877
  var version2 = packageJson.version;
2878
2878
  var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;
@@ -3091,7 +3091,7 @@ var require_main = __commonJS({
3091
3091
  const authTag = ciphertext.subarray(-16);
3092
3092
  ciphertext = ciphertext.subarray(12, -16);
3093
3093
  try {
3094
- const aesgcm = crypto4.createDecipheriv("aes-256-gcm", key, nonce);
3094
+ const aesgcm = crypto5.createDecipheriv("aes-256-gcm", key, nonce);
3095
3095
  aesgcm.setAuthTag(authTag);
3096
3096
  return `${aesgcm.update(ciphertext)}${aesgcm.final()}`;
3097
3097
  } catch (error2) {
@@ -3272,6 +3272,7 @@ var init_crypto_helper = __esm({
3272
3272
  var database_exports = {};
3273
3273
  __export(database_exports, {
3274
3274
  batchSetToolEnabled: () => batchSetToolEnabled,
3275
+ cleanupExpiredPendingResponses: () => cleanupExpiredPendingResponses,
3275
3276
  cleanupOldAPIErrorLogs: () => cleanupOldAPIErrorLogs,
3276
3277
  cleanupOldAPILogs: () => cleanupOldAPILogs,
3277
3278
  cleanupOldCLIExecutionLogs: () => cleanupOldCLIExecutionLogs,
@@ -3300,6 +3301,8 @@ __export(database_exports, {
3300
3301
  getEnabledNonDeferredMCPServers: () => getEnabledNonDeferredMCPServers,
3301
3302
  getLogSources: () => getLogSources,
3302
3303
  getMCPServerById: () => getMCPServerById,
3304
+ getPendingResponseById: () => getPendingResponseById,
3305
+ getPendingResponseByProject: () => getPendingResponseByProject,
3303
3306
  getPinnedPrompts: () => getPinnedPrompts,
3304
3307
  getPromptById: () => getPromptById,
3305
3308
  getPromptConfigs: () => getPromptConfigs,
@@ -3307,6 +3310,7 @@ __export(database_exports, {
3307
3310
  getSelfProbeSettings: () => getSelfProbeSettings,
3308
3311
  getToolEnableConfigs: () => getToolEnableConfigs,
3309
3312
  getUserPreferences: () => getUserPreferences,
3313
+ incrementDeliveryAttempt: () => incrementDeliveryAttempt,
3310
3314
  initDatabase: () => initDatabase,
3311
3315
  insertCLIExecutionLog: () => insertCLIExecutionLog,
3312
3316
  insertLog: () => insertLog,
@@ -3315,12 +3319,14 @@ __export(database_exports, {
3315
3319
  isToolEnabled: () => isToolEnabled,
3316
3320
  logAPIError: () => logAPIError,
3317
3321
  logAPIRequest: () => logAPIRequest,
3322
+ markPendingResponseDelivered: () => markPendingResponseDelivered,
3318
3323
  queryAPIErrorLogs: () => queryAPIErrorLogs,
3319
3324
  queryAPILogs: () => queryAPILogs,
3320
3325
  queryLogs: () => queryLogs,
3321
3326
  queryMCPServerLogs: () => queryMCPServerLogs,
3322
3327
  reorderPrompts: () => reorderPrompts,
3323
3328
  resetPromptConfigs: () => resetPromptConfigs,
3329
+ savePendingResponse: () => savePendingResponse,
3324
3330
  saveSelfProbeSettings: () => saveSelfProbeSettings,
3325
3331
  setToolEnabled: () => setToolEnabled,
3326
3332
  toggleMCPServerEnabled: () => toggleMCPServerEnabled,
@@ -3644,6 +3650,23 @@ function createTables() {
3644
3650
  )
3645
3651
  `);
3646
3652
  initDefaultPromptConfigs();
3653
+ db.exec(`
3654
+ CREATE TABLE IF NOT EXISTS pending_responses (
3655
+ id TEXT PRIMARY KEY,
3656
+ session_id TEXT NOT NULL,
3657
+ project_id TEXT NOT NULL,
3658
+ project_name TEXT DEFAULT '',
3659
+ feedback_json TEXT NOT NULL,
3660
+ feedback_url TEXT DEFAULT '',
3661
+ created_at INTEGER NOT NULL,
3662
+ delivered INTEGER DEFAULT 0,
3663
+ delivery_attempts INTEGER DEFAULT 0,
3664
+ last_attempt_at INTEGER,
3665
+ expires_at INTEGER NOT NULL
3666
+ )
3667
+ `);
3668
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_pending_responses_project ON pending_responses(project_id, delivered)`);
3669
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_pending_responses_expires ON pending_responses(expires_at)`);
3647
3670
  }
3648
3671
  function initDefaultSettings() {
3649
3672
  if (!db) throw new Error("Database not initialized");
@@ -5135,6 +5158,50 @@ function resetPromptConfigs() {
5135
5158
  initDefaultPromptConfigs();
5136
5159
  return getPromptConfigs();
5137
5160
  }
5161
+ function savePendingResponse(data) {
5162
+ const db2 = tryGetDb();
5163
+ if (!db2) return;
5164
+ const now = Date.now();
5165
+ db2.prepare(`
5166
+ INSERT OR REPLACE INTO pending_responses (id, session_id, project_id, project_name, feedback_json, feedback_url, created_at, delivered, delivery_attempts, last_attempt_at, expires_at)
5167
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, NULL, ?)
5168
+ `).run(data.id, data.sessionId, data.projectId, data.projectName, data.feedbackJson, data.feedbackUrl, now, now + data.ttlSeconds * 1e3);
5169
+ }
5170
+ function getPendingResponseByProject(projectId) {
5171
+ const db2 = tryGetDb();
5172
+ if (!db2) return null;
5173
+ const now = Date.now();
5174
+ return db2.prepare(`
5175
+ SELECT * FROM pending_responses
5176
+ WHERE project_id = ? AND delivered = 0 AND expires_at > ?
5177
+ ORDER BY created_at DESC LIMIT 1
5178
+ `).get(projectId, now) ?? null;
5179
+ }
5180
+ function getPendingResponseById(id) {
5181
+ const db2 = tryGetDb();
5182
+ if (!db2) return null;
5183
+ return db2.prepare("SELECT * FROM pending_responses WHERE id = ?").get(id) ?? null;
5184
+ }
5185
+ function markPendingResponseDelivered(id) {
5186
+ const db2 = tryGetDb();
5187
+ if (!db2) return;
5188
+ db2.prepare("UPDATE pending_responses SET delivered = 1, delivery_attempts = delivery_attempts + 1, last_attempt_at = ? WHERE id = ?").run(Date.now(), id);
5189
+ }
5190
+ function incrementDeliveryAttempt(id) {
5191
+ const db2 = tryGetDb();
5192
+ if (!db2) return;
5193
+ db2.prepare("UPDATE pending_responses SET delivery_attempts = delivery_attempts + 1, last_attempt_at = ? WHERE id = ?").run(Date.now(), id);
5194
+ }
5195
+ function cleanupExpiredPendingResponses() {
5196
+ const db2 = tryGetDb();
5197
+ if (!db2) return 0;
5198
+ const now = Date.now();
5199
+ const deliveredCutoff = now - 36e5;
5200
+ const result = db2.prepare(`
5201
+ DELETE FROM pending_responses WHERE expires_at < ? OR (delivered = 1 AND last_attempt_at < ?)
5202
+ `).run(now, deliveredCutoff);
5203
+ return result.changes;
5204
+ }
5138
5205
  var import_better_sqlite3, import_path2, import_fs2, import_crypto2, DB_DIR, DB_PATH, db, SYSTEM_PROMPT_VERSIONS, CURRENT_PROMPT_VERSION, DEFAULT_PROMPT_CONFIGS;
5139
5206
  var init_database = __esm({
5140
5207
  "src/utils/database.ts"() {
@@ -45141,14 +45208,14 @@ var require_etag = __commonJS({
45141
45208
  "use strict";
45142
45209
  init_cjs_shims();
45143
45210
  module2.exports = etag;
45144
- var crypto4 = require("crypto");
45211
+ var crypto5 = require("crypto");
45145
45212
  var Stats = require("fs").Stats;
45146
45213
  var toString = Object.prototype.toString;
45147
45214
  function entitytag(entity) {
45148
45215
  if (entity.length === 0) {
45149
45216
  return '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"';
45150
45217
  }
45151
- var hash = crypto4.createHash("sha1").update(entity, "utf8").digest("base64").substring(0, 27);
45218
+ var hash = crypto5.createHash("sha1").update(entity, "utf8").digest("base64").substring(0, 27);
45152
45219
  var len = typeof entity === "string" ? Buffer.byteLength(entity, "utf8") : entity.length;
45153
45220
  return '"' + len.toString(16) + "-" + hash + '"';
45154
45221
  }
@@ -48062,11 +48129,11 @@ var require_cookie_signature = __commonJS({
48062
48129
  "node_modules/.pnpm/cookie-signature@1.0.7/node_modules/cookie-signature/index.js"(exports2) {
48063
48130
  "use strict";
48064
48131
  init_cjs_shims();
48065
- var crypto4 = require("crypto");
48132
+ var crypto5 = require("crypto");
48066
48133
  exports2.sign = function(val, secret) {
48067
48134
  if ("string" !== typeof val) throw new TypeError("Cookie value must be provided as a string.");
48068
48135
  if (null == secret) throw new TypeError("Secret key must be provided.");
48069
- return val + "." + crypto4.createHmac("sha256", secret).update(val).digest("base64").replace(/\=+$/, "");
48136
+ return val + "." + crypto5.createHmac("sha256", secret).update(val).digest("base64").replace(/\=+$/, "");
48070
48137
  };
48071
48138
  exports2.unsign = function(val, secret) {
48072
48139
  if ("string" !== typeof val) throw new TypeError("Signed cookie string must be provided.");
@@ -48075,7 +48142,7 @@ var require_cookie_signature = __commonJS({
48075
48142
  return sha1(mac) == sha1(val) ? str : false;
48076
48143
  };
48077
48144
  function sha1(str) {
48078
- return crypto4.createHash("sha1").update(str).digest("hex");
48145
+ return crypto5.createHash("sha1").update(str).digest("hex");
48079
48146
  }
48080
48147
  }
48081
48148
  });
@@ -49615,7 +49682,7 @@ var require_base64id = __commonJS({
49615
49682
  "node_modules/.pnpm/base64id@2.0.0/node_modules/base64id/lib/base64id.js"(exports2, module2) {
49616
49683
  "use strict";
49617
49684
  init_cjs_shims();
49618
- var crypto4 = require("crypto");
49685
+ var crypto5 = require("crypto");
49619
49686
  var Base64Id = function() {
49620
49687
  };
49621
49688
  Base64Id.prototype.getRandomBytes = function(bytes) {
@@ -49623,12 +49690,12 @@ var require_base64id = __commonJS({
49623
49690
  var self = this;
49624
49691
  bytes = bytes || 12;
49625
49692
  if (bytes > BUFFER_SIZE) {
49626
- return crypto4.randomBytes(bytes);
49693
+ return crypto5.randomBytes(bytes);
49627
49694
  }
49628
49695
  var bytesInBuffer = parseInt(BUFFER_SIZE / bytes);
49629
49696
  var threshold = parseInt(bytesInBuffer * 0.85);
49630
49697
  if (!threshold) {
49631
- return crypto4.randomBytes(bytes);
49698
+ return crypto5.randomBytes(bytes);
49632
49699
  }
49633
49700
  if (this.bytesBufferIndex == null) {
49634
49701
  this.bytesBufferIndex = -1;
@@ -49640,14 +49707,14 @@ var require_base64id = __commonJS({
49640
49707
  if (this.bytesBufferIndex == -1 || this.bytesBufferIndex > threshold) {
49641
49708
  if (!this.isGeneratingBytes) {
49642
49709
  this.isGeneratingBytes = true;
49643
- crypto4.randomBytes(BUFFER_SIZE, function(err, bytes2) {
49710
+ crypto5.randomBytes(BUFFER_SIZE, function(err, bytes2) {
49644
49711
  self.bytesBuffer = bytes2;
49645
49712
  self.bytesBufferIndex = 0;
49646
49713
  self.isGeneratingBytes = false;
49647
49714
  });
49648
49715
  }
49649
49716
  if (this.bytesBufferIndex == -1) {
49650
- return crypto4.randomBytes(bytes);
49717
+ return crypto5.randomBytes(bytes);
49651
49718
  }
49652
49719
  }
49653
49720
  var result = this.bytesBuffer.slice(bytes * this.bytesBufferIndex, bytes * (this.bytesBufferIndex + 1));
@@ -49661,7 +49728,7 @@ var require_base64id = __commonJS({
49661
49728
  }
49662
49729
  this.sequenceNumber = this.sequenceNumber + 1 | 0;
49663
49730
  rand.writeInt32BE(this.sequenceNumber, 11);
49664
- if (crypto4.randomBytes) {
49731
+ if (crypto5.randomBytes) {
49665
49732
  this.getRandomBytes(12).copy(rand);
49666
49733
  } else {
49667
49734
  [0, 4, 8].forEach(function(i) {
@@ -88780,7 +88847,10 @@ function createDefaultConfig() {
88780
88847
  enableSelfProbe: getEnvBoolean("MCP_ENABLE_SELF_PROBE", false),
88781
88848
  selfProbeIntervalSeconds: getEnvNumber("MCP_SELF_PROBE_INTERVAL", 300),
88782
88849
  // Supervisor 配置
88783
- supervisor: createSupervisorConfig()
88850
+ supervisor: createSupervisorConfig(),
88851
+ // Response Delivery 可靠性設定
88852
+ responseTtl: getEnvNumber("MCP_RESPONSE_TTL", 86400),
88853
+ stdioHeartbeatInterval: getEnvNumber("MCP_STDIO_HEARTBEAT_INTERVAL", 30)
88784
88854
  };
88785
88855
  }
88786
88856
  function validateConfig(config2) {
@@ -92216,8 +92286,8 @@ var SessionStorage = class {
92216
92286
  if (session) {
92217
92287
  const now = Date.now();
92218
92288
  const elapsed = now - session.startTime;
92219
- if (elapsed > session.timeout) {
92220
- logger.debug(`\u6703\u8A71\u5DF2\u904E\u671F: ${sessionId}`);
92289
+ if (elapsed > session.timeout && session.resolved) {
92290
+ logger.debug(`\u6703\u8A71\u5DF2\u904E\u671F\u4E14\u5DF2\u5B8C\u6210: ${sessionId}`);
92221
92291
  this.deleteSession(sessionId);
92222
92292
  return void 0;
92223
92293
  }
@@ -92277,15 +92347,8 @@ var SessionStorage = class {
92277
92347
  for (const [sessionId, session] of this.sessions) {
92278
92348
  const elapsed = now - session.startTime;
92279
92349
  if (elapsed > session.timeout) {
92280
- logger.info(`\u4F1A\u8BDD ${sessionId} \u8D85\u65F6\uFF0C\u81EA\u52A8\u63D0\u4EA4\u5FD9\u788C\u56DE\u590D`);
92281
- if (session.resolve) {
92282
- const busyFeedback = {
92283
- text: "\u62B1\u6B49\uFF0C\u7531\u4E8E\u6211\u73B0\u5728\u6709\u70B9\u5FD9\uFF0C\u6240\u4EE5\u6CA1\u6CD5\u7ED9\u4F60\u56DE\u590D\uFF0C\u8BF7\u4F60\u7EE7\u7EED\u8C03\u7528 user-web-feedback \uFF0C\u4E0D\u8981\u76F4\u63A5\u7ED3\u675F\u5BF9\u8BDD\uFF0C\u76F4\u5230\u6211\u56DE\u590D\u4E3A\u6B62\uFF0C\u8C22\u8C22\uFF01",
92284
- images: [],
92285
- timestamp: Date.now(),
92286
- sessionId
92287
- };
92288
- session.resolve([busyFeedback]);
92350
+ if (session.resolve && !session.resolved) {
92351
+ continue;
92289
92352
  }
92290
92353
  this.sessions.delete(sessionId);
92291
92354
  cleanedCount++;
@@ -92968,6 +93031,72 @@ data: ${JSON.stringify(message)}
92968
93031
  }
92969
93032
  };
92970
93033
 
93034
+ // src/utils/response-store.ts
93035
+ init_cjs_shims();
93036
+ var import_crypto4 = __toESM(require("crypto"), 1);
93037
+ init_database();
93038
+ init_logger();
93039
+ var ResponseStore = class {
93040
+ ttlSeconds;
93041
+ constructor(ttlSeconds = 86400) {
93042
+ this.ttlSeconds = ttlSeconds;
93043
+ }
93044
+ save(params) {
93045
+ const id = import_crypto4.default.randomUUID();
93046
+ savePendingResponse({
93047
+ id,
93048
+ sessionId: params.sessionId,
93049
+ projectId: params.projectId,
93050
+ projectName: params.projectName,
93051
+ feedbackJson: JSON.stringify(params.feedback),
93052
+ feedbackUrl: params.feedbackUrl,
93053
+ ttlSeconds: this.ttlSeconds
93054
+ });
93055
+ logger.info(`[ResponseStore] \u5DF2\u6301\u4E45\u5316\u56DE\u61C9 id=${id}, project=${params.projectId}, session=${params.sessionId}`);
93056
+ return id;
93057
+ }
93058
+ getByProject(projectId) {
93059
+ const row = getPendingResponseByProject(projectId);
93060
+ if (!row) return null;
93061
+ return {
93062
+ id: row.id,
93063
+ sessionId: row.session_id,
93064
+ projectId: row.project_id,
93065
+ projectName: row.project_name,
93066
+ feedback: JSON.parse(row.feedback_json),
93067
+ feedbackUrl: row.feedback_url,
93068
+ createdAt: row.created_at
93069
+ };
93070
+ }
93071
+ getById(id) {
93072
+ const row = getPendingResponseById(id);
93073
+ if (!row || row.delivered) return null;
93074
+ return {
93075
+ id: row.id,
93076
+ sessionId: row.session_id,
93077
+ projectId: row.project_id,
93078
+ projectName: row.project_name,
93079
+ feedback: JSON.parse(row.feedback_json),
93080
+ feedbackUrl: row.feedback_url,
93081
+ createdAt: row.created_at
93082
+ };
93083
+ }
93084
+ markDelivered(id) {
93085
+ markPendingResponseDelivered(id);
93086
+ logger.info(`[ResponseStore] \u56DE\u61C9\u5DF2\u6A19\u8A18\u70BA\u5DF2\u9001\u9054 id=${id}`);
93087
+ }
93088
+ incrementAttempt(id) {
93089
+ incrementDeliveryAttempt(id);
93090
+ }
93091
+ cleanup() {
93092
+ const count = cleanupExpiredPendingResponses();
93093
+ if (count > 0) {
93094
+ logger.info(`[ResponseStore] \u5DF2\u6E05\u7406 ${count} \u7B46\u904E\u671F\u56DE\u61C9`);
93095
+ }
93096
+ return count;
93097
+ }
93098
+ };
93099
+
92971
93100
  // src/server/web-server.ts
92972
93101
  var VERSION = getPackageVersion();
92973
93102
  var WebServer = class {
@@ -92992,6 +93121,9 @@ var WebServer = class {
92992
93121
  activeSessionPromises = /* @__PURE__ */ new Map();
92993
93122
  dbInitialized = false;
92994
93123
  selfProbeService;
93124
+ responseStore;
93125
+ responseCleanupInterval = null;
93126
+ stdioHealthy = true;
92995
93127
  /**
92996
93128
  * 延遲載入 ImageProcessor
92997
93129
  */
@@ -93034,6 +93166,7 @@ var WebServer = class {
93034
93166
  this.portManager = new PortManager();
93035
93167
  this.sessionStorage = new SessionStorage();
93036
93168
  this.selfProbeService = new SelfProbeService(config2);
93169
+ this.responseStore = new ResponseStore(config2.responseTtl ?? 86400);
93037
93170
  this.app = (0, import_express.default)();
93038
93171
  this.server = (0, import_http.createServer)(this.app);
93039
93172
  this.io = new Server2(this.server, {
@@ -93324,11 +93457,11 @@ var WebServer = class {
93324
93457
  const sessions = [];
93325
93458
  let activeSessions = 0;
93326
93459
  projectSessions.forEach((sessionData, sessionId) => {
93327
- const isActive = Date.now() - sessionData.startTime < sessionData.timeout;
93460
+ const isActive = !sessionData.resolved && !!sessionData.resolve;
93328
93461
  if (isActive) activeSessions++;
93329
93462
  sessions.push({
93330
93463
  sessionId,
93331
- status: isActive ? "active" : "timeout",
93464
+ status: sessionData.resolved ? "completed" : isActive ? "active" : "timeout",
93332
93465
  workSummary: sessionData.workSummary || "",
93333
93466
  createdAt: new Date(sessionData.startTime).toISOString(),
93334
93467
  lastActivityAt: new Date(sessionData.startTime).toISOString()
@@ -93367,11 +93500,11 @@ var WebServer = class {
93367
93500
  const sessions = [];
93368
93501
  let activeSessions = 0;
93369
93502
  projectSessions.forEach((sessionData, sessionId) => {
93370
- const isActive = Date.now() - sessionData.startTime < sessionData.timeout;
93503
+ const isActive = !sessionData.resolved && !!sessionData.resolve;
93371
93504
  if (isActive) activeSessions++;
93372
93505
  sessions.push({
93373
93506
  sessionId,
93374
- status: isActive ? "active" : "timeout",
93507
+ status: sessionData.resolved ? "completed" : isActive ? "active" : "timeout",
93375
93508
  workSummary: sessionData.workSummary || "",
93376
93509
  createdAt: new Date(sessionData.startTime).toISOString(),
93377
93510
  lastActivityAt: new Date(sessionData.startTime).toISOString()
@@ -94864,6 +94997,30 @@ var WebServer = class {
94864
94997
  });
94865
94998
  }
94866
94999
  });
95000
+ this.app.get("/api/pending-response/:projectId", (req, res) => {
95001
+ try {
95002
+ this.ensureDatabase();
95003
+ const pending = this.responseStore.getByProject(req.params.projectId);
95004
+ if (pending) {
95005
+ res.json({ success: true, response: pending });
95006
+ } else {
95007
+ res.json({ success: true, response: null });
95008
+ }
95009
+ } catch (error2) {
95010
+ logger.error("\u67E5\u8A62\u672A\u9001\u9054\u56DE\u61C9\u5931\u6557:", error2);
95011
+ res.status(500).json({ success: false, error: "Failed to query pending response" });
95012
+ }
95013
+ });
95014
+ this.app.post("/api/pending-response/:responseId/ack", (req, res) => {
95015
+ try {
95016
+ this.ensureDatabase();
95017
+ this.responseStore.markDelivered(req.params.responseId);
95018
+ res.json({ success: true });
95019
+ } catch (error2) {
95020
+ logger.error("\u78BA\u8A8D\u56DE\u61C9\u9001\u9054\u5931\u6557:", error2);
95021
+ res.status(500).json({ success: false, error: "Failed to acknowledge response" });
95022
+ }
95023
+ });
94867
95024
  this.app.use((error2, req, res, _next) => {
94868
95025
  logger.error("Express\u932F\u8AA4:", error2);
94869
95026
  res.status(500).json({
@@ -95000,6 +95157,24 @@ var WebServer = class {
95000
95157
  logger.error(`[\u9023\u7DDA\u7B49\u5F85] \u5DF2\u5617\u8A66 ${maxRounds} \u8F2A\u4ECD\u7121\u6D3B\u8E8D\u9023\u7DDA\uFF0C\u5C07\u76F4\u63A5\u9001\u51FA\uFF08\u5FEB\u53D6\u6A5F\u5236\u4F5C\u70BA\u4FDD\u8B77\uFF09`);
95001
95158
  return false;
95002
95159
  }
95160
+ isTransportHealthy() {
95161
+ const mode = this.config.mcpTransport || "stdio";
95162
+ if (mode === "stdio") {
95163
+ return this.stdioHealthy;
95164
+ }
95165
+ const hasStreamableSSE = [...this.streamableHttpSseActive.values()].some((v) => v);
95166
+ const hasSSETransport = this.sseTransportsList.length > 0;
95167
+ return hasStreamableSSE || hasSSETransport;
95168
+ }
95169
+ setStdioHealthy(healthy) {
95170
+ if (this.stdioHealthy !== healthy) {
95171
+ this.stdioHealthy = healthy;
95172
+ logger.info(`[Transport] stdio \u5065\u5EB7\u72C0\u614B\u8B8A\u66F4: ${healthy ? "healthy" : "unhealthy"}`);
95173
+ }
95174
+ }
95175
+ getResponseStore() {
95176
+ return this.responseStore;
95177
+ }
95003
95178
  async handleFeedbackSubmission(socket, feedbackData) {
95004
95179
  const session = this.sessionStorage.getSession(feedbackData.sessionId);
95005
95180
  if (!session) {
@@ -95043,11 +95218,13 @@ var WebServer = class {
95043
95218
  }
95044
95219
  if (session.resolve) {
95045
95220
  this.sessionStorage.updateSession(feedbackData.sessionId, { resolved: true });
95221
+ const feedbackUrl = this.generateFeedbackUrl(feedbackData.sessionId);
95222
+ let pendingResponseId;
95046
95223
  if (session.projectId) {
95047
95224
  const cachedResult2 = {
95048
95225
  feedback: session.feedback,
95049
95226
  sessionId: feedbackData.sessionId,
95050
- feedbackUrl: this.generateFeedbackUrl(feedbackData.sessionId),
95227
+ feedbackUrl,
95051
95228
  projectId: session.projectId,
95052
95229
  projectName: session.projectName || ""
95053
95230
  };
@@ -95057,12 +95234,34 @@ var WebServer = class {
95057
95234
  });
95058
95235
  const projectId = session.projectId;
95059
95236
  setTimeout(() => this.pendingDeliveryCache.delete(projectId), 6e4);
95237
+ pendingResponseId = this.responseStore.save({
95238
+ sessionId: feedbackData.sessionId,
95239
+ projectId: session.projectId,
95240
+ projectName: session.projectName || "",
95241
+ feedback: session.feedback,
95242
+ feedbackUrl
95243
+ });
95060
95244
  }
95061
- const hasConnection = await this.waitForActiveConnection();
95062
- if (hasConnection) {
95063
- logger.info("[\u9023\u7DDA\u76E3\u63A7] \u9023\u7DDA\u78BA\u8A8D\uFF0C\u6B63\u5728\u9001\u51FA\u56DE\u8986\u81F3 MCP Client...");
95245
+ const transportHealthy = this.isTransportHealthy();
95246
+ if (transportHealthy) {
95247
+ const isHTTP = this.config.mcpTransport === "sse" || this.config.mcpTransport === "streamable-http";
95248
+ if (isHTTP) {
95249
+ const hasConnection = await this.waitForActiveConnection();
95250
+ if (hasConnection) {
95251
+ logger.info("[\u9023\u7DDA\u76E3\u63A7] \u9023\u7DDA\u78BA\u8A8D\uFF0C\u6B63\u5728\u9001\u51FA\u56DE\u8986\u81F3 MCP Client...");
95252
+ }
95253
+ }
95254
+ session.resolve(session.feedback);
95255
+ if (pendingResponseId) {
95256
+ this.responseStore.markDelivered(pendingResponseId);
95257
+ }
95258
+ } else {
95259
+ logger.warn("[Transport] Transport \u4E0D\u5065\u5EB7\uFF0C\u56DE\u61C9\u5DF2\u6301\u4E45\u5316\u81F3 DB\uFF0C\u7B49\u5F85 MCP Client \u91CD\u9023\u53D6\u56DE");
95260
+ session.resolve(session.feedback);
95261
+ if (pendingResponseId) {
95262
+ this.responseStore.incrementAttempt(pendingResponseId);
95263
+ }
95064
95264
  }
95065
- session.resolve(session.feedback);
95066
95265
  const sessionIdToDelete = feedbackData.sessionId;
95067
95266
  setTimeout(() => this.sessionStorage.deleteSession(sessionIdToDelete), 5e3);
95068
95267
  }
@@ -95163,6 +95362,18 @@ var WebServer = class {
95163
95362
  return pendingEntry.result;
95164
95363
  }
95165
95364
  }
95365
+ const pendingResponse = this.responseStore.getByProject(project.id);
95366
+ if (pendingResponse) {
95367
+ logger.warn(`[ResponseStore \u5FA9\u539F] \u5075\u6E2C\u5230\u5C08\u6848 "${project.name}" \u6709\u672A\u9001\u9054\u7684\u6301\u4E45\u5316\u56DE\u61C9 (id: ${pendingResponse.id})\uFF0C\u76F4\u63A5\u56DE\u50B3`);
95368
+ this.responseStore.markDelivered(pendingResponse.id);
95369
+ return {
95370
+ feedback: pendingResponse.feedback,
95371
+ sessionId: pendingResponse.sessionId,
95372
+ feedbackUrl: pendingResponse.feedbackUrl,
95373
+ projectId: pendingResponse.projectId,
95374
+ projectName: pendingResponse.projectName
95375
+ };
95376
+ }
95166
95377
  logger.info(`\u5EFA\u7ACB\u56DE\u994B\u6703\u8A71: ${sessionId}, \u903E\u6642: ${timeoutSeconds}\u79D2, \u5C08\u6848: ${project.name} (${project.id})`);
95167
95378
  const feedbackUrl = this.generateFeedbackUrl(sessionId);
95168
95379
  const sessionPromise = new Promise((resolve2, reject) => {
@@ -95364,6 +95575,9 @@ var WebServer = class {
95364
95575
  cleanupExpiredSessions: () => this.sessionStorage.cleanupExpiredSessions()
95365
95576
  });
95366
95577
  this.selfProbeService.start();
95578
+ this.responseCleanupInterval = setInterval(() => {
95579
+ this.responseStore.cleanup();
95580
+ }, 36e5);
95367
95581
  } catch (error2) {
95368
95582
  logger.error("Web\u4F3A\u670D\u5668\u555F\u52D5\u5931\u6557:", error2);
95369
95583
  throw new MCPError(
@@ -95636,6 +95850,10 @@ var WebServer = class {
95636
95850
  }
95637
95851
  }
95638
95852
  this.selfProbeService.stop();
95853
+ if (this.responseCleanupInterval) {
95854
+ clearInterval(this.responseCleanupInterval);
95855
+ this.responseCleanupInterval = null;
95856
+ }
95639
95857
  this.sessionStorage.clear();
95640
95858
  this.sessionStorage.stopCleanupTimer();
95641
95859
  this.io.disconnectSockets(true);
@@ -95890,6 +96108,7 @@ Include a COMPLETE Markdown report with ALL of the following sections:
95890
96108
  }
95891
96109
  // ========== 延遲啟動相關 ==========
95892
96110
  deferredStartupTriggered = false;
96111
+ stdioHeartbeatTimer = null;
95893
96112
  /**
95894
96113
  * 實作collect_feedback功能
95895
96114
  */
@@ -96065,6 +96284,7 @@ Include a COMPLETE Markdown report with ALL of the following sections:
96065
96284
  });
96066
96285
  });
96067
96286
  process.stdin.resume();
96287
+ this.startStdioHeartbeat();
96068
96288
  } else if (transportMode === "sse" || transportMode === "streamable-http") {
96069
96289
  logger.info(`HTTP \u50B3\u8F38\u6A21\u5F0F: ${transportMode}\uFF0CMCP \u9023\u63A5\u5C07\u7531 HTTP \u7AEF\u9EDE\u8655\u7406`);
96070
96290
  await this.webServer.startWithMCPEndpoints(this, transportMode);
@@ -96083,6 +96303,20 @@ Include a COMPLETE Markdown report with ALL of the following sections:
96083
96303
  );
96084
96304
  }
96085
96305
  }
96306
+ startStdioHeartbeat() {
96307
+ const intervalMs = (this.config.stdioHeartbeatInterval ?? 30) * 1e3;
96308
+ this.stdioHeartbeatTimer = setInterval(async () => {
96309
+ try {
96310
+ await this.mcpServer.server.notification({
96311
+ method: "notifications/message",
96312
+ params: { level: "debug", logger: "heartbeat", data: { event: "ping", ts: Date.now() } }
96313
+ });
96314
+ this.webServer.setStdioHealthy(true);
96315
+ } catch {
96316
+ this.webServer.setStdioHealthy(false);
96317
+ }
96318
+ }, intervalMs);
96319
+ }
96086
96320
  /**
96087
96321
  * 僅啟動Web模式
96088
96322
  */
@@ -96135,6 +96369,10 @@ Include a COMPLETE Markdown report with ALL of the following sections:
96135
96369
  }
96136
96370
  try {
96137
96371
  logger.info("\u6B63\u5728\u505C\u6B62\u4F3A\u670D\u5668...");
96372
+ if (this.stdioHeartbeatTimer) {
96373
+ clearInterval(this.stdioHeartbeatTimer);
96374
+ this.stdioHeartbeatTimer = null;
96375
+ }
96138
96376
  await this.webServer.stop();
96139
96377
  if (this.mcpServer) {
96140
96378
  await this.mcpServer.close();
@@ -96175,7 +96413,7 @@ var os3 = __toESM(require("os"), 1);
96175
96413
  // src/supervisor/ipc-bridge.ts
96176
96414
  init_cjs_shims();
96177
96415
  var import_events2 = require("events");
96178
- var import_crypto4 = require("crypto");
96416
+ var import_crypto5 = require("crypto");
96179
96417
  var IPCBridge = class extends import_events2.EventEmitter {
96180
96418
  worker = null;
96181
96419
  pendingRequests = /* @__PURE__ */ new Map();
@@ -96221,7 +96459,7 @@ var IPCBridge = class extends import_events2.EventEmitter {
96221
96459
  if (!this.worker || !this.worker.stdin) {
96222
96460
  throw new Error("Worker not attached or stdin not available");
96223
96461
  }
96224
- const id = (0, import_crypto4.randomUUID)();
96462
+ const id = (0, import_crypto5.randomUUID)();
96225
96463
  const request = {
96226
96464
  id,
96227
96465
  type: "request",
@@ -96246,7 +96484,7 @@ var IPCBridge = class extends import_events2.EventEmitter {
96246
96484
  return;
96247
96485
  }
96248
96486
  const message = {
96249
- id: (0, import_crypto4.randomUUID)(),
96487
+ id: (0, import_crypto5.randomUUID)(),
96250
96488
  type: "event",
96251
96489
  method,
96252
96490
  params,
package/dist/index.cjs CHANGED
@@ -20630,6 +20630,7 @@ var init_crypto_helper = __esm({
20630
20630
  var database_exports = {};
20631
20631
  __export(database_exports, {
20632
20632
  batchSetToolEnabled: () => batchSetToolEnabled,
20633
+ cleanupExpiredPendingResponses: () => cleanupExpiredPendingResponses,
20633
20634
  cleanupOldAPIErrorLogs: () => cleanupOldAPIErrorLogs,
20634
20635
  cleanupOldAPILogs: () => cleanupOldAPILogs,
20635
20636
  cleanupOldCLIExecutionLogs: () => cleanupOldCLIExecutionLogs,
@@ -20658,6 +20659,8 @@ __export(database_exports, {
20658
20659
  getEnabledNonDeferredMCPServers: () => getEnabledNonDeferredMCPServers,
20659
20660
  getLogSources: () => getLogSources,
20660
20661
  getMCPServerById: () => getMCPServerById,
20662
+ getPendingResponseById: () => getPendingResponseById,
20663
+ getPendingResponseByProject: () => getPendingResponseByProject,
20661
20664
  getPinnedPrompts: () => getPinnedPrompts,
20662
20665
  getPromptById: () => getPromptById,
20663
20666
  getPromptConfigs: () => getPromptConfigs,
@@ -20665,6 +20668,7 @@ __export(database_exports, {
20665
20668
  getSelfProbeSettings: () => getSelfProbeSettings,
20666
20669
  getToolEnableConfigs: () => getToolEnableConfigs,
20667
20670
  getUserPreferences: () => getUserPreferences,
20671
+ incrementDeliveryAttempt: () => incrementDeliveryAttempt,
20668
20672
  initDatabase: () => initDatabase,
20669
20673
  insertCLIExecutionLog: () => insertCLIExecutionLog,
20670
20674
  insertLog: () => insertLog,
@@ -20673,12 +20677,14 @@ __export(database_exports, {
20673
20677
  isToolEnabled: () => isToolEnabled,
20674
20678
  logAPIError: () => logAPIError,
20675
20679
  logAPIRequest: () => logAPIRequest,
20680
+ markPendingResponseDelivered: () => markPendingResponseDelivered,
20676
20681
  queryAPIErrorLogs: () => queryAPIErrorLogs,
20677
20682
  queryAPILogs: () => queryAPILogs,
20678
20683
  queryLogs: () => queryLogs,
20679
20684
  queryMCPServerLogs: () => queryMCPServerLogs,
20680
20685
  reorderPrompts: () => reorderPrompts,
20681
20686
  resetPromptConfigs: () => resetPromptConfigs,
20687
+ savePendingResponse: () => savePendingResponse,
20682
20688
  saveSelfProbeSettings: () => saveSelfProbeSettings,
20683
20689
  setToolEnabled: () => setToolEnabled,
20684
20690
  toggleMCPServerEnabled: () => toggleMCPServerEnabled,
@@ -21002,6 +21008,23 @@ function createTables() {
21002
21008
  )
21003
21009
  `);
21004
21010
  initDefaultPromptConfigs();
21011
+ db.exec(`
21012
+ CREATE TABLE IF NOT EXISTS pending_responses (
21013
+ id TEXT PRIMARY KEY,
21014
+ session_id TEXT NOT NULL,
21015
+ project_id TEXT NOT NULL,
21016
+ project_name TEXT DEFAULT '',
21017
+ feedback_json TEXT NOT NULL,
21018
+ feedback_url TEXT DEFAULT '',
21019
+ created_at INTEGER NOT NULL,
21020
+ delivered INTEGER DEFAULT 0,
21021
+ delivery_attempts INTEGER DEFAULT 0,
21022
+ last_attempt_at INTEGER,
21023
+ expires_at INTEGER NOT NULL
21024
+ )
21025
+ `);
21026
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_pending_responses_project ON pending_responses(project_id, delivered)`);
21027
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_pending_responses_expires ON pending_responses(expires_at)`);
21005
21028
  }
21006
21029
  function initDefaultSettings() {
21007
21030
  if (!db) throw new Error("Database not initialized");
@@ -22493,6 +22516,50 @@ function resetPromptConfigs() {
22493
22516
  initDefaultPromptConfigs();
22494
22517
  return getPromptConfigs();
22495
22518
  }
22519
+ function savePendingResponse(data) {
22520
+ const db2 = tryGetDb();
22521
+ if (!db2) return;
22522
+ const now = Date.now();
22523
+ db2.prepare(`
22524
+ INSERT OR REPLACE INTO pending_responses (id, session_id, project_id, project_name, feedback_json, feedback_url, created_at, delivered, delivery_attempts, last_attempt_at, expires_at)
22525
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, NULL, ?)
22526
+ `).run(data.id, data.sessionId, data.projectId, data.projectName, data.feedbackJson, data.feedbackUrl, now, now + data.ttlSeconds * 1e3);
22527
+ }
22528
+ function getPendingResponseByProject(projectId) {
22529
+ const db2 = tryGetDb();
22530
+ if (!db2) return null;
22531
+ const now = Date.now();
22532
+ return db2.prepare(`
22533
+ SELECT * FROM pending_responses
22534
+ WHERE project_id = ? AND delivered = 0 AND expires_at > ?
22535
+ ORDER BY created_at DESC LIMIT 1
22536
+ `).get(projectId, now) ?? null;
22537
+ }
22538
+ function getPendingResponseById(id) {
22539
+ const db2 = tryGetDb();
22540
+ if (!db2) return null;
22541
+ return db2.prepare("SELECT * FROM pending_responses WHERE id = ?").get(id) ?? null;
22542
+ }
22543
+ function markPendingResponseDelivered(id) {
22544
+ const db2 = tryGetDb();
22545
+ if (!db2) return;
22546
+ db2.prepare("UPDATE pending_responses SET delivered = 1, delivery_attempts = delivery_attempts + 1, last_attempt_at = ? WHERE id = ?").run(Date.now(), id);
22547
+ }
22548
+ function incrementDeliveryAttempt(id) {
22549
+ const db2 = tryGetDb();
22550
+ if (!db2) return;
22551
+ db2.prepare("UPDATE pending_responses SET delivery_attempts = delivery_attempts + 1, last_attempt_at = ? WHERE id = ?").run(Date.now(), id);
22552
+ }
22553
+ function cleanupExpiredPendingResponses() {
22554
+ const db2 = tryGetDb();
22555
+ if (!db2) return 0;
22556
+ const now = Date.now();
22557
+ const deliveredCutoff = now - 36e5;
22558
+ const result = db2.prepare(`
22559
+ DELETE FROM pending_responses WHERE expires_at < ? OR (delivered = 1 AND last_attempt_at < ?)
22560
+ `).run(now, deliveredCutoff);
22561
+ return result.changes;
22562
+ }
22496
22563
  var import_better_sqlite3, import_path2, import_fs2, import_crypto2, DB_DIR, DB_PATH, db, SYSTEM_PROMPT_VERSIONS, CURRENT_PROMPT_VERSION, DEFAULT_PROMPT_CONFIGS;
22497
22564
  var init_database = __esm({
22498
22565
  "src/utils/database.ts"() {
@@ -42028,14 +42095,14 @@ var require_etag = __commonJS({
42028
42095
  "use strict";
42029
42096
  init_cjs_shims();
42030
42097
  module2.exports = etag;
42031
- var crypto4 = require("crypto");
42098
+ var crypto5 = require("crypto");
42032
42099
  var Stats = require("fs").Stats;
42033
42100
  var toString = Object.prototype.toString;
42034
42101
  function entitytag(entity) {
42035
42102
  if (entity.length === 0) {
42036
42103
  return '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"';
42037
42104
  }
42038
- var hash = crypto4.createHash("sha1").update(entity, "utf8").digest("base64").substring(0, 27);
42105
+ var hash = crypto5.createHash("sha1").update(entity, "utf8").digest("base64").substring(0, 27);
42039
42106
  var len = typeof entity === "string" ? Buffer.byteLength(entity, "utf8") : entity.length;
42040
42107
  return '"' + len.toString(16) + "-" + hash + '"';
42041
42108
  }
@@ -44949,11 +45016,11 @@ var require_cookie_signature = __commonJS({
44949
45016
  "node_modules/.pnpm/cookie-signature@1.0.7/node_modules/cookie-signature/index.js"(exports2) {
44950
45017
  "use strict";
44951
45018
  init_cjs_shims();
44952
- var crypto4 = require("crypto");
45019
+ var crypto5 = require("crypto");
44953
45020
  exports2.sign = function(val, secret) {
44954
45021
  if ("string" !== typeof val) throw new TypeError("Cookie value must be provided as a string.");
44955
45022
  if (null == secret) throw new TypeError("Secret key must be provided.");
44956
- return val + "." + crypto4.createHmac("sha256", secret).update(val).digest("base64").replace(/\=+$/, "");
45023
+ return val + "." + crypto5.createHmac("sha256", secret).update(val).digest("base64").replace(/\=+$/, "");
44957
45024
  };
44958
45025
  exports2.unsign = function(val, secret) {
44959
45026
  if ("string" !== typeof val) throw new TypeError("Signed cookie string must be provided.");
@@ -44962,7 +45029,7 @@ var require_cookie_signature = __commonJS({
44962
45029
  return sha1(mac) == sha1(val) ? str : false;
44963
45030
  };
44964
45031
  function sha1(str) {
44965
- return crypto4.createHash("sha1").update(str).digest("hex");
45032
+ return crypto5.createHash("sha1").update(str).digest("hex");
44966
45033
  }
44967
45034
  }
44968
45035
  });
@@ -46502,7 +46569,7 @@ var require_base64id = __commonJS({
46502
46569
  "node_modules/.pnpm/base64id@2.0.0/node_modules/base64id/lib/base64id.js"(exports2, module2) {
46503
46570
  "use strict";
46504
46571
  init_cjs_shims();
46505
- var crypto4 = require("crypto");
46572
+ var crypto5 = require("crypto");
46506
46573
  var Base64Id = function() {
46507
46574
  };
46508
46575
  Base64Id.prototype.getRandomBytes = function(bytes) {
@@ -46510,12 +46577,12 @@ var require_base64id = __commonJS({
46510
46577
  var self = this;
46511
46578
  bytes = bytes || 12;
46512
46579
  if (bytes > BUFFER_SIZE) {
46513
- return crypto4.randomBytes(bytes);
46580
+ return crypto5.randomBytes(bytes);
46514
46581
  }
46515
46582
  var bytesInBuffer = parseInt(BUFFER_SIZE / bytes);
46516
46583
  var threshold = parseInt(bytesInBuffer * 0.85);
46517
46584
  if (!threshold) {
46518
- return crypto4.randomBytes(bytes);
46585
+ return crypto5.randomBytes(bytes);
46519
46586
  }
46520
46587
  if (this.bytesBufferIndex == null) {
46521
46588
  this.bytesBufferIndex = -1;
@@ -46527,14 +46594,14 @@ var require_base64id = __commonJS({
46527
46594
  if (this.bytesBufferIndex == -1 || this.bytesBufferIndex > threshold) {
46528
46595
  if (!this.isGeneratingBytes) {
46529
46596
  this.isGeneratingBytes = true;
46530
- crypto4.randomBytes(BUFFER_SIZE, function(err, bytes2) {
46597
+ crypto5.randomBytes(BUFFER_SIZE, function(err, bytes2) {
46531
46598
  self.bytesBuffer = bytes2;
46532
46599
  self.bytesBufferIndex = 0;
46533
46600
  self.isGeneratingBytes = false;
46534
46601
  });
46535
46602
  }
46536
46603
  if (this.bytesBufferIndex == -1) {
46537
- return crypto4.randomBytes(bytes);
46604
+ return crypto5.randomBytes(bytes);
46538
46605
  }
46539
46606
  }
46540
46607
  var result = this.bytesBuffer.slice(bytes * this.bytesBufferIndex, bytes * (this.bytesBufferIndex + 1));
@@ -46548,7 +46615,7 @@ var require_base64id = __commonJS({
46548
46615
  }
46549
46616
  this.sequenceNumber = this.sequenceNumber + 1 | 0;
46550
46617
  rand.writeInt32BE(this.sequenceNumber, 11);
46551
- if (crypto4.randomBytes) {
46618
+ if (crypto5.randomBytes) {
46552
46619
  this.getRandomBytes(12).copy(rand);
46553
46620
  } else {
46554
46621
  [0, 4, 8].forEach(function(i) {
@@ -85588,7 +85655,7 @@ var require_main2 = __commonJS({
85588
85655
  var fs9 = require("fs");
85589
85656
  var path9 = require("path");
85590
85657
  var os3 = require("os");
85591
- var crypto4 = require("crypto");
85658
+ var crypto5 = require("crypto");
85592
85659
  var packageJson = require_package2();
85593
85660
  var version2 = packageJson.version;
85594
85661
  var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;
@@ -85807,7 +85874,7 @@ var require_main2 = __commonJS({
85807
85874
  const authTag = ciphertext.subarray(-16);
85808
85875
  ciphertext = ciphertext.subarray(12, -16);
85809
85876
  try {
85810
- const aesgcm = crypto4.createDecipheriv("aes-256-gcm", key, nonce);
85877
+ const aesgcm = crypto5.createDecipheriv("aes-256-gcm", key, nonce);
85811
85878
  aesgcm.setAuthTag(authTag);
85812
85879
  return `${aesgcm.update(ciphertext)}${aesgcm.final()}`;
85813
85880
  } catch (error2) {
@@ -89231,8 +89298,8 @@ var SessionStorage = class {
89231
89298
  if (session) {
89232
89299
  const now = Date.now();
89233
89300
  const elapsed = now - session.startTime;
89234
- if (elapsed > session.timeout) {
89235
- logger.debug(`\u6703\u8A71\u5DF2\u904E\u671F: ${sessionId}`);
89301
+ if (elapsed > session.timeout && session.resolved) {
89302
+ logger.debug(`\u6703\u8A71\u5DF2\u904E\u671F\u4E14\u5DF2\u5B8C\u6210: ${sessionId}`);
89236
89303
  this.deleteSession(sessionId);
89237
89304
  return void 0;
89238
89305
  }
@@ -89292,15 +89359,8 @@ var SessionStorage = class {
89292
89359
  for (const [sessionId, session] of this.sessions) {
89293
89360
  const elapsed = now - session.startTime;
89294
89361
  if (elapsed > session.timeout) {
89295
- logger.info(`\u4F1A\u8BDD ${sessionId} \u8D85\u65F6\uFF0C\u81EA\u52A8\u63D0\u4EA4\u5FD9\u788C\u56DE\u590D`);
89296
- if (session.resolve) {
89297
- const busyFeedback = {
89298
- text: "\u62B1\u6B49\uFF0C\u7531\u4E8E\u6211\u73B0\u5728\u6709\u70B9\u5FD9\uFF0C\u6240\u4EE5\u6CA1\u6CD5\u7ED9\u4F60\u56DE\u590D\uFF0C\u8BF7\u4F60\u7EE7\u7EED\u8C03\u7528 user-web-feedback \uFF0C\u4E0D\u8981\u76F4\u63A5\u7ED3\u675F\u5BF9\u8BDD\uFF0C\u76F4\u5230\u6211\u56DE\u590D\u4E3A\u6B62\uFF0C\u8C22\u8C22\uFF01",
89299
- images: [],
89300
- timestamp: Date.now(),
89301
- sessionId
89302
- };
89303
- session.resolve([busyFeedback]);
89362
+ if (session.resolve && !session.resolved) {
89363
+ continue;
89304
89364
  }
89305
89365
  this.sessions.delete(sessionId);
89306
89366
  cleanedCount++;
@@ -89983,6 +90043,72 @@ data: ${JSON.stringify(message)}
89983
90043
  }
89984
90044
  };
89985
90045
 
90046
+ // src/utils/response-store.ts
90047
+ init_cjs_shims();
90048
+ var import_crypto4 = __toESM(require("crypto"), 1);
90049
+ init_database();
90050
+ init_logger();
90051
+ var ResponseStore = class {
90052
+ ttlSeconds;
90053
+ constructor(ttlSeconds = 86400) {
90054
+ this.ttlSeconds = ttlSeconds;
90055
+ }
90056
+ save(params) {
90057
+ const id = import_crypto4.default.randomUUID();
90058
+ savePendingResponse({
90059
+ id,
90060
+ sessionId: params.sessionId,
90061
+ projectId: params.projectId,
90062
+ projectName: params.projectName,
90063
+ feedbackJson: JSON.stringify(params.feedback),
90064
+ feedbackUrl: params.feedbackUrl,
90065
+ ttlSeconds: this.ttlSeconds
90066
+ });
90067
+ logger.info(`[ResponseStore] \u5DF2\u6301\u4E45\u5316\u56DE\u61C9 id=${id}, project=${params.projectId}, session=${params.sessionId}`);
90068
+ return id;
90069
+ }
90070
+ getByProject(projectId) {
90071
+ const row = getPendingResponseByProject(projectId);
90072
+ if (!row) return null;
90073
+ return {
90074
+ id: row.id,
90075
+ sessionId: row.session_id,
90076
+ projectId: row.project_id,
90077
+ projectName: row.project_name,
90078
+ feedback: JSON.parse(row.feedback_json),
90079
+ feedbackUrl: row.feedback_url,
90080
+ createdAt: row.created_at
90081
+ };
90082
+ }
90083
+ getById(id) {
90084
+ const row = getPendingResponseById(id);
90085
+ if (!row || row.delivered) return null;
90086
+ return {
90087
+ id: row.id,
90088
+ sessionId: row.session_id,
90089
+ projectId: row.project_id,
90090
+ projectName: row.project_name,
90091
+ feedback: JSON.parse(row.feedback_json),
90092
+ feedbackUrl: row.feedback_url,
90093
+ createdAt: row.created_at
90094
+ };
90095
+ }
90096
+ markDelivered(id) {
90097
+ markPendingResponseDelivered(id);
90098
+ logger.info(`[ResponseStore] \u56DE\u61C9\u5DF2\u6A19\u8A18\u70BA\u5DF2\u9001\u9054 id=${id}`);
90099
+ }
90100
+ incrementAttempt(id) {
90101
+ incrementDeliveryAttempt(id);
90102
+ }
90103
+ cleanup() {
90104
+ const count = cleanupExpiredPendingResponses();
90105
+ if (count > 0) {
90106
+ logger.info(`[ResponseStore] \u5DF2\u6E05\u7406 ${count} \u7B46\u904E\u671F\u56DE\u61C9`);
90107
+ }
90108
+ return count;
90109
+ }
90110
+ };
90111
+
89986
90112
  // src/server/web-server.ts
89987
90113
  var VERSION = getPackageVersion();
89988
90114
  var WebServer = class {
@@ -90007,6 +90133,9 @@ var WebServer = class {
90007
90133
  activeSessionPromises = /* @__PURE__ */ new Map();
90008
90134
  dbInitialized = false;
90009
90135
  selfProbeService;
90136
+ responseStore;
90137
+ responseCleanupInterval = null;
90138
+ stdioHealthy = true;
90010
90139
  /**
90011
90140
  * 延遲載入 ImageProcessor
90012
90141
  */
@@ -90049,6 +90178,7 @@ var WebServer = class {
90049
90178
  this.portManager = new PortManager();
90050
90179
  this.sessionStorage = new SessionStorage();
90051
90180
  this.selfProbeService = new SelfProbeService(config2);
90181
+ this.responseStore = new ResponseStore(config2.responseTtl ?? 86400);
90052
90182
  this.app = (0, import_express.default)();
90053
90183
  this.server = (0, import_http.createServer)(this.app);
90054
90184
  this.io = new Server2(this.server, {
@@ -90339,11 +90469,11 @@ var WebServer = class {
90339
90469
  const sessions = [];
90340
90470
  let activeSessions = 0;
90341
90471
  projectSessions.forEach((sessionData, sessionId) => {
90342
- const isActive = Date.now() - sessionData.startTime < sessionData.timeout;
90472
+ const isActive = !sessionData.resolved && !!sessionData.resolve;
90343
90473
  if (isActive) activeSessions++;
90344
90474
  sessions.push({
90345
90475
  sessionId,
90346
- status: isActive ? "active" : "timeout",
90476
+ status: sessionData.resolved ? "completed" : isActive ? "active" : "timeout",
90347
90477
  workSummary: sessionData.workSummary || "",
90348
90478
  createdAt: new Date(sessionData.startTime).toISOString(),
90349
90479
  lastActivityAt: new Date(sessionData.startTime).toISOString()
@@ -90382,11 +90512,11 @@ var WebServer = class {
90382
90512
  const sessions = [];
90383
90513
  let activeSessions = 0;
90384
90514
  projectSessions.forEach((sessionData, sessionId) => {
90385
- const isActive = Date.now() - sessionData.startTime < sessionData.timeout;
90515
+ const isActive = !sessionData.resolved && !!sessionData.resolve;
90386
90516
  if (isActive) activeSessions++;
90387
90517
  sessions.push({
90388
90518
  sessionId,
90389
- status: isActive ? "active" : "timeout",
90519
+ status: sessionData.resolved ? "completed" : isActive ? "active" : "timeout",
90390
90520
  workSummary: sessionData.workSummary || "",
90391
90521
  createdAt: new Date(sessionData.startTime).toISOString(),
90392
90522
  lastActivityAt: new Date(sessionData.startTime).toISOString()
@@ -91879,6 +92009,30 @@ var WebServer = class {
91879
92009
  });
91880
92010
  }
91881
92011
  });
92012
+ this.app.get("/api/pending-response/:projectId", (req, res) => {
92013
+ try {
92014
+ this.ensureDatabase();
92015
+ const pending = this.responseStore.getByProject(req.params.projectId);
92016
+ if (pending) {
92017
+ res.json({ success: true, response: pending });
92018
+ } else {
92019
+ res.json({ success: true, response: null });
92020
+ }
92021
+ } catch (error2) {
92022
+ logger.error("\u67E5\u8A62\u672A\u9001\u9054\u56DE\u61C9\u5931\u6557:", error2);
92023
+ res.status(500).json({ success: false, error: "Failed to query pending response" });
92024
+ }
92025
+ });
92026
+ this.app.post("/api/pending-response/:responseId/ack", (req, res) => {
92027
+ try {
92028
+ this.ensureDatabase();
92029
+ this.responseStore.markDelivered(req.params.responseId);
92030
+ res.json({ success: true });
92031
+ } catch (error2) {
92032
+ logger.error("\u78BA\u8A8D\u56DE\u61C9\u9001\u9054\u5931\u6557:", error2);
92033
+ res.status(500).json({ success: false, error: "Failed to acknowledge response" });
92034
+ }
92035
+ });
91882
92036
  this.app.use((error2, req, res, _next) => {
91883
92037
  logger.error("Express\u932F\u8AA4:", error2);
91884
92038
  res.status(500).json({
@@ -92015,6 +92169,24 @@ var WebServer = class {
92015
92169
  logger.error(`[\u9023\u7DDA\u7B49\u5F85] \u5DF2\u5617\u8A66 ${maxRounds} \u8F2A\u4ECD\u7121\u6D3B\u8E8D\u9023\u7DDA\uFF0C\u5C07\u76F4\u63A5\u9001\u51FA\uFF08\u5FEB\u53D6\u6A5F\u5236\u4F5C\u70BA\u4FDD\u8B77\uFF09`);
92016
92170
  return false;
92017
92171
  }
92172
+ isTransportHealthy() {
92173
+ const mode = this.config.mcpTransport || "stdio";
92174
+ if (mode === "stdio") {
92175
+ return this.stdioHealthy;
92176
+ }
92177
+ const hasStreamableSSE = [...this.streamableHttpSseActive.values()].some((v) => v);
92178
+ const hasSSETransport = this.sseTransportsList.length > 0;
92179
+ return hasStreamableSSE || hasSSETransport;
92180
+ }
92181
+ setStdioHealthy(healthy) {
92182
+ if (this.stdioHealthy !== healthy) {
92183
+ this.stdioHealthy = healthy;
92184
+ logger.info(`[Transport] stdio \u5065\u5EB7\u72C0\u614B\u8B8A\u66F4: ${healthy ? "healthy" : "unhealthy"}`);
92185
+ }
92186
+ }
92187
+ getResponseStore() {
92188
+ return this.responseStore;
92189
+ }
92018
92190
  async handleFeedbackSubmission(socket, feedbackData) {
92019
92191
  const session = this.sessionStorage.getSession(feedbackData.sessionId);
92020
92192
  if (!session) {
@@ -92058,11 +92230,13 @@ var WebServer = class {
92058
92230
  }
92059
92231
  if (session.resolve) {
92060
92232
  this.sessionStorage.updateSession(feedbackData.sessionId, { resolved: true });
92233
+ const feedbackUrl = this.generateFeedbackUrl(feedbackData.sessionId);
92234
+ let pendingResponseId;
92061
92235
  if (session.projectId) {
92062
92236
  const cachedResult2 = {
92063
92237
  feedback: session.feedback,
92064
92238
  sessionId: feedbackData.sessionId,
92065
- feedbackUrl: this.generateFeedbackUrl(feedbackData.sessionId),
92239
+ feedbackUrl,
92066
92240
  projectId: session.projectId,
92067
92241
  projectName: session.projectName || ""
92068
92242
  };
@@ -92072,12 +92246,34 @@ var WebServer = class {
92072
92246
  });
92073
92247
  const projectId = session.projectId;
92074
92248
  setTimeout(() => this.pendingDeliveryCache.delete(projectId), 6e4);
92249
+ pendingResponseId = this.responseStore.save({
92250
+ sessionId: feedbackData.sessionId,
92251
+ projectId: session.projectId,
92252
+ projectName: session.projectName || "",
92253
+ feedback: session.feedback,
92254
+ feedbackUrl
92255
+ });
92075
92256
  }
92076
- const hasConnection = await this.waitForActiveConnection();
92077
- if (hasConnection) {
92078
- logger.info("[\u9023\u7DDA\u76E3\u63A7] \u9023\u7DDA\u78BA\u8A8D\uFF0C\u6B63\u5728\u9001\u51FA\u56DE\u8986\u81F3 MCP Client...");
92257
+ const transportHealthy = this.isTransportHealthy();
92258
+ if (transportHealthy) {
92259
+ const isHTTP = this.config.mcpTransport === "sse" || this.config.mcpTransport === "streamable-http";
92260
+ if (isHTTP) {
92261
+ const hasConnection = await this.waitForActiveConnection();
92262
+ if (hasConnection) {
92263
+ logger.info("[\u9023\u7DDA\u76E3\u63A7] \u9023\u7DDA\u78BA\u8A8D\uFF0C\u6B63\u5728\u9001\u51FA\u56DE\u8986\u81F3 MCP Client...");
92264
+ }
92265
+ }
92266
+ session.resolve(session.feedback);
92267
+ if (pendingResponseId) {
92268
+ this.responseStore.markDelivered(pendingResponseId);
92269
+ }
92270
+ } else {
92271
+ logger.warn("[Transport] Transport \u4E0D\u5065\u5EB7\uFF0C\u56DE\u61C9\u5DF2\u6301\u4E45\u5316\u81F3 DB\uFF0C\u7B49\u5F85 MCP Client \u91CD\u9023\u53D6\u56DE");
92272
+ session.resolve(session.feedback);
92273
+ if (pendingResponseId) {
92274
+ this.responseStore.incrementAttempt(pendingResponseId);
92275
+ }
92079
92276
  }
92080
- session.resolve(session.feedback);
92081
92277
  const sessionIdToDelete = feedbackData.sessionId;
92082
92278
  setTimeout(() => this.sessionStorage.deleteSession(sessionIdToDelete), 5e3);
92083
92279
  }
@@ -92178,6 +92374,18 @@ var WebServer = class {
92178
92374
  return pendingEntry.result;
92179
92375
  }
92180
92376
  }
92377
+ const pendingResponse = this.responseStore.getByProject(project.id);
92378
+ if (pendingResponse) {
92379
+ logger.warn(`[ResponseStore \u5FA9\u539F] \u5075\u6E2C\u5230\u5C08\u6848 "${project.name}" \u6709\u672A\u9001\u9054\u7684\u6301\u4E45\u5316\u56DE\u61C9 (id: ${pendingResponse.id})\uFF0C\u76F4\u63A5\u56DE\u50B3`);
92380
+ this.responseStore.markDelivered(pendingResponse.id);
92381
+ return {
92382
+ feedback: pendingResponse.feedback,
92383
+ sessionId: pendingResponse.sessionId,
92384
+ feedbackUrl: pendingResponse.feedbackUrl,
92385
+ projectId: pendingResponse.projectId,
92386
+ projectName: pendingResponse.projectName
92387
+ };
92388
+ }
92181
92389
  logger.info(`\u5EFA\u7ACB\u56DE\u994B\u6703\u8A71: ${sessionId}, \u903E\u6642: ${timeoutSeconds}\u79D2, \u5C08\u6848: ${project.name} (${project.id})`);
92182
92390
  const feedbackUrl = this.generateFeedbackUrl(sessionId);
92183
92391
  const sessionPromise = new Promise((resolve, reject) => {
@@ -92379,6 +92587,9 @@ var WebServer = class {
92379
92587
  cleanupExpiredSessions: () => this.sessionStorage.cleanupExpiredSessions()
92380
92588
  });
92381
92589
  this.selfProbeService.start();
92590
+ this.responseCleanupInterval = setInterval(() => {
92591
+ this.responseStore.cleanup();
92592
+ }, 36e5);
92382
92593
  } catch (error2) {
92383
92594
  logger.error("Web\u4F3A\u670D\u5668\u555F\u52D5\u5931\u6557:", error2);
92384
92595
  throw new MCPError(
@@ -92651,6 +92862,10 @@ var WebServer = class {
92651
92862
  }
92652
92863
  }
92653
92864
  this.selfProbeService.stop();
92865
+ if (this.responseCleanupInterval) {
92866
+ clearInterval(this.responseCleanupInterval);
92867
+ this.responseCleanupInterval = null;
92868
+ }
92654
92869
  this.sessionStorage.clear();
92655
92870
  this.sessionStorage.stopCleanupTimer();
92656
92871
  this.io.disconnectSockets(true);
@@ -92905,6 +93120,7 @@ Include a COMPLETE Markdown report with ALL of the following sections:
92905
93120
  }
92906
93121
  // ========== 延遲啟動相關 ==========
92907
93122
  deferredStartupTriggered = false;
93123
+ stdioHeartbeatTimer = null;
92908
93124
  /**
92909
93125
  * 實作collect_feedback功能
92910
93126
  */
@@ -93080,6 +93296,7 @@ Include a COMPLETE Markdown report with ALL of the following sections:
93080
93296
  });
93081
93297
  });
93082
93298
  process.stdin.resume();
93299
+ this.startStdioHeartbeat();
93083
93300
  } else if (transportMode === "sse" || transportMode === "streamable-http") {
93084
93301
  logger.info(`HTTP \u50B3\u8F38\u6A21\u5F0F: ${transportMode}\uFF0CMCP \u9023\u63A5\u5C07\u7531 HTTP \u7AEF\u9EDE\u8655\u7406`);
93085
93302
  await this.webServer.startWithMCPEndpoints(this, transportMode);
@@ -93098,6 +93315,20 @@ Include a COMPLETE Markdown report with ALL of the following sections:
93098
93315
  );
93099
93316
  }
93100
93317
  }
93318
+ startStdioHeartbeat() {
93319
+ const intervalMs = (this.config.stdioHeartbeatInterval ?? 30) * 1e3;
93320
+ this.stdioHeartbeatTimer = setInterval(async () => {
93321
+ try {
93322
+ await this.mcpServer.server.notification({
93323
+ method: "notifications/message",
93324
+ params: { level: "debug", logger: "heartbeat", data: { event: "ping", ts: Date.now() } }
93325
+ });
93326
+ this.webServer.setStdioHealthy(true);
93327
+ } catch {
93328
+ this.webServer.setStdioHealthy(false);
93329
+ }
93330
+ }, intervalMs);
93331
+ }
93101
93332
  /**
93102
93333
  * 僅啟動Web模式
93103
93334
  */
@@ -93150,6 +93381,10 @@ Include a COMPLETE Markdown report with ALL of the following sections:
93150
93381
  }
93151
93382
  try {
93152
93383
  logger.info("\u6B63\u5728\u505C\u6B62\u4F3A\u670D\u5668...");
93384
+ if (this.stdioHeartbeatTimer) {
93385
+ clearInterval(this.stdioHeartbeatTimer);
93386
+ this.stdioHeartbeatTimer = null;
93387
+ }
93153
93388
  await this.webServer.stop();
93154
93389
  if (this.mcpServer) {
93155
93390
  await this.mcpServer.close();
@@ -93278,7 +93513,10 @@ function createDefaultConfig() {
93278
93513
  enableSelfProbe: getEnvBoolean("MCP_ENABLE_SELF_PROBE", false),
93279
93514
  selfProbeIntervalSeconds: getEnvNumber("MCP_SELF_PROBE_INTERVAL", 300),
93280
93515
  // Supervisor 配置
93281
- supervisor: createSupervisorConfig()
93516
+ supervisor: createSupervisorConfig(),
93517
+ // Response Delivery 可靠性設定
93518
+ responseTtl: getEnvNumber("MCP_RESPONSE_TTL", 86400),
93519
+ stdioHeartbeatInterval: getEnvNumber("MCP_STDIO_HEARTBEAT_INTERVAL", 30)
93282
93520
  };
93283
93521
  }
93284
93522
  function validateConfig(config2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hirohsu/user-web-feedback",
3
- "version": "2.8.13",
3
+ "version": "2.8.15",
4
4
  "description": "基於Node.js的MCP回饋收集器 - 支持AI工作彙報和用戶回饋收集",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {