nikou-cli 0.1.6 → 0.1.8

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/README.md +4 -0
  2. package/dist/index.js +174 -97
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -162,6 +162,8 @@ Hook 支持三种启动模式:
162
162
 
163
163
  主节点负责连接聊天平台、接收消息、维护 Worker 列表并分发任务。从节点负责连接主节点,收到任务后调用 Codex、Claude 或 Gemini 执行。
164
164
 
165
+ 群聊发送 `@机器人 绑定: <目录>` 时,主节点会先检查当前用户是否已经绑定该目录;已有绑定会直接返回结果,未绑定则只把路径校验任务发给该用户自己的在线 Worker。一个群可以保留多个用户、多个目录的 Agent 绑定,绑定指令不会广播给其他 Worker 抢占。
166
+
165
167
  ### 初始化配置
166
168
 
167
169
  ```bash
@@ -177,6 +179,8 @@ ai-hook local
177
179
 
178
180
  初始化时会提示部署方式和主节点地址。没有远端主节点配置时默认选择本机一体,并使用 `127.0.0.1`;如果旧配置里已有非本机主节点地址,默认选择从节点并沿用该地址。
179
181
 
182
+ 单独从节点连接已有主节点时,不要求配置主节点专用的 `hook.bind_allowed_user_ids`。本机一体或单独启动主节点时,仍必须至少配置一个允许绑定的飞书 `user_id`。
183
+
180
184
  `ai-hook` 裸命令为兼容旧习惯,仍默认等价于 `nikou-cli hook worker codex`;如需只启动 Claude Worker:
181
185
 
182
186
  ```bash
