@dhf-openclaw/grix 0.4.11 → 0.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenClaw Grix 插件
2
2
 
3
- 把 OpenClaw 接到 Grix(`grix.dhf.pub`)的统一插件,包含:
3
+ 把 OpenClaw 接到 Grix 服务的统一插件,默认支持正式环境,也支持本地开发地址,包含:
4
4
 
5
5
  - Grix Channel(收发消息、流式回复、`unsend`、`delete`)
6
6
  - 管理工具:`grix_query`、`grix_group`、`grix_agent_admin`
@@ -28,7 +28,7 @@ openclaw plugins enable grix
28
28
  openclaw channels add \
29
29
  --channel grix \
30
30
  --name grix-main \
31
- --http-url "wss://grix.dhf.pub/v1/agent-api/ws?agent_id={agent_id}" \
31
+ --http-url "wss://<YOUR_GRIX_HOST>/v1/agent-api/ws?agent_id={agent_id}" \
32
32
  --user-id "<YOUR_AGENT_ID>" \
33
33
  --token "<YOUR_API_KEY>"
34
34
  ```
@@ -85,12 +85,31 @@ openclaw skills list
85
85
 
86
86
  ### 最小可用配置(单账户)
87
87
 
88
+ 线上发布示例:
89
+
90
+ ```json
91
+ {
92
+ "channels": {
93
+ "grix": {
94
+ "enabled": true,
95
+ "wsUrl": "wss://<YOUR_GRIX_HOST>/v1/agent-api/ws?agent_id=<YOUR_AGENT_ID>",
96
+ "apiBaseUrl": "https://<YOUR_GRIX_HOST>/v1/agent-api",
97
+ "agentId": "<YOUR_AGENT_ID>",
98
+ "apiKey": "<YOUR_API_KEY>"
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ 本地开发示例:
105
+
88
106
  ```json
89
107
  {
90
108
  "channels": {
91
109
  "grix": {
92
110
  "enabled": true,
93
- "wsUrl": "wss://grix.dhf.pub/v1/agent-api/ws?agent_id=<YOUR_AGENT_ID>",
111
+ "wsUrl": "ws://127.0.0.1:27189/v1/agent-api/ws?agent_id=<YOUR_AGENT_ID>",
112
+ "apiBaseUrl": "http://127.0.0.1:27180/v1/agent-api",
94
113
  "agentId": "<YOUR_AGENT_ID>",
95
114
  "apiKey": "<YOUR_API_KEY>"
96
115
  }
@@ -110,13 +129,15 @@ openclaw skills list
110
129
  "ops": {
111
130
  "enabled": true,
112
131
  "name": "Ops",
113
- "wsUrl": "wss://grix.dhf.pub/v1/agent-api/ws?agent_id=<OPS_AGENT_ID>",
132
+ "wsUrl": "ws://127.0.0.1:27189/v1/agent-api/ws?agent_id=<OPS_AGENT_ID>",
133
+ "apiBaseUrl": "http://127.0.0.1:27180/v1/agent-api",
114
134
  "agentId": "<OPS_AGENT_ID>",
115
135
  "apiKey": "<OPS_API_KEY>"
116
136
  },
117
137
  "prod": {
118
138
  "enabled": true,
119
- "wsUrl": "wss://grix.dhf.pub/v1/agent-api/ws?agent_id=<PROD_AGENT_ID>",
139
+ "wsUrl": "wss://<YOUR_GRIX_HOST>/v1/agent-api/ws?agent_id=<PROD_AGENT_ID>",
140
+ "apiBaseUrl": "https://<YOUR_GRIX_HOST>/v1/agent-api",
120
141
  "agentId": "<PROD_AGENT_ID>",
121
142
  "apiKey": "<PROD_API_KEY>"
122
143
  }
@@ -135,6 +156,7 @@ openclaw skills list
135
156
  | `accounts.<id>` | 否 | - | 多账户配置项,`<id>` 自定义(如 `ops`)。 |
136
157
  | `name` | 否 | - | 账户显示名。 |
137
158
  | `wsUrl` | 是(可用环境变量兜底) | `ws://127.0.0.1:27189/...`(当有 `agentId` 且未填时) | Grix WebSocket 地址。 |
159
+ | `apiBaseUrl` | 否(可用环境变量兜底) | 优先使用显式配置;否则按同一账号的 `wsUrl` 推导;本地 `ws://127.0.0.1:27189/...` 会默认映射成 `http://127.0.0.1:27180/v1/agent-api`;只有账号自己既没配 `apiBaseUrl` 也没配 `wsUrl` 时,才回退环境变量 | Grix HTTP API 地址。开发时可单独指向本地后端。 |
138
160
  | `agentId` | 是(可用环境变量兜底) | - | Grix agent ID。 |
139
161
  | `apiKey` | 是(可用环境变量兜底) | - | Grix API Key。 |
140
162
  | `reconnectMs` | 否 | `2000` | 重连基础延迟(毫秒)。 |
@@ -160,17 +182,50 @@ openclaw skills list
160
182
  如果配置文件没填,插件会按下列环境变量读取:
161
183
 
162
184
  - `GRIX_WS_URL`
185
+ - `GRIX_AGENT_API_BASE`
186
+ - `GRIX_WEB_BASE_URL`
163
187
  - `GRIX_AGENT_ID`
164
188
  - `GRIX_API_KEY`
165
189
 
190
+ 注册脚本默认使用正式环境地址;如果要切到本地或其他部署,可额外设置:
191
+
192
+ - `GRIX_WEB_BASE_URL`
193
+
194
+ 说明:
195
+
196
+ - `grix_query`、`grix_group`、`grix_agent_admin` 这些 HTTP 请求会优先使用当前账号自己的 `apiBaseUrl`。
197
+ - 如果当前账号没配 `apiBaseUrl`,会先按当前账号自己的 `wsUrl` 自动推导。
198
+ - 只有当前账号自己既没配 `apiBaseUrl`,也没提供可用的 `wsUrl` 时,才会回退到 `GRIX_AGENT_API_BASE` 或 `GRIX_WEB_BASE_URL`。
199
+ - `skills/grix-register/scripts/grix_auth.py` 会优先读取 `GRIX_WEB_BASE_URL`,再回落到正式环境地址;插件运行时也会把它当作 HTTP 基地址兜底。
200
+ - 多账号混用不同环境时,不建议设置全局 `GRIX_AGENT_API_BASE` / `GRIX_WEB_BASE_URL`,否则容易把一个账号的 HTTP 请求导到另一个环境。
201
+ - 本地开发最稳妥的写法是同时配置:
202
+
203
+ ```json
204
+ {
205
+ "channels": {
206
+ "grix": {
207
+ "wsUrl": "ws://127.0.0.1:27189/v1/agent-api/ws?agent_id=<YOUR_AGENT_ID>",
208
+ "apiBaseUrl": "http://127.0.0.1:27180/v1/agent-api",
209
+ "agentId": "<YOUR_AGENT_ID>",
210
+ "apiKey": "<YOUR_API_KEY>"
211
+ }
212
+ }
213
+ }
214
+ ```
215
+
166
216
  ## 工具与命令
167
217
 
168
218
  ### Agent 可调用工具
169
219
 
170
220
  - `grix_query`:`contact_search`、`session_search`、`message_history`
171
- - `grix_group`:`create`、`detail`、`add_members`、`remove_members`、`update_member_role`、`update_all_members_muted`、`update_member_speaking`、`dissolve`
221
+ - `grix_group`:`create`、`detail`、`leave`、`add_members`、`remove_members`、`update_member_role`、`update_all_members_muted`、`update_member_speaking`、`dissolve`
172
222
  - `grix_agent_admin`:创建 `provider_type=3` 的 Grix API agent(只创建远端 agent,不会直接改本地 `channels.grix`)
173
223
 
224
+ 工具调用约束:
225
+
226
+ - 以上三个工具都必须显式传入 `accountId`。
227
+ - 如果工具调用上下文存在当前连接账号,则 `accountId` 必须与上下文账号一致;不一致会直接拒绝执行。
228
+
174
229
  ### 运维命令
175
230
 
176
231
  查看账户:
package/dist/index.js CHANGED
@@ -90,6 +90,17 @@ function resolveWsUrl(merged, agentId) {
90
90
  }
91
91
  return `ws://127.0.0.1:27189/v1/agent-api/ws?agent_id=${encodeURIComponent(agentId)}`;
92
92
  }
93
+ function resolveAgentAPIBaseUrl(merged) {
94
+ const cfgBase = normalizeNonEmpty(merged.apiBaseUrl);
95
+ if (cfgBase) {
96
+ return cfgBase;
97
+ }
98
+ if (normalizeNonEmpty(merged.wsUrl)) {
99
+ return "";
100
+ }
101
+ const envBase = normalizeNonEmpty(process.env.GRIX_AGENT_API_BASE);
102
+ return envBase;
103
+ }
93
104
  function redactAibotWsUrl(wsUrl) {
94
105
  if (!wsUrl) {
95
106
  return "";
@@ -113,6 +124,7 @@ function resolveAibotAccount(params) {
113
124
  const agentId = normalizeAgentId(merged.agentId || process.env.GRIX_AGENT_ID);
114
125
  const apiKey = normalizeNonEmpty(merged.apiKey || process.env.GRIX_API_KEY);
115
126
  const wsUrl = resolveWsUrl(merged, agentId);
127
+ const apiBaseUrl = resolveAgentAPIBaseUrl(merged);
116
128
  const configured = Boolean(wsUrl && agentId && apiKey);
117
129
  return {
118
130
  accountId,
@@ -120,6 +132,7 @@ function resolveAibotAccount(params) {
120
132
  enabled,
121
133
  configured,
122
134
  wsUrl,
135
+ apiBaseUrl,
123
136
  agentId,
124
137
  apiKey,
125
138
  config: merged
@@ -467,6 +480,9 @@ var AibotWsClient = class {
467
480
  `reconnect scheduled in ${params.delayMs}ms attempt=${params.attempt} stable=${params.stable} authRejected=${params.authRejected} penaltyFloor=${params.penaltyFloor} suppressed=${suppressed}`
468
481
  );
469
482
  }
483
+ shouldLogInboundPacket(cmd) {
484
+ return cmd !== "ping" && cmd !== "pong" && cmd !== "send_ack";
485
+ }
470
486
  getStatus() {
471
487
  return { ...this.status };
472
488
  }
@@ -542,9 +558,6 @@ var AibotWsClient = class {
542
558
  if (!normalizedChannel || !normalizedAccountID || !normalizedRouteSessionKey || !normalizedSessionID) {
543
559
  throw new Error("grix session_route_bind requires channel/account_id/route_session_key/session_id");
544
560
  }
545
- this.logInfo(
546
- `session_route_bind request channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey} sessionId=${normalizedSessionID}`
547
- );
548
561
  const packet = await this.request(
549
562
  "session_route_bind",
550
563
  {
@@ -564,9 +577,6 @@ var AibotWsClient = class {
564
577
  );
565
578
  throw this.packetError(packet);
566
579
  }
567
- this.logInfo(
568
- `session_route_bind ack channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey} sessionId=${normalizedSessionID}`
569
- );
570
580
  return packet.payload;
571
581
  }
572
582
  async resolveSessionRoute(channel, accountId, routeSessionKey, opts = {}) {
@@ -577,9 +587,6 @@ var AibotWsClient = class {
577
587
  if (!normalizedChannel || !normalizedAccountID || !normalizedRouteSessionKey) {
578
588
  throw new Error("grix session_route_resolve requires channel/account_id/route_session_key");
579
589
  }
580
- this.logInfo(
581
- `session_route_resolve request channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey}`
582
- );
583
590
  const packet = await this.request(
584
591
  "session_route_resolve",
585
592
  {
@@ -603,9 +610,6 @@ var AibotWsClient = class {
603
610
  if (!normalizedSessionID) {
604
611
  throw new Error("grix session_route_resolve ack missing session_id");
605
612
  }
606
- this.logInfo(
607
- `session_route_resolve ack channel=${normalizedChannel} accountId=${normalizedAccountID} routeSessionKey=${normalizedRouteSessionKey} sessionId=${normalizedSessionID}`
608
- );
609
613
  return {
610
614
  ...payload,
611
615
  channel: String(payload.channel ?? normalizedChannel),
@@ -1058,7 +1062,7 @@ var AibotWsClient = class {
1058
1062
  }
1059
1063
  const cmd = String(packet.cmd ?? "").trim();
1060
1064
  const seq = Number(packet.seq ?? 0);
1061
- if (cmd !== "ping") {
1065
+ if (this.shouldLogInboundPacket(cmd)) {
1062
1066
  this.logInfo(
1063
1067
  `inbound packet conn=${resolvedConnSerial} cmd=${cmd || "-"} seq=${seq} bytes=${text.length}`
1064
1068
  );
@@ -3656,9 +3660,6 @@ async function deliverAibotStreamBlock(params) {
3656
3660
  messageSid: params.messageSid,
3657
3661
  clientMsgId: params.clientMsgId
3658
3662
  });
3659
- params.runtime.log(
3660
- `[grix:${params.account.accountId}] stream block split into ${chunks.length} chunk(s) ${context} textLen=${params.text.length} chunkDelayMs=${chunkDelayMs}`
3661
- );
3662
3663
  for (let index = 0; index < chunks.length; index++) {
3663
3664
  if (params.abortSignal?.aborted) {
3664
3665
  params.runtime.log(
@@ -3671,9 +3672,6 @@ async function deliverAibotStreamBlock(params) {
3671
3672
  if (!normalized) {
3672
3673
  continue;
3673
3674
  }
3674
- params.runtime.log(
3675
- `[grix:${params.account.accountId}] stream chunk send ${context} chunkIndex=${index + 1}/${chunks.length} deltaLen=${normalized.length}`
3676
- );
3677
3675
  await params.client.sendStreamChunk(params.sessionId, normalized, {
3678
3676
  eventId: params.eventId,
3679
3677
  clientMsgId: params.clientMsgId,
@@ -3726,9 +3724,6 @@ async function bindSessionRouteMapping(params) {
3726
3724
  return;
3727
3725
  }
3728
3726
  try {
3729
- params.runtime.log(
3730
- `[grix:${params.account.accountId}] session route bind begin routeSessionKey=${routeSessionKey} sessionId=${sessionId}`
3731
- );
3732
3727
  await params.client.bindSessionRoute(
3733
3728
  "grix",
3734
3729
  params.account.accountId,
@@ -3958,9 +3953,6 @@ async function processEvent(params) {
3958
3953
  sessionId,
3959
3954
  controller: runAbortController
3960
3955
  });
3961
- runtime2.log(
3962
- `[grix:${account.accountId}] active reply run registered eventId=${eventId || `${sessionId}:${messageSid}`} sessionId=${sessionId} messageSid=${messageSid} activeRun=${activeRun ? "true" : "false"}`
3963
- );
3964
3956
  try {
3965
3957
  const route = core.channel.routing.resolveAgentRoute({
3966
3958
  cfg: config,
@@ -4186,22 +4178,10 @@ async function processEvent(params) {
4186
4178
  }
4187
4179
  hasSentBlock = false;
4188
4180
  try {
4189
- const finishContext = buildEventLogContext({
4190
- eventId,
4191
- sessionId,
4192
- messageSid,
4193
- clientMsgId: streamClientMsgId
4194
- });
4195
4181
  const finishDelayMs = resolveStreamFinishDelayMs(account);
4196
4182
  if (finishDelayMs > 0) {
4197
- runtime2.log(
4198
- `[grix:${account.accountId}] stream finish delay ${finishContext} delayMs=${finishDelayMs}`
4199
- );
4200
4183
  await sleep2(finishDelayMs);
4201
4184
  }
4202
- runtime2.log(
4203
- `[grix:${account.accountId}] stream finish ${finishContext}`
4204
- );
4205
4185
  await client.sendStreamChunk(sessionId, "", {
4206
4186
  eventId,
4207
4187
  clientMsgId: streamClientMsgId,
@@ -4235,9 +4215,12 @@ async function processEvent(params) {
4235
4215
  clientMsgId: info.kind === "block" ? streamClientMsgId : `reply_${messageSid}_${outboundCounter}`,
4236
4216
  outboundCounter
4237
4217
  });
4238
- runtime2.log(
4239
- `[grix:${account.accountId}] deliver ${deliverContext} kind=${info.kind} textLen=${text.length} hasMedia=${hasMedia} streamedBefore=${streamedTextAlreadyVisible}`
4240
- );
4218
+ const isStreamBlock = info.kind === "block" && !guardedText && !hasMedia && text.length > 0;
4219
+ if (!isStreamBlock) {
4220
+ runtime2.log(
4221
+ `[grix:${account.accountId}] deliver ${deliverContext} kind=${info.kind} textLen=${text.length} hasMedia=${hasMedia} streamedBefore=${streamedTextAlreadyVisible}`
4222
+ );
4223
+ }
4241
4224
  if (guardedText) {
4242
4225
  runtime2.error(
4243
4226
  `[grix:${account.accountId}] rewrite internal reply text ${deliverContext} code=${guardedText.code} raw=${JSON.stringify(guardedText.rawText)}`
@@ -4256,7 +4239,7 @@ async function processEvent(params) {
4256
4239
  );
4257
4240
  return;
4258
4241
  }
4259
- if (info.kind === "block" && !guardedText && !hasMedia && text) {
4242
+ if (isStreamBlock) {
4260
4243
  const didSendBlock = await deliverAibotStreamBlock({
4261
4244
  text,
4262
4245
  client,
@@ -4407,7 +4390,7 @@ async function processEvent(params) {
4407
4390
  }
4408
4391
  } finally {
4409
4392
  runtime2.log(
4410
- `[grix:${account.accountId}] active reply run clearing eventId=${activeRun?.eventId || "-"} sessionId=${activeRun?.sessionId || sessionId} stopRequested=${activeRun?.stopRequested === true} abortReason=${activeRun ? resolveAbortReason(activeRun.controller.signal) : "-"} visibleOutputSent=${visibleOutputSent}`
4393
+ `[grix:${account.accountId}] active reply run clearing eventId=${activeRun?.eventId || "-"} stopRequested=${activeRun?.stopRequested === true} abortReason=${activeRun ? resolveAbortReason(activeRun.controller.signal) : "-"} visibleOutputSent=${visibleOutputSent}`
4411
4394
  );
4412
4395
  clearActiveReplyRun(activeRun);
4413
4396
  if (!inboundEventAccepted) {
@@ -4602,7 +4585,7 @@ var meta = {
4602
4585
  label: "Grix",
4603
4586
  selectionLabel: "Grix",
4604
4587
  docsPath: "/channels/grix",
4605
- blurb: "Connect OpenClaw to grix.dhf.pub for OpenClaw website management with mobile PWA support.",
4588
+ blurb: "Connect OpenClaw to a Grix deployment for website management with mobile PWA support.",
4606
4589
  aliases: ["gr"],
4607
4590
  order: 90
4608
4591
  };
@@ -4715,6 +4698,7 @@ var aibotPlugin = {
4715
4698
  clearBaseFields: [
4716
4699
  "name",
4717
4700
  "wsUrl",
4701
+ "apiBaseUrl",
4718
4702
  "agentId",
4719
4703
  "apiKey",
4720
4704
  "reconnectMs",
@@ -5309,6 +5293,17 @@ function buildGroupMemberAddRequest(params) {
5309
5293
  body
5310
5294
  };
5311
5295
  }
5296
+ function buildGroupLeaveSelfRequest(params) {
5297
+ const sessionID = readRequiredStringParam(params, "sessionId");
5298
+ return {
5299
+ actionName: "group_leave_self",
5300
+ method: "POST",
5301
+ path: "/sessions/leave",
5302
+ body: {
5303
+ session_id: sessionID
5304
+ }
5305
+ };
5306
+ }
5312
5307
  function buildGroupMemberRemoveRequest(params) {
5313
5308
  const sessionID = readRequiredStringParam(params, "sessionId");
5314
5309
  const memberIDs = readNumericIDArray(params, "memberIds", true);
@@ -5516,6 +5511,8 @@ function buildAgentHTTPRequest(action, params) {
5516
5511
  return buildMessageHistoryRequest(params);
5517
5512
  case "group_create":
5518
5513
  return buildGroupCreateRequest(params);
5514
+ case "group_leave_self":
5515
+ return buildGroupLeaveSelfRequest(params);
5519
5516
  case "group_member_add":
5520
5517
  return buildGroupMemberAddRequest(params);
5521
5518
  case "group_member_remove":
@@ -5539,13 +5536,19 @@ function buildAgentHTTPRequest(action, params) {
5539
5536
 
5540
5537
  // src/admin/agent-api-http.ts
5541
5538
  var DEFAULT_HTTP_TIMEOUT_MS = 15e3;
5539
+ var MAX_LOG_KEYS = 8;
5540
+ var MAX_LOG_PAYLOAD_CHARS = 1200;
5542
5541
  function trimTrailingSlash(value) {
5543
5542
  return value.replace(/\/+$/, "");
5544
5543
  }
5544
+ function logAgentAPIInfo(message) {
5545
+ console.info(`[grix:agent-api] ${message}`);
5546
+ }
5547
+ function logAgentAPIError(message) {
5548
+ console.error(`[grix:agent-api] ${message}`);
5549
+ }
5545
5550
  function resolveExplicitAgentAPIBase() {
5546
- const base = String(
5547
- process.env.GRIX_AGENT_API_BASE ?? process.env.AIBOT_AGENT_API_BASE ?? ""
5548
- ).trim();
5551
+ const base = String(process.env.GRIX_AGENT_API_BASE ?? "").trim();
5549
5552
  if (!base) {
5550
5553
  return "";
5551
5554
  }
@@ -5595,16 +5598,39 @@ function deriveLocalAgentAPIBaseFromWsUrl(wsUrl) {
5595
5598
  const protocol = parsed.protocol === "wss:" ? "https:" : "http:";
5596
5599
  return trimTrailingSlash(`${protocol}//${parsed.hostname}:${apiPort}`) + "/v1/agent-api";
5597
5600
  }
5598
- function resolveAgentAPIBase(account) {
5599
- const explicit = resolveExplicitAgentAPIBase();
5600
- if (explicit) {
5601
- return explicit;
5601
+ function resolveAgentAPIBaseInfo(account) {
5602
+ const accountBase = trimTrailingSlash(String(account.apiBaseUrl ?? "").trim());
5603
+ if (accountBase) {
5604
+ return {
5605
+ base: accountBase,
5606
+ source: "account_api_base_url"
5607
+ };
5602
5608
  }
5603
- const local = deriveLocalAgentAPIBaseFromWsUrl(account.wsUrl);
5609
+ const normalizedWsUrl = String(account.wsUrl ?? "").trim();
5610
+ const local = deriveLocalAgentAPIBaseFromWsUrl(normalizedWsUrl);
5604
5611
  if (local) {
5605
- return local;
5612
+ return {
5613
+ base: local,
5614
+ source: "local_ws_url"
5615
+ };
5616
+ }
5617
+ if (normalizedWsUrl) {
5618
+ return {
5619
+ base: deriveAgentAPIBaseFromWsUrl(normalizedWsUrl),
5620
+ source: "derived_from_ws_url"
5621
+ };
5622
+ }
5623
+ const explicit = resolveExplicitAgentAPIBase();
5624
+ if (explicit) {
5625
+ return {
5626
+ base: explicit,
5627
+ source: "env_grix_agent_api_base"
5628
+ };
5606
5629
  }
5607
- return deriveAgentAPIBaseFromWsUrl(account.wsUrl);
5630
+ return {
5631
+ base: deriveAgentAPIBaseFromWsUrl(normalizedWsUrl),
5632
+ source: "derived_from_ws_url"
5633
+ };
5608
5634
  }
5609
5635
  function buildRequestURL(base, path, query) {
5610
5636
  const normalizedPath = path.startsWith("/") ? path : `/${path}`;
@@ -5647,10 +5673,105 @@ function extractNetworkErrorMessage(error) {
5647
5673
  }
5648
5674
  return String(error);
5649
5675
  }
5676
+ function buildAPIKeyState(apiKey) {
5677
+ const normalized = String(apiKey ?? "").trim();
5678
+ if (!normalized) {
5679
+ return "empty";
5680
+ }
5681
+ return "present";
5682
+ }
5683
+ function summarizePayloadKeys(payload) {
5684
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
5685
+ return "none";
5686
+ }
5687
+ const keys = Object.keys(payload).map((k) => String(k).trim()).filter(Boolean).sort();
5688
+ if (!keys.length) {
5689
+ return "none";
5690
+ }
5691
+ const limited = keys.slice(0, MAX_LOG_KEYS);
5692
+ if (keys.length <= MAX_LOG_KEYS) {
5693
+ return limited.join(",");
5694
+ }
5695
+ return `${limited.join(",")}...(total=${keys.length})`;
5696
+ }
5697
+ function summarizePayloadBytes(payload) {
5698
+ try {
5699
+ return String(Buffer.byteLength(JSON.stringify(payload ?? {}), "utf8"));
5700
+ } catch {
5701
+ return "unknown";
5702
+ }
5703
+ }
5704
+ function isSensitiveLogKey(key) {
5705
+ const normalized = String(key ?? "").trim().toLowerCase();
5706
+ return normalized.includes("api_key") || normalized.includes("apikey") || normalized.includes("token") || normalized.includes("authorization") || normalized.includes("password") || normalized.includes("secret");
5707
+ }
5708
+ function sanitizePayloadForLog(payload, depth = 0) {
5709
+ if (depth >= 5) {
5710
+ return "[max-depth]";
5711
+ }
5712
+ if (payload == null) {
5713
+ return payload;
5714
+ }
5715
+ if (typeof payload === "string" || typeof payload === "number" || typeof payload === "boolean") {
5716
+ return payload;
5717
+ }
5718
+ if (Array.isArray(payload)) {
5719
+ return payload.map((item) => sanitizePayloadForLog(item, depth + 1));
5720
+ }
5721
+ if (typeof payload === "object") {
5722
+ const raw = payload;
5723
+ const sanitized = {};
5724
+ for (const [key, value] of Object.entries(raw)) {
5725
+ sanitized[key] = isSensitiveLogKey(key) ? "<redacted>" : sanitizePayloadForLog(value, depth + 1);
5726
+ }
5727
+ return sanitized;
5728
+ }
5729
+ return String(payload);
5730
+ }
5731
+ function stringifyPayloadForLog(payload) {
5732
+ let json = "";
5733
+ try {
5734
+ json = JSON.stringify(sanitizePayloadForLog(payload));
5735
+ } catch {
5736
+ return '"[unserializable]"';
5737
+ }
5738
+ if (!json) {
5739
+ return "{}";
5740
+ }
5741
+ if (json.length <= MAX_LOG_PAYLOAD_CHARS) {
5742
+ return json;
5743
+ }
5744
+ return `${json.slice(0, MAX_LOG_PAYLOAD_CHARS)}...(truncated,len=${json.length})`;
5745
+ }
5746
+ function buildRequestLogContext(params, context) {
5747
+ const queryPayload = params.query ?? {};
5748
+ const bodyPayload = params.method === "POST" ? params.body ?? {} : {};
5749
+ return [
5750
+ `action=${params.actionName}`,
5751
+ `account=${params.account.accountId}`,
5752
+ `agent=${params.account.agentId}`,
5753
+ `method=${params.method}`,
5754
+ `source=${context.resolvedBase.source}`,
5755
+ `url=${context.url}`,
5756
+ `timeout_ms=${context.timeoutMs}`,
5757
+ `api_key=${buildAPIKeyState(params.account.apiKey)}`,
5758
+ `query_keys=${summarizePayloadKeys(queryPayload)}`,
5759
+ `query_payload=${JSON.stringify(stringifyPayloadForLog(queryPayload))}`,
5760
+ `body_keys=${summarizePayloadKeys(bodyPayload)}`,
5761
+ `body_payload=${JSON.stringify(stringifyPayloadForLog(bodyPayload))}`,
5762
+ `body_bytes=${summarizePayloadBytes(bodyPayload)}`
5763
+ ].join(" ");
5764
+ }
5650
5765
  async function callAgentAPI(params) {
5651
- const base = resolveAgentAPIBase(params.account);
5652
- const url = buildRequestURL(base, params.path, params.query);
5766
+ const resolvedBase = resolveAgentAPIBaseInfo(params.account);
5767
+ const url = buildRequestURL(resolvedBase.base, params.path, params.query);
5653
5768
  const timeoutMs = Number.isFinite(params.timeoutMs) ? Math.max(1e3, Math.floor(params.timeoutMs)) : DEFAULT_HTTP_TIMEOUT_MS;
5769
+ const requestLogContext = buildRequestLogContext(params, {
5770
+ resolvedBase,
5771
+ url,
5772
+ timeoutMs
5773
+ });
5774
+ logAgentAPIInfo(`request ${requestLogContext}`);
5654
5775
  const controller = new AbortController();
5655
5776
  const timer = setTimeout(() => controller.abort(), timeoutMs);
5656
5777
  let resp;
@@ -5666,6 +5787,9 @@ async function callAgentAPI(params) {
5666
5787
  });
5667
5788
  } catch (error) {
5668
5789
  clearTimeout(timer);
5790
+ logAgentAPIError(
5791
+ `network_error ${requestLogContext} error=${JSON.stringify(extractNetworkErrorMessage(error))}`
5792
+ );
5669
5793
  throw new Error(
5670
5794
  `Grix ${params.actionName} network error: ${extractNetworkErrorMessage(error)}`
5671
5795
  );
@@ -5677,6 +5801,9 @@ async function callAgentAPI(params) {
5677
5801
  try {
5678
5802
  envelope = JSON.parse(rawBody);
5679
5803
  } catch {
5804
+ logAgentAPIError(
5805
+ `invalid_response ${requestLogContext} status=${status} raw_len=${rawBody.length}`
5806
+ );
5680
5807
  throw new Error(
5681
5808
  `Grix ${params.actionName} invalid response: status=${status} body=${rawBody.slice(0, 256)}`
5682
5809
  );
@@ -5684,13 +5811,41 @@ async function callAgentAPI(params) {
5684
5811
  const bizCode = normalizeBizCode(envelope.code);
5685
5812
  if (!resp.ok || bizCode !== 0) {
5686
5813
  const message = normalizeMessage(envelope.msg);
5814
+ logAgentAPIError(
5815
+ `failed ${requestLogContext} status=${status} code=${bizCode} msg=${message} has_data=${envelope.data == null ? "false" : "true"}`
5816
+ );
5687
5817
  throw new Error(
5688
5818
  `Grix ${params.actionName} failed: status=${status} code=${bizCode} msg=${message}`
5689
5819
  );
5690
5820
  }
5821
+ logAgentAPIInfo(`success ${requestLogContext} status=${status}`);
5691
5822
  return envelope.data;
5692
5823
  }
5693
5824
 
5825
+ // src/admin/account-binding.ts
5826
+ function normalizeNonEmpty2(value) {
5827
+ const normalized = String(value ?? "").trim();
5828
+ return normalized || void 0;
5829
+ }
5830
+ function resolveStrictToolAccountId(params) {
5831
+ const toolAccountId = normalizeNonEmpty2(params.toolAccountId);
5832
+ const contextAccountId = normalizeNonEmpty2(params.contextAccountId);
5833
+ console.info(
5834
+ `[grix:account-binding] tool=${params.toolName} request_account=${toolAccountId ?? "-"} context_account=${contextAccountId ?? "-"}`
5835
+ );
5836
+ if (!toolAccountId) {
5837
+ throw new Error(
5838
+ `[${params.toolName}] accountId is required. Pass the exact accountId of the current connection.`
5839
+ );
5840
+ }
5841
+ if (contextAccountId && toolAccountId !== contextAccountId) {
5842
+ throw new Error(
5843
+ `[${params.toolName}] accountId mismatch. request=${toolAccountId}, context=${contextAccountId}. Refusing cross-account execution.`
5844
+ );
5845
+ }
5846
+ return toolAccountId;
5847
+ }
5848
+
5694
5849
  // src/admin/accounts.ts
5695
5850
  var DEFAULT_ACCOUNT_ID2 = "default";
5696
5851
  function normalizeAccountId2(value) {
@@ -5711,9 +5866,24 @@ function listConfiguredAccountIds2(cfg) {
5711
5866
  }
5712
5867
  return Object.keys(accounts).filter(Boolean);
5713
5868
  }
5714
- function normalizeNonEmpty2(value) {
5869
+ function normalizeNonEmpty3(value) {
5715
5870
  return String(value ?? "").trim();
5716
5871
  }
5872
+ function trimTrailingSlash2(value) {
5873
+ return value.replace(/\/+$/, "");
5874
+ }
5875
+ function summarizeEndpoint(value) {
5876
+ const normalized = normalizeNonEmpty3(value);
5877
+ if (!normalized) {
5878
+ return "-";
5879
+ }
5880
+ try {
5881
+ const parsed = new URL(normalized);
5882
+ return `${parsed.protocol}//${parsed.host}`;
5883
+ } catch {
5884
+ return normalized.slice(0, 128);
5885
+ }
5886
+ }
5717
5887
  function appendAgentIdToWsUrl2(rawWsUrl, agentId) {
5718
5888
  if (!rawWsUrl) {
5719
5889
  return "";
@@ -5736,8 +5906,8 @@ function appendAgentIdToWsUrl2(rawWsUrl, agentId) {
5736
5906
  }
5737
5907
  }
5738
5908
  function resolveWsUrl2(merged, agentId) {
5739
- const envWs = normalizeNonEmpty2(process.env.GRIX_WS_URL);
5740
- const cfgWs = normalizeNonEmpty2(merged.wsUrl);
5909
+ const envWs = normalizeNonEmpty3(process.env.GRIX_WS_URL);
5910
+ const cfgWs = normalizeNonEmpty3(merged.wsUrl);
5741
5911
  const ws = cfgWs || envWs;
5742
5912
  if (ws) {
5743
5913
  return appendAgentIdToWsUrl2(ws, agentId);
@@ -5747,6 +5917,44 @@ function resolveWsUrl2(merged, agentId) {
5747
5917
  }
5748
5918
  return `ws://127.0.0.1:27189/v1/agent-api/ws?agent_id=${encodeURIComponent(agentId)}`;
5749
5919
  }
5920
+ function resolveAgentAPIBaseUrl2(merged) {
5921
+ const cfgBase = trimTrailingSlash2(normalizeNonEmpty3(merged.apiBaseUrl));
5922
+ if (cfgBase) {
5923
+ return cfgBase;
5924
+ }
5925
+ if (normalizeNonEmpty3(merged.wsUrl)) {
5926
+ return "";
5927
+ }
5928
+ const envBase = trimTrailingSlash2(normalizeNonEmpty3(process.env.GRIX_AGENT_API_BASE));
5929
+ const webBase = trimTrailingSlash2(normalizeNonEmpty3(process.env.GRIX_WEB_BASE_URL));
5930
+ return envBase || webBase;
5931
+ }
5932
+ function resolveStrictAccountConfig(cfg, accountId) {
5933
+ const grixCfg = rawGrixConfig(cfg);
5934
+ const accounts = grixCfg.accounts;
5935
+ if (!accounts || typeof accounts !== "object") {
5936
+ console.error(
5937
+ `[grix:account] strict lookup failed account=${accountId} reason=accounts_map_missing`
5938
+ );
5939
+ throw new Error(
5940
+ `Grix account "${accountId}" is not configured under channels.grix.accounts.`
5941
+ );
5942
+ }
5943
+ const configuredIds = Object.keys(accounts).filter(Boolean).sort();
5944
+ const account = accounts[accountId];
5945
+ if (!account || typeof account !== "object") {
5946
+ console.error(
5947
+ `[grix:account] strict lookup failed account=${accountId} reason=account_missing configured_accounts=${configuredIds.join(",") || "none"}`
5948
+ );
5949
+ throw new Error(
5950
+ `Grix account "${accountId}" is missing under channels.grix.accounts.${accountId}.`
5951
+ );
5952
+ }
5953
+ console.info(
5954
+ `[grix:account] strict lookup account=${accountId} configured_accounts=${configuredIds.join(",") || "none"} has_ws=${normalizeNonEmpty3(account.wsUrl) ? "yes" : "no"} has_api_base=${normalizeNonEmpty3(account.apiBaseUrl) ? "yes" : "no"} has_agent_id=${normalizeNonEmpty3(account.agentId) ? "yes" : "no"} has_api_key=${normalizeNonEmpty3(account.apiKey) ? "yes" : "no"}`
5955
+ );
5956
+ return account;
5957
+ }
5750
5958
  function resolveMergedAccountConfig(cfg, accountId) {
5751
5959
  const grixCfg = rawGrixConfig(cfg);
5752
5960
  const { accounts: _ignoredAccounts, defaultAccount: _ignoredDefault, ...base } = grixCfg;
@@ -5776,21 +5984,29 @@ function resolveDefaultGrixAccountId(cfg) {
5776
5984
  return ids[0] ?? DEFAULT_ACCOUNT_ID2;
5777
5985
  }
5778
5986
  function resolveGrixAccount(params) {
5987
+ const strictScope = Boolean(params.strictAccountScope);
5779
5988
  const accountId = params.accountId == null || String(params.accountId).trim() === "" ? resolveDefaultGrixAccountId(params.cfg) : normalizeAccountId2(params.accountId);
5780
- const merged = resolveMergedAccountConfig(params.cfg, accountId);
5989
+ const merged = strictScope ? resolveStrictAccountConfig(params.cfg, accountId) : resolveMergedAccountConfig(params.cfg, accountId);
5781
5990
  const baseEnabled = rawGrixConfig(params.cfg).enabled !== false;
5782
5991
  const accountEnabled = merged.enabled !== false;
5783
5992
  const enabled = baseEnabled && accountEnabled;
5784
- const agentId = normalizeNonEmpty2(merged.agentId || process.env.GRIX_AGENT_ID);
5785
- const apiKey = normalizeNonEmpty2(merged.apiKey || process.env.GRIX_API_KEY);
5786
- const wsUrl = resolveWsUrl2(merged, agentId);
5993
+ const agentId = strictScope ? normalizeNonEmpty3(merged.agentId) : normalizeNonEmpty3(merged.agentId || process.env.GRIX_AGENT_ID);
5994
+ const apiKey = strictScope ? normalizeNonEmpty3(merged.apiKey) : normalizeNonEmpty3(merged.apiKey || process.env.GRIX_API_KEY);
5995
+ const wsUrl = strictScope ? appendAgentIdToWsUrl2(normalizeNonEmpty3(merged.wsUrl), agentId) : resolveWsUrl2(merged, agentId);
5996
+ const apiBaseUrl = strictScope ? trimTrailingSlash2(normalizeNonEmpty3(merged.apiBaseUrl)) : resolveAgentAPIBaseUrl2(merged);
5787
5997
  const configured = Boolean(wsUrl && agentId && apiKey);
5998
+ if (strictScope) {
5999
+ console.info(
6000
+ `[grix:account] strict resolved account=${accountId} enabled=${enabled} configured=${configured} ws_endpoint=${summarizeEndpoint(wsUrl)} api_base_endpoint=${summarizeEndpoint(apiBaseUrl)} agent_id=${agentId || "-"} api_key=${apiKey ? "present" : "empty"}`
6001
+ );
6002
+ }
5788
6003
  return {
5789
6004
  accountId,
5790
- name: normalizeNonEmpty2(merged.name) || void 0,
6005
+ name: normalizeNonEmpty3(merged.name) || void 0,
5791
6006
  enabled,
5792
6007
  configured,
5793
6008
  wsUrl,
6009
+ apiBaseUrl,
5794
6010
  agentId,
5795
6011
  apiKey,
5796
6012
  config: merged
@@ -5805,6 +6021,7 @@ function summarizeGrixAccounts(cfg) {
5805
6021
  enabled: account.enabled,
5806
6022
  configured: account.configured,
5807
6023
  wsUrl: account.wsUrl || null,
6024
+ apiBaseUrl: account.apiBaseUrl || null,
5808
6025
  agentId: account.agentId || null
5809
6026
  };
5810
6027
  });
@@ -5842,9 +6059,15 @@ function sanitizeCreatedAgentData(data) {
5842
6059
  return payload;
5843
6060
  }
5844
6061
  async function createGrixApiAgent(params) {
6062
+ const accountId = resolveStrictToolAccountId({
6063
+ toolName: "grix_agent_admin",
6064
+ toolAccountId: params.toolParams.accountId,
6065
+ contextAccountId: params.contextAccountId
6066
+ });
5845
6067
  const account = resolveGrixAccount({
5846
6068
  cfg: params.cfg,
5847
- accountId: params.toolParams.accountId
6069
+ accountId,
6070
+ strictAccountScope: true
5848
6071
  });
5849
6072
  if (!account.enabled) {
5850
6073
  throw new Error(`Grix account "${account.accountId}" is disabled.`);
@@ -5968,9 +6191,10 @@ var GrixAgentAdminToolSchema = {
5968
6191
  required: ["actions"]
5969
6192
  }
5970
6193
  },
5971
- required: ["agentName", "describeMessageTool"]
6194
+ required: ["accountId", "agentName", "describeMessageTool"]
5972
6195
  };
5973
- function createGrixAgentAdminTool(api) {
6196
+ function createGrixAgentAdminTool(api, ctx) {
6197
+ const contextAccountId = ctx?.agentAccountId;
5974
6198
  return {
5975
6199
  name: "grix_agent_admin",
5976
6200
  label: "Grix Agent Admin",
@@ -5981,7 +6205,8 @@ function createGrixAgentAdminTool(api) {
5981
6205
  return jsonToolResult(
5982
6206
  await createGrixApiAgent({
5983
6207
  cfg: api.config,
5984
- toolParams: params
6208
+ toolParams: params,
6209
+ contextAccountId
5985
6210
  })
5986
6211
  );
5987
6212
  } catch (err) {
@@ -6000,6 +6225,8 @@ function mapGroupActionToRequestAction(action) {
6000
6225
  return "group_create";
6001
6226
  case "detail":
6002
6227
  return "group_detail_read";
6228
+ case "leave":
6229
+ return "group_leave_self";
6003
6230
  case "add_members":
6004
6231
  return "group_member_add";
6005
6232
  case "remove_members":
@@ -6018,9 +6245,15 @@ function mapGroupActionToRequestAction(action) {
6018
6245
  }
6019
6246
  }
6020
6247
  async function runGrixGroupAction(params) {
6248
+ const accountId = resolveStrictToolAccountId({
6249
+ toolName: "grix_group",
6250
+ toolAccountId: params.toolParams.accountId,
6251
+ contextAccountId: params.contextAccountId
6252
+ });
6021
6253
  const account = resolveGrixAccount({
6022
6254
  cfg: params.cfg,
6023
- accountId: params.toolParams.accountId
6255
+ accountId,
6256
+ strictAccountScope: true
6024
6257
  });
6025
6258
  if (!account.enabled) {
6026
6259
  throw new Error(`Grix account "${account.accountId}" is disabled.`);
@@ -6038,6 +6271,13 @@ async function runGrixGroupAction(params) {
6038
6271
  query: request.query,
6039
6272
  body: request.body
6040
6273
  });
6274
+ if (params.toolParams.action === "leave") {
6275
+ const d = data;
6276
+ const left = d != null && typeof d === "object" ? d["left"] : void 0;
6277
+ console.info(
6278
+ `[grix:group] leave result account=${account.accountId} agent=${account.agentId} session=${String(params.toolParams.sessionId ?? "")} left=${left}`
6279
+ );
6280
+ }
6041
6281
  return {
6042
6282
  ok: true,
6043
6283
  accountId: account.accountId,
@@ -6063,7 +6303,7 @@ var GrixGroupToolSchema = {
6063
6303
  memberIds: { type: "array", items: numericIdSchema },
6064
6304
  memberTypes: { type: "array", items: { type: "integer", enum: [1, 2] } }
6065
6305
  },
6066
- required: ["action", "name"]
6306
+ required: ["action", "accountId", "name"]
6067
6307
  },
6068
6308
  {
6069
6309
  type: "object",
@@ -6073,7 +6313,17 @@ var GrixGroupToolSchema = {
6073
6313
  accountId: { type: "string", minLength: 1 },
6074
6314
  sessionId: { type: "string", minLength: 1 }
6075
6315
  },
6076
- required: ["action", "sessionId"]
6316
+ required: ["action", "accountId", "sessionId"]
6317
+ },
6318
+ {
6319
+ type: "object",
6320
+ additionalProperties: false,
6321
+ properties: {
6322
+ action: { const: "leave" },
6323
+ accountId: { type: "string", minLength: 1 },
6324
+ sessionId: { type: "string", minLength: 1 }
6325
+ },
6326
+ required: ["action", "accountId", "sessionId"]
6077
6327
  },
6078
6328
  {
6079
6329
  type: "object",
@@ -6085,7 +6335,7 @@ var GrixGroupToolSchema = {
6085
6335
  memberIds: { type: "array", items: numericIdSchema, minItems: 1 },
6086
6336
  memberTypes: { type: "array", items: { type: "integer", enum: [1, 2] } }
6087
6337
  },
6088
- required: ["action", "sessionId", "memberIds"]
6338
+ required: ["action", "accountId", "sessionId", "memberIds"]
6089
6339
  },
6090
6340
  {
6091
6341
  type: "object",
@@ -6097,7 +6347,7 @@ var GrixGroupToolSchema = {
6097
6347
  memberIds: { type: "array", items: numericIdSchema, minItems: 1 },
6098
6348
  memberTypes: { type: "array", items: { type: "integer", enum: [1, 2] } }
6099
6349
  },
6100
- required: ["action", "sessionId", "memberIds"]
6350
+ required: ["action", "accountId", "sessionId", "memberIds"]
6101
6351
  },
6102
6352
  {
6103
6353
  type: "object",
@@ -6110,7 +6360,7 @@ var GrixGroupToolSchema = {
6110
6360
  memberType: { type: "integer", enum: [1] },
6111
6361
  role: { type: "integer", enum: [1, 2] }
6112
6362
  },
6113
- required: ["action", "sessionId", "memberId", "role"]
6363
+ required: ["action", "accountId", "sessionId", "memberId", "role"]
6114
6364
  },
6115
6365
  {
6116
6366
  type: "object",
@@ -6121,7 +6371,7 @@ var GrixGroupToolSchema = {
6121
6371
  sessionId: { type: "string", minLength: 1 },
6122
6372
  allMembersMuted: { type: "boolean" }
6123
6373
  },
6124
- required: ["action", "sessionId", "allMembersMuted"]
6374
+ required: ["action", "accountId", "sessionId", "allMembersMuted"]
6125
6375
  },
6126
6376
  {
6127
6377
  type: "object",
@@ -6135,7 +6385,7 @@ var GrixGroupToolSchema = {
6135
6385
  isSpeakMuted: { type: "boolean" },
6136
6386
  canSpeakWhenAllMuted: { type: "boolean" }
6137
6387
  },
6138
- required: ["action", "sessionId", "memberId"],
6388
+ required: ["action", "accountId", "sessionId", "memberId"],
6139
6389
  anyOf: [
6140
6390
  { required: ["isSpeakMuted"] },
6141
6391
  { required: ["canSpeakWhenAllMuted"] }
@@ -6149,11 +6399,12 @@ var GrixGroupToolSchema = {
6149
6399
  accountId: { type: "string", minLength: 1 },
6150
6400
  sessionId: { type: "string", minLength: 1 }
6151
6401
  },
6152
- required: ["action", "sessionId"]
6402
+ required: ["action", "accountId", "sessionId"]
6153
6403
  }
6154
6404
  ]
6155
6405
  };
6156
- function createGrixGroupTool(api) {
6406
+ function createGrixGroupTool(api, ctx) {
6407
+ const contextAccountId = ctx?.agentAccountId;
6157
6408
  return {
6158
6409
  name: "grix_group",
6159
6410
  label: "Grix Group",
@@ -6164,7 +6415,8 @@ function createGrixGroupTool(api) {
6164
6415
  return jsonToolResult(
6165
6416
  await runGrixGroupAction({
6166
6417
  cfg: api.config,
6167
- toolParams: params
6418
+ toolParams: params,
6419
+ contextAccountId
6168
6420
  })
6169
6421
  );
6170
6422
  } catch (err) {
@@ -6191,9 +6443,15 @@ function mapQueryActionToRequestAction(action) {
6191
6443
  }
6192
6444
  }
6193
6445
  async function runGrixQueryAction(params) {
6446
+ const accountId = resolveStrictToolAccountId({
6447
+ toolName: "grix_query",
6448
+ toolAccountId: params.toolParams.accountId,
6449
+ contextAccountId: params.contextAccountId
6450
+ });
6194
6451
  const account = resolveGrixAccount({
6195
6452
  cfg: params.cfg,
6196
- accountId: params.toolParams.accountId
6453
+ accountId,
6454
+ strictAccountScope: true
6197
6455
  });
6198
6456
  if (!account.enabled) {
6199
6457
  throw new Error(`Grix account "${account.accountId}" is disabled.`);
@@ -6232,7 +6490,7 @@ var GrixQueryToolSchema = {
6232
6490
  limit: { type: "integer", minimum: 1 },
6233
6491
  offset: { type: "integer", minimum: 0 }
6234
6492
  },
6235
- required: ["action", "id"]
6493
+ required: ["action", "accountId", "id"]
6236
6494
  },
6237
6495
  {
6238
6496
  type: "object",
@@ -6244,7 +6502,7 @@ var GrixQueryToolSchema = {
6244
6502
  limit: { type: "integer", minimum: 1 },
6245
6503
  offset: { type: "integer", minimum: 0 }
6246
6504
  },
6247
- required: ["action", "id"]
6505
+ required: ["action", "accountId", "id"]
6248
6506
  },
6249
6507
  {
6250
6508
  type: "object",
@@ -6256,11 +6514,12 @@ var GrixQueryToolSchema = {
6256
6514
  beforeId: { type: "string", pattern: "^[0-9]+$" },
6257
6515
  limit: { type: "integer", minimum: 1 }
6258
6516
  },
6259
- required: ["action", "sessionId"]
6517
+ required: ["action", "accountId", "sessionId"]
6260
6518
  }
6261
6519
  ]
6262
6520
  };
6263
- function createGrixQueryTool(api) {
6521
+ function createGrixQueryTool(api, ctx) {
6522
+ const contextAccountId = ctx?.agentAccountId;
6264
6523
  return {
6265
6524
  name: "grix_query",
6266
6525
  label: "Grix Query",
@@ -6271,7 +6530,8 @@ function createGrixQueryTool(api) {
6271
6530
  return jsonToolResult(
6272
6531
  await runGrixQueryAction({
6273
6532
  cfg: api.config,
6274
- toolParams: params
6533
+ toolParams: params,
6534
+ contextAccountId
6275
6535
  })
6276
6536
  );
6277
6537
  } catch (err) {
@@ -6364,9 +6624,9 @@ var plugin = {
6364
6624
  register(api) {
6365
6625
  setAibotRuntime(api.runtime);
6366
6626
  api.registerChannel({ plugin: aibotPlugin });
6367
- api.registerTool(createGrixQueryTool(api), { optional: true });
6368
- api.registerTool(createGrixGroupTool(api), { optional: true });
6369
- api.registerTool(createGrixAgentAdminTool(api), { optional: true });
6627
+ api.registerTool((ctx) => createGrixQueryTool(api, ctx), { optional: true });
6628
+ api.registerTool((ctx) => createGrixGroupTool(api, ctx), { optional: true });
6629
+ api.registerTool((ctx) => createGrixAgentAdminTool(api, ctx), { optional: true });
6370
6630
  api.registerCli(({ program }) => registerGrixAdminCli({ api, program }), {
6371
6631
  commands: ["grix"]
6372
6632
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhf-openclaw/grix",
3
- "version": "0.4.11",
3
+ "version": "0.4.14",
4
4
  "description": "Unified Grix OpenClaw plugin with channel transport, typed admin tools, and operator CLI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -60,7 +60,7 @@
60
60
  "label": "Grix",
61
61
  "selectionLabel": "Grix",
62
62
  "docsPath": "/channels/grix",
63
- "blurb": "Connect OpenClaw to grix.dhf.pub for OpenClaw website management with mobile PWA support.",
63
+ "blurb": "Connect OpenClaw to a Grix deployment for website management with mobile PWA support.",
64
64
  "aliases": [
65
65
  "gr"
66
66
  ],
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: grix-group
3
- description: Use the typed `grix_group` tool for Grix group lifecycle and membership operations. Trigger when users ask to create, inspect, update, or dissolve groups, or when these operations fail with scope or permission errors.
3
+ description: Use the typed `grix_group` tool for Grix group lifecycle and membership operations. Trigger when users ask to create, inspect, leave, update, or dissolve groups, or when these operations fail with scope or permission errors.
4
4
  ---
5
5
 
6
6
  # Grix Group Governance
@@ -11,7 +11,7 @@ This skill is about tool selection and guardrails, not protocol bridging.
11
11
  ## Workflow
12
12
 
13
13
  1. Parse the user request into one action:
14
- `create`, `detail`, `add_members`, `remove_members`, `update_member_role`, `update_all_members_muted`, `update_member_speaking`, or `dissolve`.
14
+ `create`, `detail`, `leave`, `add_members`, `remove_members`, `update_member_role`, `update_all_members_muted`, `update_member_speaking`, or `dissolve`.
15
15
  2. Validate required fields before any call.
16
16
  3. Call `grix_group` exactly once per business action.
17
17
  4. Classify failures by HTTP/BizCode and return exact remediation.
@@ -23,8 +23,8 @@ This skill is about tool selection and guardrails, not protocol bridging.
23
23
  For Grix group governance, always call:
24
24
 
25
25
  1. Tool: `grix_group`
26
- 2. `action`: one of `create`, `detail`, `add_members`, `remove_members`, `update_member_role`, `update_all_members_muted`, `update_member_speaking`, `dissolve`
27
- 3. `accountId`: optional; include it when the configured account is ambiguous
26
+ 2. `action`: one of `create`, `detail`, `leave`, `add_members`, `remove_members`, `update_member_role`, `update_all_members_muted`, `update_member_speaking`, `dissolve`
27
+ 3. `accountId`: always pass `{{AccountId}}` from the current conversation context for every action, including `leave`.
28
28
 
29
29
  Rules:
30
30
 
@@ -67,6 +67,24 @@ Guardrails:
67
67
  2. Reject non-numeric `memberIds` before calling the tool.
68
68
  3. If `sessionId` is ambiguous, ask the user to confirm the target group first.
69
69
 
70
+ ### leave
71
+
72
+ Purpose: let the current Agent leave a group by itself.
73
+
74
+ Required input:
75
+
76
+ 1. `sessionId`
77
+
78
+ Guardrails:
79
+
80
+ 1. Only the current Agent owner can request this action.
81
+ 2. If the requester is not the current Agent owner, reject the request directly and do not call `grix_group`.
82
+ 3. This action is only for the current Agent leaving its own group membership.
83
+ 4. Never translate a request to remove other members into `leave`; use `remove_members` for that.
84
+ 5. Do not send `memberId`, `memberIds`, or `memberTypes` with this action.
85
+ 6. This action does not require scope and should not be described as a scope-grant workflow.
86
+ 7. Execute `leave` silently: do not send any pre-leave or farewell message to the group before leaving.
87
+
70
88
  ### remove_members
71
89
 
72
90
  Required input:
@@ -123,6 +141,7 @@ Required input:
123
141
 
124
142
  1. `403/20011`:
125
143
  report missing scope and ask owner to grant the scope in Aibot Agent permission page.
144
+ Do not use this remediation for `leave`, because `leave` is scope-free.
126
145
  2. `401/10001`:
127
146
  report invalid key/auth and suggest checking agent config or rotating API key.
128
147
  3. `403/10002`:
@@ -138,6 +157,7 @@ Required input:
138
157
  2. Include key identifiers (`session_id`, member count, mute state) when successful.
139
158
  3. Include exact remediation when failed.
140
159
  4. Never hide scope or auth errors behind generic wording.
160
+ 5. For `leave`, report result to the requester only; do not post extra messages into the group session.
141
161
 
142
162
  ## References
143
163
 
@@ -16,6 +16,7 @@ Map high-level governance actions to Aibot Agent API HTTP routes.
16
16
  | Action | Method | Route | Required Scope |
17
17
  |---|---|---|---|
18
18
  | `group_create` | `POST` | `/sessions/create_group` | `group.create` |
19
+ | `group_leave_self` | `POST` | `/sessions/leave` | - |
19
20
  | `group_member_add` | `POST` | `/sessions/members/add` | `group.member.add` |
20
21
 
21
22
  ## OpenClaw Tool Mapping
@@ -26,6 +27,7 @@ Use the native `grix_group` tool with typed fields:
26
27
  |---|---|---|
27
28
  | `create` | `group_create` | `name` |
28
29
  | `detail` | `group_detail_read` | `sessionId` |
30
+ | `leave` | `group_leave_self` | `sessionId` |
29
31
  | `add_members` | `group_member_add` | `sessionId`, `memberIds` |
30
32
  | `remove_members` | `group_member_remove` | `sessionId`, `memberIds` |
31
33
  | `update_member_role` | `group_member_role_update` | `sessionId`, `memberId`, `role` |
@@ -57,6 +59,15 @@ Use the native `grix_group` tool with typed fields:
57
59
  }
58
60
  ```
59
61
 
62
+ ### leave
63
+
64
+ ```json
65
+ {
66
+ "action": "leave",
67
+ "sessionId": "task_room_9083"
68
+ }
69
+ ```
70
+
60
71
  ## Error Matrix
61
72
 
62
73
  | HTTP/BizCode | Meaning | Skill Response |
@@ -66,6 +77,10 @@ Use the native `grix_group` tool with typed fields:
66
77
  | `401/10001` | invalid or missing auth | Check api_key and account config |
67
78
  | `403/10002` | agent not active / invalid provider | Ask owner to activate the agent |
68
79
 
80
+ Notes:
81
+
82
+ 1. `leave` does not require scope and should not route `403/20011` into scope remediation.
83
+
69
84
  ## Retry Policy
70
85
 
71
86
  1. Never auto-retry `group_create` unless user confirms.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: grix-register
3
- description: 仅用于初次安装阶段,完成 grix.dhf.pub 账号注册/登录并拿到第一个 provider_type=3 Agent 的参数;本技能不做任何本地 OpenClaw 配置。
3
+ description: 仅用于初次安装阶段,完成 Grix 环境的账号注册/登录并拿到第一个 provider_type=3 Agent 的参数;本技能不做任何本地 OpenClaw 配置。
4
4
  ---
5
5
 
6
6
  # Grix Register
@@ -8,8 +8,9 @@
8
8
 
9
9
  ## Base
10
10
 
11
- 1. Website: `https://grix.dhf.pub/`
12
- 2. Public Grix API base: `https://grix.dhf.pub/v1`
11
+ 1. Default website: `https://grix.dhf.pub/`
12
+ 2. Default public Grix API base: `https://grix.dhf.pub/v1`
13
+ 3. Local development or private deployment can override the base URL.
13
14
 
14
15
  ## Route Mapping
15
16
 
@@ -13,7 +13,24 @@ import uuid
13
13
 
14
14
  DEFAULT_BASE_URL = "https://grix.dhf.pub"
15
15
  DEFAULT_TIMEOUT_SECONDS = 15
16
- DEFAULT_PORTAL_URL = "https://grix.dhf.pub/"
16
+
17
+
18
+ def resolve_default_base_url() -> str:
19
+ return (os.environ.get("GRIX_WEB_BASE_URL", "") or "").strip() or DEFAULT_BASE_URL
20
+
21
+
22
+ def derive_portal_url(raw_base_url: str) -> str:
23
+ base = (raw_base_url or "").strip() or resolve_default_base_url()
24
+ parsed = urllib.parse.urlparse(base)
25
+ if not parsed.scheme or not parsed.netloc:
26
+ raise ValueError(f"Invalid base URL: {base}")
27
+
28
+ path = parsed.path.rstrip("/")
29
+ if path.endswith("/v1"):
30
+ path = path[: -len("/v1")]
31
+
32
+ normalized = parsed._replace(path=path or "/", params="", query="", fragment="")
33
+ return urllib.parse.urlunparse(normalized).rstrip("/") + "/"
17
34
 
18
35
 
19
36
  class GrixAuthError(RuntimeError):
@@ -25,7 +42,7 @@ class GrixAuthError(RuntimeError):
25
42
 
26
43
 
27
44
  def normalize_base_url(raw_base_url: str) -> str:
28
- base = (raw_base_url or "").strip() or DEFAULT_BASE_URL
45
+ base = (raw_base_url or "").strip() or resolve_default_base_url()
29
46
  parsed = urllib.parse.urlparse(base)
30
47
  if not parsed.scheme or not parsed.netloc:
31
48
  raise ValueError(f"Invalid base URL: {base}")
@@ -112,7 +129,7 @@ def print_json(payload):
112
129
  sys.stdout.write("\n")
113
130
 
114
131
 
115
- def build_auth_result(action: str, result: dict):
132
+ def build_auth_result(action: str, result: dict, base_url: str):
116
133
  data = result.get("data") or {}
117
134
  user = data.get("user") or {}
118
135
  return {
@@ -123,7 +140,7 @@ def build_auth_result(action: str, result: dict):
123
140
  "refresh_token": data.get("refresh_token", ""),
124
141
  "expires_in": data.get("expires_in", 0),
125
142
  "user_id": user.get("id", ""),
126
- "portal_url": DEFAULT_PORTAL_URL,
143
+ "portal_url": derive_portal_url(base_url),
127
144
  "data": data,
128
145
  }
129
146
 
@@ -170,7 +187,7 @@ def login_with_credentials(base_url: str, account: str, password: str, device_id
170
187
  "platform": platform,
171
188
  },
172
189
  )
173
- return build_auth_result("login", result)
190
+ return build_auth_result("login", result, base_url)
174
191
 
175
192
 
176
193
  def create_api_agent(base_url: str, access_token: str, agent_name: str, avatar_url: str):
@@ -338,7 +355,7 @@ def handle_register(args):
338
355
  "platform": platform,
339
356
  },
340
357
  )
341
- print_json(build_auth_result("register", result))
358
+ print_json(build_auth_result("register", result, args.base_url))
342
359
 
343
360
 
344
361
  def handle_login(args):
@@ -374,7 +391,11 @@ def handle_create_api_agent(args):
374
391
 
375
392
  def build_parser():
376
393
  parser = argparse.ArgumentParser(description="Grix public auth API helper")
377
- parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="Grix web base URL")
394
+ parser.add_argument(
395
+ "--base-url",
396
+ default=resolve_default_base_url(),
397
+ help="Grix web base URL (defaults to GRIX_WEB_BASE_URL or https://grix.dhf.pub)",
398
+ )
378
399
 
379
400
  subparsers = parser.add_subparsers(dest="action", required=True)
380
401