package/dist/index.js CHANGED
@@ -912,7 +912,7 @@ function pad3(str, width) {
912
912
 
913
913
  // src/hook/master/master-command.ts
914
914
  import * as lark2 from "@larksuiteoapi/node-sdk";
915
- import fs8 from "fs";
915
+ import fs7 from "fs";
916
916
  import path8 from "path";
917
917
  import process4 from "process";
918
918
 
@@ -975,7 +975,7 @@ var CARD_STREAM_STDOUT_MAX_LENGTH = 12e3;
975
975
  var BIND_WHITELIST_REFRESH_MS = 5e3;
976
976
 
977
977
  // src/hook/utils.ts
978
- import fs2 from "fs";
978
+ import fs from "fs";
979
979
  import os2 from "os";
980
980
  import path2 from "path";
981
981
  import process2 from "process";
@@ -1029,9 +1029,9 @@ function resolveAiHookGuestPath() {
1029
1029
  }
1030
1030
  function resolveOpsUserName() {
1031
1031
  const propsPath = path2.join(os2.homedir(), "ops", "ops_global.properties");
1032
- if (!fs2.existsSync(propsPath)) return "\u672A\u77E5";
1032
+ if (!fs.existsSync(propsPath)) return "\u672A\u77E5";
1033
1033
  try {
1034
- const lines = fs2.readFileSync(propsPath, "utf-8").split(/\r?\n/);
1034
+ const lines = fs.readFileSync(propsPath, "utf-8").split(/\r?\n/);
1035
1035
  for (const line of lines) {
1036
1036
  const trimmed = line.trim();
1037
1037
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -1056,8 +1056,8 @@ function resolveCardPathDisplay(dirPath) {
1056
1056
  return `${segments[segments.length - 2]}\\${segments[segments.length - 1]}`;
1057
1057
  }
1058
1058
  function ensureDirExists(dirPath) {
1059
- if (!fs2.existsSync(dirPath)) {
1060
- fs2.mkdirSync(dirPath, { recursive: true });
1059
+ if (!fs.existsSync(dirPath)) {
1060
+ fs.mkdirSync(dirPath, { recursive: true });
1061
1061
  }
1062
1062
  }
1063
1063
  function normalizeHost(value) {
@@ -1369,7 +1369,7 @@ function formatCompactTokenCount(value) {
1369
1369
  }
1370
1370
 
1371
1371
  // src/hook/config-loader.ts
1372
- import fs3 from "fs";
1372
+ import fs2 from "fs";
1373
1373
 
1374
1374
  // src/hook/json-config.ts
1375
1375
  function stripJsonLineComments(content) {
@@ -1413,7 +1413,7 @@ function parseJsonWithLineComments(content, filePath = "JSON \u914D\u7F6E") {
1413
1413
  // src/hook/config-loader.ts
1414
1414
  function resolveConfigPath(role = "worker") {
1415
1415
  const configPath = role === "master" ? MASTER_CONFIG_PATH : WORKER_CONFIG_PATH;
1416
- return fs3.existsSync(configPath) ? configPath : null;
1416
+ return fs2.existsSync(configPath) ? configPath : null;
1417
1417
  }
1418
1418
  function loadRoleConfig(role) {
1419
1419
  const configPath = resolveConfigPath(role);
@@ -1427,7 +1427,7 @@ function loadRoleConfig(role) {
1427
1427
  ` + sampleBody + "}"
1428
1428
  );
1429
1429
  }
1430
- const raw = fs3.readFileSync(configPath, "utf-8");
1430
+ const raw = fs2.readFileSync(configPath, "utf-8");
1431
1431
  const config = parseJsonWithLineComments(raw, configPath);
1432
1432
  return { config, configPath };
1433
1433
  }
@@ -1597,7 +1597,7 @@ function resolveAuthConfig(config) {
1597
1597
  }
1598
1598
 
1599
1599
  // src/hook/state-manager.ts
1600
- import fs4 from "fs";
1600
+ import fs3 from "fs";
1601
1601
  import path3 from "path";
1602
1602
  import process3 from "process";
1603
1603
  var StateManager = class {
@@ -1609,13 +1609,13 @@ var StateManager = class {
1609
1609
  this.logger = logger3;
1610
1610
  }
1611
1611
  read() {
1612
- if (!fs4.existsSync(this.statePath)) {
1612
+ if (!fs3.existsSync(this.statePath)) {
1613
1613
  this.state = null;
1614
1614
  return null;
1615
1615
  }
1616
1616
  for (let attempt = 0; attempt < 2; attempt++) {
1617
1617
  try {
1618
- const raw = fs4.readFileSync(this.statePath, "utf-8");
1618
+ const raw = fs3.readFileSync(this.statePath, "utf-8");
1619
1619
  this.state = JSON.parse(raw);
1620
1620
  return this.state;
1621
1621
  } catch (error) {
@@ -1634,13 +1634,13 @@ var StateManager = class {
1634
1634
  const tempPath = `${this.statePath}.tmp.${process3.pid}`;
1635
1635
  try {
1636
1636
  ensureDirExists(path3.dirname(this.statePath));
1637
- fs4.writeFileSync(tempPath, JSON.stringify(nextState, null, 4));
1638
- fs4.renameSync(tempPath, this.statePath);
1637
+ fs3.writeFileSync(tempPath, JSON.stringify(nextState, null, 4));
1638
+ fs3.renameSync(tempPath, this.statePath);
1639
1639
  } catch (error) {
1640
1640
  const msg = error instanceof Error ? error.message : String(error);
1641
1641
  this.logger.warn(`\u5199\u5165\u72B6\u6001\u6587\u4EF6\u5931\u8D25: ${msg}`);
1642
1642
  try {
1643
- if (fs4.existsSync(tempPath)) fs4.unlinkSync(tempPath);
1643
+ if (fs3.existsSync(tempPath)) fs3.unlinkSync(tempPath);
1644
1644
  } catch (cleanupError) {
1645
1645
  const cmsg = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
1646
1646
  this.logger.warn(`\u6E05\u7406\u4E34\u65F6\u72B6\u6001\u6587\u4EF6\u5931\u8D25: ${cmsg}`);
@@ -3041,7 +3041,10 @@ var TaskManager = class {
3041
3041
  }
3042
3042
  if (task.status !== "pending") {
3043
3043
  this.logger.info(
3044
- formatTraceLog(task.traceId, `\u8BA4\u9886\u88AB\u62D2\u7EDD\uFF1A\u4EFB\u52A1\u5DF2\u88AB\u8BA4\u9886 worker=${workerMeta.workerId}`)
3044
+ formatTraceLog(
3045
+ task.traceId,
3046
+ `\u8BA4\u9886\u88AB\u62D2\u7EDD\uFF1A\u4EFB\u52A1\u5DF2\u88AB\u8BA4\u9886 claimed_worker=${task.claimedWorkerId || "-"}, rejected_worker=${workerMeta.workerId}`
3047
+ )
3045
3048
  );
3046
3049
  return false;
3047
3050
  }
@@ -3909,7 +3912,7 @@ var DirectWorkerRegistry = class {
3909
3912
  };
3910
3913
 
3911
3914
  // src/hook/master/routing/chat-binding-registry.ts
3912
- import fs5 from "fs";
3915
+ import fs4 from "fs";
3913
3916
  import path4 from "path";
3914
3917
  function normalize2(value) {
3915
3918
  return String(value || "").trim();
@@ -3970,12 +3973,12 @@ var ChatBindingRegistry = class {
3970
3973
  }
3971
3974
  load() {
3972
3975
  this.bindings.clear();
3973
- if (!fs5.existsSync(this.filePath)) {
3976
+ if (!fs4.existsSync(this.filePath)) {
3974
3977
  this.save();
3975
3978
  return;
3976
3979
  }
3977
3980
  try {
3978
- const parsed = JSON.parse(fs5.readFileSync(this.filePath, "utf-8") || "{}");
3981
+ const parsed = JSON.parse(fs4.readFileSync(this.filePath, "utf-8") || "{}");
3979
3982
  for (const [chatId, value] of Object.entries(parsed.bindings || {})) {
3980
3983
  const normalizedChatId = normalize2(chatId);
3981
3984
  if (!normalizedChatId || !value || typeof value !== "object") {
@@ -4003,7 +4006,7 @@ var ChatBindingRegistry = class {
4003
4006
  }
4004
4007
  }
4005
4008
  save() {
4006
- fs5.mkdirSync(path4.dirname(this.filePath), { recursive: true });
4009
+ fs4.mkdirSync(path4.dirname(this.filePath), { recursive: true });
4007
4010
  const bindings = {};
4008
4011
  for (const [chatId, chatMap] of this.bindings.entries()) {
4009
4012
  const serialized = {};
@@ -4012,7 +4015,7 @@ var ChatBindingRegistry = class {
4012
4015
  }
4013
4016
  bindings[chatId] = serialized;
4014
4017
  }
4015
- fs5.writeFileSync(
4018
+ fs4.writeFileSync(
4016
4019
  this.filePath,
4017
4020
  `${JSON.stringify({ version: 2, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), bindings }, null, 2)}
4018
4021
  `,
@@ -4028,6 +4031,15 @@ var ChatBindingRegistry = class {
4028
4031
  getByKey(chatId, bindingKey) {
4029
4032
  return this.bindings.get(normalize2(chatId))?.get(normalize2(bindingKey)) || null;
4030
4033
  }
4034
+ /** 按群、绑定用户和目录查找普通绑定,供绑定指令做幂等校验。 */
4035
+ getNormalBinding(chatId, ownerUserId, dirPath) {
4036
+ const owner = normalize2(ownerUserId);
4037
+ const dir = normalize2(dirPath);
4038
+ if (!owner || !dir) {
4039
+ return null;
4040
+ }
4041
+ return this.listByChat(chatId).find((binding) => binding.kind === "normal" && binding.ownerUserId === owner && binding.dirPath === dir) || null;
4042
+ }
4031
4043
  /** 返回全部绑定记录(扁平) */
4032
4044
  list() {
4033
4045
  const all = [];
@@ -4136,7 +4148,7 @@ var ChatBindingRegistry = class {
4136
4148
  };
4137
4149
 
4138
4150
  // src/hook/master/routing/worker-diagnostics-registry.ts
4139
- import fs6 from "fs";
4151
+ import fs5 from "fs";
4140
4152
  import path5 from "path";
4141
4153
  function normalize3(value) {
4142
4154
  return String(value || "").trim();
@@ -4161,12 +4173,12 @@ var WorkerDiagnosticsRegistry = class {
4161
4173
  }
4162
4174
  load() {
4163
4175
  this.workers.clear();
4164
- if (!fs6.existsSync(this.filePath)) {
4176
+ if (!fs5.existsSync(this.filePath)) {
4165
4177
  this.save();
4166
4178
  return;
4167
4179
  }
4168
4180
  try {
4169
- const parsed = JSON.parse(fs6.readFileSync(this.filePath, "utf-8") || "{}");
4181
+ const parsed = JSON.parse(fs5.readFileSync(this.filePath, "utf-8") || "{}");
4170
4182
  for (const [workerId, diagnostics] of Object.entries(safeObject(parsed.workers))) {
4171
4183
  const normalizedWorkerId = normalize3(diagnostics.workerId || workerId);
4172
4184
  const registryKey = buildRegistryKey({
@@ -4198,12 +4210,12 @@ var WorkerDiagnosticsRegistry = class {
4198
4210
  }
4199
4211
  }
4200
4212
  save() {
4201
- fs6.mkdirSync(path5.dirname(this.filePath), { recursive: true });
4213
+ fs5.mkdirSync(path5.dirname(this.filePath), { recursive: true });
4202
4214
  const workers = {};
4203
4215
  for (const [key, diagnostics] of this.workers.entries()) {
4204
4216
  workers[key] = diagnostics;
4205
4217
  }
4206
- fs6.writeFileSync(
4218
+ fs5.writeFileSync(
4207
4219
  this.filePath,
4208
4220
  `${JSON.stringify({ version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), workers }, null, 2)}
4209
4221
  `,
@@ -4252,7 +4264,7 @@ var WorkerDiagnosticsRegistry = class {
4252
4264
  };
4253
4265
 
4254
4266
  // src/hook/master/master-event-utils.ts
4255
- import fs7 from "fs";
4267
+ import fs6 from "fs";
4256
4268
  import os3 from "os";
4257
4269
  import path6 from "path";
4258
4270
  var VOPS_USER_CACHE_PATH = path6.join(os3.homedir(), "ops", "vops_user.json");
@@ -4339,11 +4351,11 @@ function isTaskOperatorMatched(task, operatorUserId, operatorOpenId) {
4339
4351
  function resolveSenderNameFromCache(senderUserId, senderOpenId = "") {
4340
4352
  const targetUserId = String(senderUserId || "").trim();
4341
4353
  const targetOpenId = String(senderOpenId || "").trim();
4342
- if (!targetUserId && !targetOpenId || !fs7.existsSync(VOPS_USER_CACHE_PATH)) {
4354
+ if (!targetUserId && !targetOpenId || !fs6.existsSync(VOPS_USER_CACHE_PATH)) {
4343
4355
  return "";
4344
4356
  }
4345
4357
  try {
4346
- const parsed = JSON.parse(fs7.readFileSync(VOPS_USER_CACHE_PATH, "utf-8"));
4358
+ const parsed = JSON.parse(fs6.readFileSync(VOPS_USER_CACHE_PATH, "utf-8"));
4347
4359
  const entries = Array.isArray(parsed) ? parsed : [];
4348
4360
  const matched = entries.find((item) => {
4349
4361
  if (!item || typeof item !== "object" || item.__cacheType) {
@@ -5231,7 +5243,7 @@ var HookMasterCommand = class {
5231
5243
  if (masterConfig.botName) {
5232
5244
  this.botName = masterConfig.botName;
5233
5245
  }
5234
- const configStat = fs8.statSync(configPath);
5246
+ const configStat = fs7.statSync(configPath);
5235
5247
  this.bindWhitelistConfigMtimeMs = Number(configStat.mtimeMs || 0);
5236
5248
  this.chatBindingRegistry.load();
5237
5249
  this.workerDiagnosticsRegistry.load();
@@ -5356,9 +5368,17 @@ var HookMasterCommand = class {
5356
5368
  const bindCmd = parseBindingCommand(text);
5357
5369
  if (bindCmd.matched) {
5358
5370
  this.logger.info(formatMasterTraceLog(messageId, `\u4E3B\u8282\u70B9\u547D\u4E2D\u7ED1\u5B9A\u6307\u4EE4: chat_id=${chatId}, target=${bindCmd.target}`));
5359
- if (!this.handleBindCommand(chatId, messageId, senderId, bindCmd.target)) {
5360
- return;
5361
- }
5371
+ await this.handleBindCommand({
5372
+ data,
5373
+ message,
5374
+ chatId,
5375
+ chatType,
5376
+ messageId,
5377
+ senderId,
5378
+ senderOpenId,
5379
+ target: bindCmd.target
5380
+ });
5381
+ return;
5362
5382
  }
5363
5383
  const stopCmd = parseStopCommand(text);
5364
5384
  if (stopCmd.matched) {
@@ -5378,8 +5398,7 @@ var HookMasterCommand = class {
5378
5398
  chatType,
5379
5399
  senderId,
5380
5400
  senderOpenId,
5381
- text,
5382
- isBindingCommand: bindCmd.matched
5401
+ text
5383
5402
  });
5384
5403
  }
5385
5404
  async dispatchFeishuTask(input) {
@@ -5409,10 +5428,6 @@ var HookMasterCommand = class {
5409
5428
  } else {
5410
5429
  this.logger.warn(formatMasterTraceLog(input.messageId, `\u5355\u804A\u4EFB\u52A1\u672A\u5339\u914D\u5230\u4ECE\u8282\u70B9: task_id=${task.taskId}, requester=${input.senderId || "-"}, chat_id=${input.chatId || "-"}`));
5411
5430
  }
5412
- } else if (input.isBindingCommand) {
5413
- this.wsServer.broadcast(buildTaskOffer(task, eventKey, input.data));
5414
- delivered = this.wsServer.getWorkerCount();
5415
- this.logger.info(formatMasterTraceLog(input.messageId, `\u7ED1\u5B9A\u6307\u4EE4\u5DF2\u5E7F\u64AD\u7ED9\u4ECE\u8282\u70B9\u5339\u914D\u76EE\u5F55: task_id=${task.taskId}, chat_id=${input.chatId || "-"}, delivered=${delivered}, message_id=${input.messageId}`));
5416
5431
  } else {
5417
5432
  await this.groupRouteDispatcher?.dispatch(task, input.text);
5418
5433
  return;
@@ -5433,16 +5448,54 @@ var HookMasterCommand = class {
5433
5448
  () => this.messageResponder?.replyNoWorker(task)
5434
5449
  );
5435
5450
  }
5436
- handleBindCommand(chatId, messageId, senderId, target) {
5451
+ async handleBindCommand(input) {
5452
+ const { chatId, messageId, senderId, target } = input;
5453
+ const replyInThread = resolveReplyInThread(input.message);
5437
5454
  if (this.bindAllowedUserIds.length > 0 && !this.bindAllowedUserIds.includes(senderId)) {
5438
- void this.messageResponder?.replyText(chatId, "\u7ED1\u5B9A\u5931\u8D25\uFF1A\u5F53\u524D\u7528\u6237\u4E0D\u5728\u7ED1\u5B9A\u767D\u540D\u5355", messageId, resolveReplyInThread({
5439
- chat_id: chatId,
5440
- message_id: messageId
5441
- }));
5442
- return false;
5455
+ await this.messageResponder?.replyText(chatId, "\u7ED1\u5B9A\u5931\u8D25\uFF1A\u5F53\u524D\u7528\u6237\u4E0D\u5728\u7ED1\u5B9A\u767D\u540D\u5355", messageId, replyInThread);
5456
+ return;
5443
5457
  }
5444
- this.logger.info(formatMasterTraceLog(messageId, `\u7ED1\u5B9A\u6307\u4EE4\u6821\u9A8C\u901A\u8FC7\uFF0C\u7EE7\u7EED\u4E0B\u53D1\u5230\u4ECE\u8282\u70B9: chat_id=${chatId}, target=${target}`));
5445
- return true;
5458
+ if (!target) {
5459
+ await this.messageResponder?.replyText(chatId, "\u7ED1\u5B9A\u5931\u8D25\uFF1A\u8BF7\u6307\u5B9A\u7ED1\u5B9A\u8DEF\u5F84", messageId, replyInThread);
5460
+ return;
5461
+ }
5462
+ const existing = this.chatBindingRegistry.getNormalBinding(chatId, senderId, target);
5463
+ if (existing) {
5464
+ this.logger.info(formatMasterTraceLog(messageId, `\u4E3B\u8282\u70B9\u547D\u4E2D\u5DF2\u6709\u7FA4\u804A\u7ED1\u5B9A: chat_id=${chatId}, worker=${existing.workerId}, owner_user_id=${senderId}, dir=${target}`));
5465
+ await this.messageResponder?.replyText(chatId, `\u5DF2\u7ED1\u5B9A\u5230: ${target}`, messageId, replyInThread);
5466
+ return;
5467
+ }
5468
+ if (!this.taskManager || !this.wsServer) {
5469
+ return;
5470
+ }
5471
+ const workerId = this.workerRegistry.getWorkerIdByOwner(senderId);
5472
+ if (!workerId) {
5473
+ this.logger.warn(formatMasterTraceLog(messageId, `\u7ED1\u5B9A\u5931\u8D25\uFF0C\u672A\u627E\u5230\u53D1\u8D77\u4EBA\u7684\u5728\u7EBF\u4ECE\u8282\u70B9: chat_id=${chatId}, owner_user_id=${senderId}, dir=${target}`));
5474
+ await this.messageResponder?.replyText(chatId, "\u7ED1\u5B9A\u5931\u8D25\uFF1A\u672A\u627E\u5230\u4F60\u7684\u5728\u7EBF\u4ECE\u8282\u70B9\uFF0C\u8BF7\u5148\u542F\u52A8 ai-hook", messageId, replyInThread);
5475
+ return;
5476
+ }
5477
+ const eventKey = "im.message.receive_v1";
5478
+ const task = this.taskManager.createTaskRecord(input.data, messageId, eventKey, {
5479
+ chatId,
5480
+ chatType: input.chatType,
5481
+ requesterUserId: senderId,
5482
+ requesterOpenId: input.senderOpenId,
5483
+ responseMode: "card",
5484
+ routeMode: "direct_group",
5485
+ replyInThread
5486
+ });
5487
+ task.targetWorkerId = workerId;
5488
+ const delivered = this.wsServer.sendTo(workerId, buildTaskOffer(task, eventKey, input.data));
5489
+ if (!delivered) {
5490
+ this.taskManager.cleanupTask(task.taskId);
5491
+ await this.messageResponder?.replyText(chatId, "\u7ED1\u5B9A\u5931\u8D25\uFF1A\u4F60\u7684\u4ECE\u8282\u70B9\u5F53\u524D\u4E0D\u53EF\u7528\uFF0C\u8BF7\u91CD\u65B0\u542F\u52A8 ai-hook", messageId, replyInThread);
5492
+ return;
5493
+ }
5494
+ this.logger.info(formatMasterTraceLog(messageId, `\u7ED1\u5B9A\u6307\u4EE4\u5DF2\u5B9A\u5411\u4E0B\u53D1: task_id=${task.taskId}, chat_id=${chatId}, owner_user_id=${senderId}, worker=${workerId}, dir=${target}`));
5495
+ this.taskManager.scheduleClaimTimeout(
5496
+ task.taskId,
5497
+ () => this.messageResponder?.replyText(chatId, "\u7ED1\u5B9A\u5931\u8D25\uFF1A\u4ECE\u8282\u70B9\u54CD\u5E94\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5", messageId, replyInThread)
5498
+ );
5446
5499
  }
5447
5500
  async handleBotMenuEvent(data) {
5448
5501
  const event = data?.event || data;
@@ -5753,8 +5806,8 @@ var HookMasterCommand = class {
5753
5806
  }
5754
5807
  refreshBindWhitelistIfNeeded() {
5755
5808
  try {
5756
- if (!this.configPath || !fs8.existsSync(this.configPath)) return;
5757
- const stat = fs8.statSync(this.configPath);
5809
+ if (!this.configPath || !fs7.existsSync(this.configPath)) return;
5810
+ const stat = fs7.statSync(this.configPath);
5758
5811
  const mtimeMs = Number(stat.mtimeMs || 0);
5759
5812
  if (!mtimeMs || mtimeMs <= this.bindWhitelistConfigMtimeMs) return;
5760
5813
  const { config } = loadMasterHstMcpConfig();
@@ -5836,11 +5889,11 @@ var HookMasterCommand = class {
5836
5889
  };
5837
5890
 
5838
5891
  // src/hook/logger.ts
5839
- import fs9 from "fs";
5892
+ import fs8 from "fs";
5840
5893
  import path9 from "path";
5841
5894
  function ensureLogsDir() {
5842
- if (!fs9.existsSync(LOGS_DIR)) {
5843
- fs9.mkdirSync(LOGS_DIR, { recursive: true });
5895
+ if (!fs8.existsSync(LOGS_DIR)) {
5896
+ fs8.mkdirSync(LOGS_DIR, { recursive: true });
5844
5897
  }
5845
5898
  }
5846
5899
  function todaySuffix() {
@@ -5851,8 +5904,8 @@ function timestamp() {
5851
5904
  }
5852
5905
  function appendLine(filePath, line) {
5853
5906
  try {
5854
- fs9.mkdirSync(path9.dirname(filePath), { recursive: true });
5855
- fs9.appendFileSync(filePath, line + "\n", "utf-8");
5907
+ fs8.mkdirSync(path9.dirname(filePath), { recursive: true });
5908
+ fs8.appendFileSync(filePath, line + "\n", "utf-8");
5856
5909
  } catch {
5857
5910
  }
5858
5911
  }
@@ -5877,22 +5930,22 @@ function resolveServiceLogName(role) {
5877
5930
  }
5878
5931
  function ensureCurrentLogLink(currentPath, dailyPath) {
5879
5932
  try {
5880
- fs9.mkdirSync(path9.dirname(currentPath), { recursive: true });
5881
- fs9.mkdirSync(path9.dirname(dailyPath), { recursive: true });
5882
- fs9.writeFileSync(dailyPath, "", { flag: "a" });
5883
- if (fs9.existsSync(currentPath)) {
5884
- const stat = fs9.lstatSync(currentPath);
5933
+ fs8.mkdirSync(path9.dirname(currentPath), { recursive: true });
5934
+ fs8.mkdirSync(path9.dirname(dailyPath), { recursive: true });
5935
+ fs8.writeFileSync(dailyPath, "", { flag: "a" });
5936
+ if (fs8.existsSync(currentPath)) {
5937
+ const stat = fs8.lstatSync(currentPath);
5885
5938
  if (!stat.isSymbolicLink()) {
5886
5939
  const legacyDir = path9.join(path9.dirname(currentPath), "logs", "legacy");
5887
- fs9.mkdirSync(legacyDir, { recursive: true });
5940
+ fs8.mkdirSync(legacyDir, { recursive: true });
5888
5941
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5889
- fs9.renameSync(currentPath, path9.join(legacyDir, `${path9.basename(currentPath)}.${stamp}`));
5942
+ fs8.renameSync(currentPath, path9.join(legacyDir, `${path9.basename(currentPath)}.${stamp}`));
5890
5943
  }
5891
5944
  }
5892
- fs9.rmSync(currentPath, { force: true });
5893
- fs9.symlinkSync(dailyPath, currentPath);
5945
+ fs8.rmSync(currentPath, { force: true });
5946
+ fs8.symlinkSync(dailyPath, currentPath);
5894
5947
  } catch {
5895
- fs9.writeFileSync(currentPath, "", { flag: "a" });
5948
+ fs8.writeFileSync(currentPath, "", { flag: "a" });
5896
5949
  }
5897
5950
  }
5898
5951
  function createLogger(name, verbose = false) {
@@ -5957,12 +6010,15 @@ import os17 from "os";
5957
6010
  import path27 from "path";
5958
6011
  import * as lark3 from "@larksuiteoapi/node-sdk";
5959
6012
 
6013
+ // src/hook/worker/message-strategy.ts
6014
+ import fs14 from "fs";
6015
+
5960
6016
  // src/hook/worker/runners/codex-runner.ts
5961
6017
  import process7 from "process";
5962
6018
  import { spawn, spawnSync as spawnSync4 } from "child_process";
5963
6019
 
5964
6020
  // src/hook/runtime-env.ts
5965
- import fs10 from "fs";
6021
+ import fs9 from "fs";
5966
6022
  import process5 from "process";
5967
6023
  import { spawnSync as spawnSync2 } from "child_process";
5968
6024
  var HST_MCP_DISABLED_MODULES_ENV_KEY = "HST_MCP_DISABLED_MODULES";
@@ -6007,11 +6063,11 @@ function parseEnvFileContent(content = "") {
6007
6063
  return env;
6008
6064
  }
6009
6065
  function loadRuntimeEnvFile(filePath = RUNTIME_ENV_PATH) {
6010
- if (!filePath || !fs10.existsSync(filePath)) {
6066
+ if (!filePath || !fs9.existsSync(filePath)) {
6011
6067
  return {};
6012
6068
  }
6013
6069
  try {
6014
- return parseEnvFileContent(fs10.readFileSync(filePath, "utf-8"));
6070
+ return parseEnvFileContent(fs9.readFileSync(filePath, "utf-8"));
6015
6071
  } catch {
6016
6072
  return {};
6017
6073
  }
@@ -6023,7 +6079,7 @@ function resolveUnixShell(baseEnv = process5.env) {
6023
6079
  "/bin/bash",
6024
6080
  "/bin/sh"
6025
6081
  ].filter(Boolean);
6026
- return candidates.find((item) => fs10.existsSync(item)) || "/bin/sh";
6082
+ return candidates.find((item) => fs9.existsSync(item)) || "/bin/sh";
6027
6083
  }
6028
6084
  function parseNullSeparatedEnv(output = "") {
6029
6085
  const env = {};
@@ -6144,23 +6200,23 @@ function buildHookRuntimeEnv(baseEnv = process5.env, {
6144
6200
  }
6145
6201
 
6146
6202
  // src/hook/worker/runners/command-resolver.ts
6147
- import fs11 from "fs";
6203
+ import fs10 from "fs";
6148
6204
  import path10 from "path";
6149
6205
  import process6 from "process";
6150
6206
  import { spawnSync as spawnSync3 } from "child_process";
6151
6207
  function isExecutableFile(filePath) {
6152
- if (!filePath || !fs11.existsSync(filePath)) {
6208
+ if (!filePath || !fs10.existsSync(filePath)) {
6153
6209
  return false;
6154
6210
  }
6155
6211
  try {
6156
- const stat = fs11.statSync(filePath);
6212
+ const stat = fs10.statSync(filePath);
6157
6213
  if (!stat.isFile()) {
6158
6214
  return false;
6159
6215
  }
6160
6216
  if (process6.platform === "win32") {
6161
6217
  return true;
6162
6218
  }
6163
- fs11.accessSync(filePath, fs11.constants.X_OK);
6219
+ fs10.accessSync(filePath, fs10.constants.X_OK);
6164
6220
  return true;
6165
6221
  } catch {
6166
6222
  return false;
@@ -6705,7 +6761,7 @@ command=${execConfig.displayCommand}`));
6705
6761
  };
6706
6762
 
6707
6763
  // src/hook/context/identity-resolver.ts
6708
- import fs12 from "fs";
6764
+ import fs11 from "fs";
6709
6765
  import os5 from "os";
6710
6766
  import path11 from "path";
6711
6767
 
@@ -6910,12 +6966,12 @@ var IdentityResolver = class {
6910
6966
  if (Array.isArray(this.cache)) {
6911
6967
  return this.cache;
6912
6968
  }
6913
- if (!fs12.existsSync(this.cachePath)) {
6969
+ if (!fs11.existsSync(this.cachePath)) {
6914
6970
  this.cache = [];
6915
6971
  return this.cache;
6916
6972
  }
6917
6973
  try {
6918
- const parsed = JSON.parse(fs12.readFileSync(this.cachePath, "utf-8"));
6974
+ const parsed = JSON.parse(fs11.readFileSync(this.cachePath, "utf-8"));
6919
6975
  const input = Array.isArray(parsed) ? parsed : [];
6920
6976
  this.cache = this.dedupeCacheEntries(input);
6921
6977
  if (this.cache.length !== input.length) {
@@ -6932,7 +6988,7 @@ var IdentityResolver = class {
6932
6988
  saveCache() {
6933
6989
  try {
6934
6990
  this.cache = this.dedupeCacheEntries(this.cache || []);
6935
- fs12.writeFileSync(this.cachePath, JSON.stringify(this.cache, null, 2));
6991
+ fs11.writeFileSync(this.cachePath, JSON.stringify(this.cache, null, 2));
6936
6992
  } catch (error) {
6937
6993
  const message = error instanceof Error ? error.message : String(error);
6938
6994
  this.logger.warn(`\u5199\u5165 vops_user.json \u5931\u8D25: ${message}`);
@@ -7265,7 +7321,7 @@ var IdentityResolver = class {
7265
7321
  };
7266
7322
 
7267
7323
  // src/hook/prompt/nikou-prompt-store.ts
7268
- import fs13 from "fs";
7324
+ import fs12 from "fs";
7269
7325
  import os6 from "os";
7270
7326
  import path12 from "path";
7271
7327
 
@@ -7320,7 +7376,7 @@ function normalizeText2(value) {
7320
7376
  return String(value || "").trim();
7321
7377
  }
7322
7378
  function ensureDir(dirPath) {
7323
- fs13.mkdirSync(dirPath, { recursive: true });
7379
+ fs12.mkdirSync(dirPath, { recursive: true });
7324
7380
  }
7325
7381
  function parseFrontmatter(content) {
7326
7382
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
@@ -7367,18 +7423,18 @@ ${body}
7367
7423
  ` : "\n"}`;
7368
7424
  }
7369
7425
  function readTextFile(filePath) {
7370
- if (!fs13.existsSync(filePath)) {
7426
+ if (!fs12.existsSync(filePath)) {
7371
7427
  return "";
7372
7428
  }
7373
7429
  try {
7374
- return fs13.readFileSync(filePath, "utf-8").trim();
7430
+ return fs12.readFileSync(filePath, "utf-8").trim();
7375
7431
  } catch {
7376
7432
  return "";
7377
7433
  }
7378
7434
  }
7379
7435
  function writeTextFile(filePath, content) {
7380
7436
  ensureDir(path12.dirname(filePath));
7381
- fs13.writeFileSync(filePath, content, "utf-8");
7437
+ fs12.writeFileSync(filePath, content, "utf-8");
7382
7438
  }
7383
7439
  function buildPromptDefinitions() {
7384
7440
  const roleDefinitions = Object.entries(REQUESTER_PERSONA_RULES.roles).map(([roleKey, roleConfig]) => ({
@@ -7439,7 +7495,7 @@ var NikouPromptStore = class {
7439
7495
  return definition.defaultContent.trim();
7440
7496
  }
7441
7497
  readPromptFile(definition) {
7442
- if (!fs13.existsSync(definition.path)) {
7498
+ if (!fs12.existsSync(definition.path)) {
7443
7499
  const content = this.resolveBootstrapContent(definition);
7444
7500
  const file = {
7445
7501
  title: definition.title,
@@ -7450,7 +7506,7 @@ var NikouPromptStore = class {
7450
7506
  this.syncLegacyFiles(definition, file);
7451
7507
  return file;
7452
7508
  }
7453
- return parseFrontmatter(fs13.readFileSync(definition.path, "utf-8"));
7509
+ return parseFrontmatter(fs12.readFileSync(definition.path, "utf-8"));
7454
7510
  }
7455
7511
  syncLegacyFiles(definition, file) {
7456
7512
  if (!definition.legacyPaths.length) {
@@ -7463,7 +7519,7 @@ var NikouPromptStore = class {
7463
7519
  }
7464
7520
  }
7465
7521
  toSummary(definition, file) {
7466
- const stat = fs13.existsSync(definition.path) ? fs13.statSync(definition.path) : null;
7522
+ const stat = fs12.existsSync(definition.path) ? fs12.statSync(definition.path) : null;
7467
7523
  return {
7468
7524
  id: definition.id,
7469
7525
  kind: definition.kind,
@@ -7544,7 +7600,7 @@ var NikouPromptStore = class {
7544
7600
  };
7545
7601
 
7546
7602
  // src/hook/worker/task-route-binding.ts
7547
- import fs14 from "fs";
7603
+ import fs13 from "fs";
7548
7604
  import path13 from "path";
7549
7605
  function resolveTaskRouteDir(routeBinding, fallbackDir) {
7550
7606
  if (routeBinding) {
@@ -7553,7 +7609,7 @@ function resolveTaskRouteDir(routeBinding, fallbackDir) {
7553
7609
  return { dirPath: "", reason: "invalid_route_dir" };
7554
7610
  }
7555
7611
  try {
7556
- if (!fs14.statSync(dirPath2).isDirectory()) {
7612
+ if (!fs13.statSync(dirPath2).isDirectory()) {
7557
7613
  return { dirPath: "", reason: "invalid_route_dir" };
7558
7614
  }
7559
7615
  } catch {
@@ -8323,10 +8379,9 @@ ${question}` : question;
8323
8379
  if (!bindTarget) {
8324
8380
  return { ...baseTask, canHandle: true, type: "quick_result", quickResult: "\u8BF7\u6307\u5B9A\u7ED1\u5B9A\u8DEF\u5F84\uFF0C\u683C\u5F0F: \u7ED1\u5B9A:/path/to/project" };
8325
8381
  }
8326
- if (!fs.existsSync(bindTarget)) {
8382
+ if (!fs14.existsSync(bindTarget)) {
8327
8383
  return { ...baseTask, canHandle: true, type: "quick_result", quickResult: `\u8DEF\u5F84\u4E0D\u5B58\u5728: ${bindTarget}` };
8328
8384
  }
8329
- this.stateManager.rebindChatToDir(message.chatId, bindTarget);
8330
8385
  const bindAt = (/* @__PURE__ */ new Date()).toISOString();
8331
8386
  const pathDisplay = resolveCardPathDisplay(bindTarget);
8332
8387
  this.logger.info(formatTraceLog(baseTask.traceId, `\u7ED1\u5B9A\u6210\u529F: chatId=${message.chatId} -> ${bindTarget}`));
@@ -9759,7 +9814,15 @@ var SlaveRelayClient = class {
9759
9814
  this.resolvePendingClaim(packet.task_id, false);
9760
9815
  break;
9761
9816
  case "task_offer":
9762
- this.handleTaskOffer(packet);
9817
+ void this.handleTaskOffer(packet).catch((error) => {
9818
+ const taskId = String(packet.task_id || "").trim();
9819
+ const traceId = resolveTraceId(taskId);
9820
+ const message = error instanceof Error ? error.message : String(error);
9821
+ this.logger.error(formatTraceLog(
9822
+ traceId,
9823
+ `\u5904\u7406\u4EFB\u52A1 offer \u5931\u8D25: task_id=${taskId || "-"}, error=${message}`
9824
+ ));
9825
+ });
9763
9826
  break;
9764
9827
  case "task_cancel": {
9765
9828
  const cancelResult = this.taskExecutor.cancelTask(packet.task_id);
@@ -9945,6 +10008,7 @@ var SlaveRelayClient = class {
9945
10008
  if (!chatId || !dirPath) {
9946
10009
  return;
9947
10010
  }
10011
+ this.stateManager.rebindChatToDir(chatId, dirPath);
9948
10012
  this.send({
9949
10013
  type: "binding_update",
9950
10014
  worker_id: this.workerId,
@@ -14000,6 +14064,7 @@ import { execFile, spawn as spawn4 } from "child_process";
14000
14064
  import { createRequire } from "module";
14001
14065
  import { promisify } from "util";
14002
14066
  var execFileAsync = promisify(execFile);
14067
+ var MISSING_CLI_AUTH_SECRET_ERROR = "\u672A\u914D\u7F6E CLI_AUTH_SECRET\uFF0C\u65E0\u6CD5\u62C9\u53D6 Nacos \u540C\u6B65\u8BA1\u5212";
14003
14068
  var NacosSubscriptionSyncCoordinator = class {
14004
14069
  logger;
14005
14070
  workerId;
@@ -14008,12 +14073,15 @@ var NacosSubscriptionSyncCoordinator = class {
14008
14073
  daemon = null;
14009
14074
  running = false;
14010
14075
  stopped = false;
14076
+ missingCliAuthSecretWarned = false;
14011
14077
  status;
14078
+ resolveAuthRuntimeConfig;
14012
14079
  statePath = path26.join(os16.homedir(), ".nikou-block", "worker", "nacos-subscriptions.json");
14013
14080
  constructor(options) {
14014
14081
  this.logger = options.logger;
14015
14082
  this.workerId = options.workerId;
14016
14083
  this.config = options.config;
14084
+ this.resolveAuthRuntimeConfig = options.resolveAuthRuntimeConfig || resolveCliAuthRuntimeConfig;
14017
14085
  this.status = {
14018
14086
  enabled: this.config.enabled,
14019
14087
  configured: false,
@@ -14081,15 +14149,20 @@ var NacosSubscriptionSyncCoordinator = class {
14081
14149
  } catch (error) {
14082
14150
  const message = error instanceof Error ? error.message : String(error);
14083
14151
  this.status.lastError = message;
14084
- this.logger.warn(`Nacos Skill \u8BA2\u9605\u540C\u6B65\u5931\u8D25: ${message}`);
14152
+ if (message !== MISSING_CLI_AUTH_SECRET_ERROR || !this.missingCliAuthSecretWarned) {
14153
+ this.logger.warn(`Nacos Skill \u8BA2\u9605\u540C\u6B65\u5931\u8D25: ${message}`);
14154
+ }
14155
+ if (message === MISSING_CLI_AUTH_SECRET_ERROR) {
14156
+ this.missingCliAuthSecretWarned = true;
14157
+ }
14085
14158
  } finally {
14086
14159
  this.running = false;
14087
14160
  }
14088
14161
  }
14089
14162
  async fetchPlan() {
14090
- const runtime = resolveCliAuthRuntimeConfig();
14163
+ const runtime = this.resolveAuthRuntimeConfig();
14091
14164
  if (!runtime.cliAuthSecret) {
14092
- throw new Error("\u672A\u914D\u7F6E CLI_AUTH_SECRET\uFF0C\u65E0\u6CD5\u62C9\u53D6 Nacos \u540C\u6B65\u8BA1\u5212");
14165
+ throw new Error(MISSING_CLI_AUTH_SECRET_ERROR);
14093
14166
  }
14094
14167
  const response = await fetch(`${runtime.apiUrl}/worker-skills/nacos-sync/plan`, {
14095
14168
  method: "POST",
@@ -14104,7 +14177,7 @@ var NacosSubscriptionSyncCoordinator = class {
14104
14177
  }
14105
14178
  async report(results) {
14106
14179
  if (!results.length) return;
14107
- const runtime = resolveCliAuthRuntimeConfig();
14180
+ const runtime = this.resolveAuthRuntimeConfig();
14108
14181
  const response = await fetch(`${runtime.apiUrl}/worker-skills/nacos-sync/report`, {
14109
14182
  method: "POST",
14110
14183
  headers: { "Content-Type": "application/json" },
@@ -14839,7 +14912,9 @@ function normalizeHookInitConfig(config, defaults = {}) {
14839
14912
  if (!appSecret) missing.push("feishu.app_secret");
14840
14913
  if (!masterHost) missing.push(`hook.master_host \u6216 ${DEFAULT_MASTER_HOST_ENV}`);
14841
14914
  if (!sharedSecret) missing.push("hook.shared_secret");
14842
- if (bindAllowedUserIds.length === 0) missing.push("hook.bind_allowed_user_ids");
14915
+ if (defaults.deploymentMode !== "slave" && bindAllowedUserIds.length === 0) {
14916
+ missing.push("hook.bind_allowed_user_ids");
14917
+ }
14843
14918
  if (missing.length > 0) {
14844
14919
  throw new Error(`\u521D\u59CB\u5316\u6240\u9700\u914D\u7F6E\u7F3A\u5931: ${missing.join(", ")}`);
14845
14920
  }
@@ -14897,7 +14972,8 @@ var HookInitCommand = class {
14897
14972
  const defaultMasterHost = deploymentMode === "local" ? LOCAL_MASTER_HOST : deploymentDefaults.masterHost;
14898
14973
  const masterHost = await this.askMasterHost(rl, source.config, defaultMasterHost);
14899
14974
  const normalized = this.normalizeSourceConfig(
14900
- mergeHookInitMasterHost(source.config, masterHost)
14975
+ mergeHookInitMasterHost(source.config, masterHost),
14976
+ deploymentMode
14901
14977
  );
14902
14978
  const nextMasterConfig = this.buildMasterConfigPayload(normalized, botName);
14903
14979
  const nextWorkerConfig = this.buildWorkerConfigPayload(normalized, botName);
@@ -14969,9 +15045,10 @@ var HookInitCommand = class {
14969
15045
  }
14970
15046
  throw new Error(`\u672A\u627E\u5230\u53EF\u521D\u59CB\u5316\u7684\u914D\u7F6E\u6E90\uFF0C\u8BF7\u5148\u51C6\u5907 ${LEGACY_CONFIG_PATH}\u3001${DEFAULT_CONFIG_PATH} \u6216\u89D2\u8272\u914D\u7F6E\u6587\u4EF6`);
14971
15047
  }
14972
- normalizeSourceConfig(config) {
15048
+ normalizeSourceConfig(config, deploymentMode) {
14973
15049
  return normalizeHookInitConfig(config, {
14974
- defaultMasterHost: resolveDefaultMasterHost()
15050
+ defaultMasterHost: resolveDefaultMasterHost(),
15051
+ deploymentMode
14975
15052
  });
14976
15053
  }
14977
15054
  printEnvironmentSummary(sourcePath, existingConfig, migrationSummary) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nikou-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Install Agent Skills and MCP servers, and run distributed AI hook workers.",
5
5
  "type": "module",
6
6
  "bin": {