@dhf-openclaw/grix 0.4.14 → 0.4.16

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/dist/index.js CHANGED
@@ -2795,7 +2795,6 @@ function buildExecApprovalCardEnvelope(payload) {
2795
2795
  var BIZ_CARD_EXTRA_KEY2 = "biz_card";
2796
2796
  var BIZ_CARD_VERSION2 = 1;
2797
2797
  var EGG_INSTALL_STATUS_CARD_TYPE = "egg_install_status";
2798
- var DIRECTIVE_REGEX = /^\s*\[\[egg-install-status\|(.+?)\]\]\s*$/i;
2799
2798
  function normalizeText3(value) {
2800
2799
  return String(value ?? "").replace(/\r\n/g, "\n").trim();
2801
2800
  }
@@ -2806,20 +2805,6 @@ function normalizeStatus(value) {
2806
2805
  }
2807
2806
  return void 0;
2808
2807
  }
2809
- function decodeDirectiveValue(rawValue) {
2810
- const normalized = rawValue.trim();
2811
- if (!normalized) {
2812
- return void 0;
2813
- }
2814
- if (!normalized.includes("%")) {
2815
- return normalized;
2816
- }
2817
- try {
2818
- return decodeURIComponent(normalized);
2819
- } catch {
2820
- return normalized;
2821
- }
2822
- }
2823
2808
  function stripUndefinedFields2(record) {
2824
2809
  const next = {};
2825
2810
  for (const [key, value] of Object.entries(record)) {
@@ -2887,8 +2872,7 @@ function finalizeParsed(parsed) {
2887
2872
  });
2888
2873
  return next;
2889
2874
  }
2890
- function parseStructuredEggInstall(payload) {
2891
- const channelData = payload.channelData;
2875
+ function extractEggInstallRecord(channelData) {
2892
2876
  if (!channelData || typeof channelData !== "object" || Array.isArray(channelData)) {
2893
2877
  return null;
2894
2878
  }
@@ -2900,7 +2884,13 @@ function parseStructuredEggInstall(payload) {
2900
2884
  if (!eggInstall || typeof eggInstall !== "object" || Array.isArray(eggInstall)) {
2901
2885
  return null;
2902
2886
  }
2903
- const record = eggInstall;
2887
+ return eggInstall;
2888
+ }
2889
+ function parseStructuredEggInstall(payload) {
2890
+ const record = extractEggInstallRecord(payload.channelData);
2891
+ if (!record) {
2892
+ return null;
2893
+ }
2904
2894
  return finalizeParsed({
2905
2895
  install_id: record.install_id,
2906
2896
  status: record.status,
@@ -2912,46 +2902,38 @@ function parseStructuredEggInstall(payload) {
2912
2902
  error_msg: record.error_msg
2913
2903
  });
2914
2904
  }
2915
- function parseDirectiveEggInstall(payload) {
2916
- const rawText = String(payload.text ?? "");
2917
- const match = DIRECTIVE_REGEX.exec(rawText);
2918
- if (!match) {
2905
+ function parseEmbeddedReplyPayloadEggInstall(payload) {
2906
+ const rawText = normalizeText3(payload.text);
2907
+ if (!rawText || !/^[{\[]/.test(rawText)) {
2919
2908
  return null;
2920
2909
  }
2921
- const body = String(match[1] ?? "").trim();
2922
- if (!body) {
2910
+ let embeddedReply;
2911
+ try {
2912
+ embeddedReply = JSON.parse(rawText);
2913
+ } catch {
2923
2914
  return null;
2924
2915
  }
2925
- const fields = /* @__PURE__ */ new Map();
2926
- for (const segment of body.split("|")) {
2927
- const normalizedSegment = segment.trim();
2928
- if (!normalizedSegment) {
2929
- return null;
2930
- }
2931
- const separatorIndex = normalizedSegment.indexOf("=");
2932
- if (separatorIndex <= 0 || separatorIndex >= normalizedSegment.length - 1) {
2933
- return null;
2934
- }
2935
- const key = normalizedSegment.slice(0, separatorIndex).trim();
2936
- const decoded = decodeDirectiveValue(normalizedSegment.slice(separatorIndex + 1));
2937
- if (!key || !decoded) {
2938
- return null;
2939
- }
2940
- fields.set(key, decoded);
2916
+ if (!embeddedReply || typeof embeddedReply !== "object" || Array.isArray(embeddedReply)) {
2917
+ return null;
2918
+ }
2919
+ const record = embeddedReply;
2920
+ const eggInstall = extractEggInstallRecord(record.channelData);
2921
+ if (!eggInstall) {
2922
+ return null;
2941
2923
  }
2942
2924
  return finalizeParsed({
2943
- install_id: fields.get("install_id"),
2944
- status: fields.get("status"),
2945
- step: fields.get("step"),
2946
- summary: fields.get("summary"),
2947
- detail_text: fields.get("detail_text"),
2948
- target_agent_id: fields.get("target_agent_id"),
2949
- error_code: fields.get("error_code"),
2950
- error_msg: fields.get("error_msg")
2925
+ install_id: eggInstall.install_id,
2926
+ status: eggInstall.status,
2927
+ step: eggInstall.step,
2928
+ summary: eggInstall.summary ?? record.text,
2929
+ detail_text: eggInstall.detail_text,
2930
+ target_agent_id: eggInstall.target_agent_id,
2931
+ error_code: eggInstall.error_code,
2932
+ error_msg: eggInstall.error_msg
2951
2933
  });
2952
2934
  }
2953
2935
  function buildEggInstallStatusCardEnvelope(payload) {
2954
- const parsed = parseStructuredEggInstall(payload) ?? parseDirectiveEggInstall(payload);
2936
+ const parsed = parseStructuredEggInstall(payload) ?? parseEmbeddedReplyPayloadEggInstall(payload);
2955
2937
  if (!parsed) {
2956
2938
  return void 0;
2957
2939
  }
@@ -3075,24 +3057,149 @@ function buildExecApprovalResolutionReply(params) {
3075
3057
  };
3076
3058
  }
3077
3059
 
3060
+ // src/user-profile-card.ts
3061
+ var BIZ_CARD_EXTRA_KEY4 = "biz_card";
3062
+ var BIZ_CARD_VERSION4 = 1;
3063
+ var USER_PROFILE_CARD_TYPE = "user_profile";
3064
+ function normalizeText5(value) {
3065
+ return String(value ?? "").replace(/\r\n/g, "\n").trim();
3066
+ }
3067
+ function normalizePeerType(value) {
3068
+ if (value === 1 || value === "1") {
3069
+ return 1;
3070
+ }
3071
+ if (value === 2 || value === "2") {
3072
+ return 2;
3073
+ }
3074
+ return void 0;
3075
+ }
3076
+ function stripUndefinedFields4(record) {
3077
+ const next = {};
3078
+ for (const [key, value] of Object.entries(record)) {
3079
+ if (value !== void 0) {
3080
+ next[key] = value;
3081
+ }
3082
+ }
3083
+ return next;
3084
+ }
3085
+ function buildFallbackText2(parsed) {
3086
+ const nickname = parsed.nickname.replace(/\s+/g, " ").trim();
3087
+ const compactNickname = nickname.length > 120 ? `${nickname.slice(0, 117)}...` : nickname;
3088
+ return `[Profile Card] ${compactNickname}`;
3089
+ }
3090
+ function buildExtra2(parsed) {
3091
+ return {
3092
+ [BIZ_CARD_EXTRA_KEY4]: {
3093
+ version: BIZ_CARD_VERSION4,
3094
+ type: USER_PROFILE_CARD_TYPE,
3095
+ payload: stripUndefinedFields4(parsed)
3096
+ },
3097
+ channel_data: {
3098
+ grix: {
3099
+ userProfile: stripUndefinedFields4(parsed)
3100
+ }
3101
+ }
3102
+ };
3103
+ }
3104
+ function finalizeParsed2(parsed) {
3105
+ const userId = normalizeText5(parsed.user_id);
3106
+ const nickname = normalizeText5(parsed.nickname);
3107
+ const peerType = parsed.peer_type === void 0 ? 1 : normalizePeerType(parsed.peer_type);
3108
+ if (!userId || !nickname || !peerType) {
3109
+ return null;
3110
+ }
3111
+ return stripUndefinedFields4({
3112
+ user_id: userId,
3113
+ peer_type: peerType,
3114
+ nickname,
3115
+ avatar_url: normalizeText5(parsed.avatar_url) || void 0
3116
+ });
3117
+ }
3118
+ function extractUserProfileRecord(channelData) {
3119
+ if (!channelData || typeof channelData !== "object" || Array.isArray(channelData)) {
3120
+ return null;
3121
+ }
3122
+ const grix = channelData.grix;
3123
+ if (!grix || typeof grix !== "object" || Array.isArray(grix)) {
3124
+ return null;
3125
+ }
3126
+ const userProfile = grix.userProfile;
3127
+ if (!userProfile || typeof userProfile !== "object" || Array.isArray(userProfile)) {
3128
+ return null;
3129
+ }
3130
+ return userProfile;
3131
+ }
3132
+ function parseStructuredUserProfile(payload) {
3133
+ const record = extractUserProfileRecord(payload.channelData);
3134
+ if (!record) {
3135
+ return null;
3136
+ }
3137
+ return finalizeParsed2({
3138
+ user_id: record.user_id,
3139
+ peer_type: record.peer_type,
3140
+ nickname: record.nickname,
3141
+ avatar_url: record.avatar_url
3142
+ });
3143
+ }
3144
+ function parseEmbeddedReplyPayloadUserProfile(payload) {
3145
+ const rawText = normalizeText5(payload.text);
3146
+ if (!rawText || !/^[{\[]/.test(rawText)) {
3147
+ return null;
3148
+ }
3149
+ let embeddedReply;
3150
+ try {
3151
+ embeddedReply = JSON.parse(rawText);
3152
+ } catch {
3153
+ return null;
3154
+ }
3155
+ if (!embeddedReply || typeof embeddedReply !== "object" || Array.isArray(embeddedReply)) {
3156
+ return null;
3157
+ }
3158
+ const record = embeddedReply;
3159
+ const userProfile = extractUserProfileRecord(record.channelData);
3160
+ if (!userProfile) {
3161
+ return null;
3162
+ }
3163
+ return finalizeParsed2({
3164
+ user_id: userProfile.user_id,
3165
+ peer_type: userProfile.peer_type,
3166
+ nickname: userProfile.nickname ?? record.text,
3167
+ avatar_url: userProfile.avatar_url
3168
+ });
3169
+ }
3170
+ function buildUserProfileCardEnvelope(payload) {
3171
+ const parsed = parseStructuredUserProfile(payload) ?? parseEmbeddedReplyPayloadUserProfile(payload);
3172
+ if (!parsed) {
3173
+ return void 0;
3174
+ }
3175
+ return {
3176
+ extra: buildExtra2(parsed),
3177
+ fallbackText: buildFallbackText2(parsed)
3178
+ };
3179
+ }
3180
+
3078
3181
  // src/outbound-envelope.ts
3182
+ function buildAibotOutboundTextEnvelope(text) {
3183
+ return buildAibotOutboundEnvelope({ text });
3184
+ }
3079
3185
  function buildAibotOutboundEnvelope(payload) {
3080
3186
  const execApprovalDiagnostic = diagnoseExecApprovalPayload(payload);
3081
3187
  const execApprovalCard = buildExecApprovalCardEnvelope(payload);
3082
3188
  const execStatusCard = execApprovalCard ? void 0 : buildExecStatusCardEnvelope(payload);
3083
3189
  const eggInstallStatusCard = execApprovalCard || execStatusCard ? void 0 : buildEggInstallStatusCardEnvelope(payload);
3084
- const envelope = execApprovalCard ?? execStatusCard ?? eggInstallStatusCard;
3190
+ const userProfileCard = execApprovalCard || execStatusCard || eggInstallStatusCard ? void 0 : buildUserProfileCardEnvelope(payload);
3191
+ const envelope = execApprovalCard ?? execStatusCard ?? eggInstallStatusCard ?? userProfileCard;
3085
3192
  return {
3086
3193
  text: envelope?.fallbackText ?? String(payload.text ?? ""),
3087
3194
  extra: envelope?.extra,
3088
- cardKind: execApprovalCard ? "exec_approval" : execStatusCard ? "exec_status" : eggInstallStatusCard ? "egg_install_status" : void 0,
3195
+ cardKind: execApprovalCard ? "exec_approval" : execStatusCard ? "exec_status" : eggInstallStatusCard ? "egg_install_status" : userProfileCard ? "user_profile" : void 0,
3089
3196
  execApprovalDiagnostic
3090
3197
  };
3091
3198
  }
3092
3199
 
3093
3200
  // src/exec-approval-command.ts
3094
3201
  var COMMAND_REGEX = /^\/approve(?:@[^\s]+)?(?:\s|$)/i;
3095
- var DIRECTIVE_REGEX2 = /\[\[exec-approval-resolution\|(.+?)\]\]/i;
3202
+ var DIRECTIVE_REGEX = /\[\[exec-approval-resolution\|(.+?)\]\]/i;
3096
3203
  var DECISION_ALIASES = {
3097
3204
  allow: "allow-once",
3098
3205
  once: "allow-once",
@@ -3106,7 +3213,7 @@ var DECISION_ALIASES = {
3106
3213
  block: "deny"
3107
3214
  };
3108
3215
  var EXEC_APPROVAL_USAGE = "Usage: /approve <id> allow-once|allow-always|deny";
3109
- function decodeDirectiveValue2(rawValue) {
3216
+ function decodeDirectiveValue(rawValue) {
3110
3217
  const normalized = rawValue.trim();
3111
3218
  if (!normalized) {
3112
3219
  return void 0;
@@ -3121,7 +3228,7 @@ function decodeDirectiveValue2(rawValue) {
3121
3228
  }
3122
3229
  }
3123
3230
  function parseExecApprovalResolutionDirective(raw) {
3124
- const match = DIRECTIVE_REGEX2.exec(String(raw ?? ""));
3231
+ const match = DIRECTIVE_REGEX.exec(String(raw ?? ""));
3125
3232
  if (!match) {
3126
3233
  return { matched: false };
3127
3234
  }
@@ -3141,7 +3248,7 @@ function parseExecApprovalResolutionDirective(raw) {
3141
3248
  }
3142
3249
  const key = normalizedSegment.slice(0, separatorIndex).trim();
3143
3250
  const rawValue = normalizedSegment.slice(separatorIndex + 1);
3144
- const value = decodeDirectiveValue2(rawValue);
3251
+ const value = decodeDirectiveValue(rawValue);
3145
3252
  if (!key || !value) {
3146
3253
  return { matched: true, ok: false, error: EXEC_APPROVAL_USAGE };
3147
3254
  }
@@ -3430,6 +3537,11 @@ function shouldTreatDispatchAsRespondedWithoutVisibleOutput(result) {
3430
3537
  return false;
3431
3538
  }
3432
3539
 
3540
+ // src/final-streamed-reply-policy.ts
3541
+ function shouldSkipFinalReplyAfterStreamedBlock(params) {
3542
+ return params.kind === "final" && params.streamedTextAlreadyVisible && !params.hasMedia && params.text.length > 0 && !params.hasStructuredCard;
3543
+ }
3544
+
3433
3545
  // src/outbound-text-delivery-plan.ts
3434
3546
  function buildAibotTextSendPlan(params) {
3435
3547
  const plan = [];
@@ -3570,6 +3682,87 @@ async function deliverAibotPayload(params) {
3570
3682
  return { sent, firstMessageId };
3571
3683
  }
3572
3684
 
3685
+ // src/group-semantics.ts
3686
+ function normalizeEventType(value) {
3687
+ return String(value ?? "").trim().toLowerCase();
3688
+ }
3689
+ function normalizeMentionUserIds(value) {
3690
+ if (!Array.isArray(value)) {
3691
+ return [];
3692
+ }
3693
+ const deduped = /* @__PURE__ */ new Set();
3694
+ for (const entry of value) {
3695
+ const normalized = String(entry ?? "").trim();
3696
+ if (normalized) {
3697
+ deduped.add(normalized);
3698
+ }
3699
+ }
3700
+ return [...deduped];
3701
+ }
3702
+ function resolveGrixInboundSemantics(event) {
3703
+ const eventType = normalizeEventType(event.event_type);
3704
+ const isGroup = Number(event.session_type ?? 0) === 2 || eventType.startsWith("group_");
3705
+ const mentionUserIds = normalizeMentionUserIds(event.mention_user_ids);
3706
+ const hasAnyMention = mentionUserIds.length > 0;
3707
+ const wasMentioned = isGroup && eventType === "group_mention";
3708
+ const mentionsOther = isGroup && hasAnyMention && !wasMentioned;
3709
+ const mustReply = wasMentioned;
3710
+ const allowSilent = isGroup && !mustReply;
3711
+ const allowActions = !isGroup || mustReply;
3712
+ return {
3713
+ isGroup,
3714
+ eventType,
3715
+ wasMentioned,
3716
+ hasAnyMention,
3717
+ mentionsOther,
3718
+ mentionUserIds,
3719
+ mustReply,
3720
+ allowSilent,
3721
+ allowActions
3722
+ };
3723
+ }
3724
+ function buildGrixGroupSystemPrompt(semantics) {
3725
+ if (!semantics.isGroup) {
3726
+ return void 0;
3727
+ }
3728
+ if (semantics.wasMentioned) {
3729
+ return [
3730
+ "This group turn explicitly targeted you.",
3731
+ "You must reply or take the requested action when appropriate.",
3732
+ "Do not return NO_REPLY for this turn."
3733
+ ].join(" ");
3734
+ }
3735
+ if (semantics.mentionsOther) {
3736
+ return [
3737
+ "This group turn explicitly targeted someone else, not you.",
3738
+ "You may reply only if you add clear value.",
3739
+ "Otherwise return NO_REPLY.",
3740
+ "Do not take action unless the task is clearly yours."
3741
+ ].join(" ");
3742
+ }
3743
+ return [
3744
+ "This group turn is visible context, not an explicit mention for you.",
3745
+ "Reply only if it clearly helps the conversation.",
3746
+ "Otherwise return NO_REPLY.",
3747
+ "Do not take action unless the task is clearly yours."
3748
+ ].join(" ");
3749
+ }
3750
+ function resolveGrixMentionFallbackText() {
3751
+ return "I'm here.";
3752
+ }
3753
+ function resolveGrixDispatchResolution(params) {
3754
+ if (params.visibleOutputSent || params.eventResultReported) {
3755
+ return {
3756
+ shouldCompleteSilently: false,
3757
+ shouldSendMentionFallback: false
3758
+ };
3759
+ }
3760
+ return {
3761
+ shouldCompleteSilently: params.semantics.allowSilent,
3762
+ shouldSendMentionFallback: params.semantics.mustReply
3763
+ };
3764
+ }
3765
+
3573
3766
  // src/monitor.ts
3574
3767
  var activeMonitorClients = /* @__PURE__ */ new Map();
3575
3768
  function registerActiveMonitor(accountId, client) {
@@ -3847,8 +4040,10 @@ async function processEvent(params) {
3847
4040
  const quotedMessageId = normalizeNumericMessageId(event.quoted_message_id);
3848
4041
  const bodyForAgent = buildBodyWithQuotedReplyId(rawBody, quotedMessageId);
3849
4042
  const senderId = toStringId2(event.sender_id);
3850
- const isGroup = Number(event.session_type ?? 0) === 2 || String(event.event_type ?? "").startsWith("group_");
4043
+ const semantics = resolveGrixInboundSemantics(event);
4044
+ const isGroup = semantics.isGroup;
3851
4045
  const chatType = isGroup ? "group" : "direct";
4046
+ const groupSystemPrompt = buildGrixGroupSystemPrompt(semantics);
3852
4047
  const createdAt = toTimestampMs(event.created_at);
3853
4048
  const baseLogContext = buildEventLogContext({
3854
4049
  eventId,
@@ -3884,7 +4079,7 @@ async function processEvent(params) {
3884
4079
  return;
3885
4080
  }
3886
4081
  runtime2.log(
3887
- `[grix:${account.accountId}] inbound event ${baseLogContext} chatType=${chatType} bodyLen=${rawBody.length} quotedMessageId=${quotedMessageId || "-"}`
4082
+ `[grix:${account.accountId}] inbound event ${baseLogContext} chatType=${chatType} eventType=${semantics.eventType || "-"} wasMentioned=${semantics.wasMentioned ? "true" : "false"} mentionsOther=${semantics.mentionsOther ? "true" : "false"} bodyLen=${rawBody.length} quotedMessageId=${quotedMessageId || "-"}`
3888
4083
  );
3889
4084
  let inboundEventAccepted = false;
3890
4085
  const commandOutcome = await handleExecApprovalCommand({
@@ -3994,6 +4189,7 @@ async function processEvent(params) {
3994
4189
  SessionKey: route.sessionKey,
3995
4190
  AccountId: route.accountId,
3996
4191
  ChatType: chatType,
4192
+ GroupSystemPrompt: groupSystemPrompt,
3997
4193
  ConversationLabel: fromLabel,
3998
4194
  SenderName: senderId || void 0,
3999
4195
  SenderId: senderId || void 0,
@@ -4004,6 +4200,7 @@ async function processEvent(params) {
4004
4200
  // This field carries the inbound quoted message id from end user (event.quoted_message_id).
4005
4201
  // It is not the outbound reply anchor used when plugin sends replies back to Aibot.
4006
4202
  ReplyToMessageSid: quotedMessageId,
4203
+ WasMentioned: isGroup ? semantics.wasMentioned : void 0,
4007
4204
  OriginatingChannel: "grix",
4008
4205
  OriginatingTo: to
4009
4206
  });
@@ -4216,6 +4413,7 @@ async function processEvent(params) {
4216
4413
  outboundCounter
4217
4414
  });
4218
4415
  const isStreamBlock = info.kind === "block" && !guardedText && !hasMedia && text.length > 0;
4416
+ const finalOutboundEnvelope = info.kind === "final" ? buildAibotOutboundEnvelope(normalizedPayload) : void 0;
4219
4417
  if (!isStreamBlock) {
4220
4418
  runtime2.log(
4221
4419
  `[grix:${account.accountId}] deliver ${deliverContext} kind=${info.kind} textLen=${text.length} hasMedia=${hasMedia} streamedBefore=${streamedTextAlreadyVisible}`
@@ -4261,7 +4459,13 @@ async function processEvent(params) {
4261
4459
  return;
4262
4460
  }
4263
4461
  await finishStreamIfNeeded();
4264
- if (info.kind === "final" && streamedTextAlreadyVisible && !hasMedia && text) {
4462
+ if (shouldSkipFinalReplyAfterStreamedBlock({
4463
+ kind: info.kind,
4464
+ streamedTextAlreadyVisible,
4465
+ hasMedia,
4466
+ text,
4467
+ hasStructuredCard: Boolean(finalOutboundEnvelope?.cardKind)
4468
+ })) {
4265
4469
  runtime2.log(
4266
4470
  `[grix:${account.accountId}] skip final text after streamed block ${deliverContext} textLen=${text.length}`
4267
4471
  );
@@ -4357,6 +4561,49 @@ async function processEvent(params) {
4357
4561
  markVisibleOutputSent();
4358
4562
  }
4359
4563
  }
4564
+ const dispatchResolution = resolveGrixDispatchResolution({
4565
+ semantics,
4566
+ visibleOutputSent,
4567
+ eventResultReported
4568
+ });
4569
+ if (dispatchResolution.shouldCompleteSilently) {
4570
+ runtime2.log(
4571
+ `[grix:${account.accountId}] group dispatch completed silently ${baseLogContext} attempt=${attemptLabel} wasMentioned=${semantics.wasMentioned ? "true" : "false"}`
4572
+ );
4573
+ reportEventResult("responded");
4574
+ }
4575
+ if (dispatchResolution.shouldSendMentionFallback) {
4576
+ outboundCounter++;
4577
+ const stableClientMsgId = `reply_${messageSid}_${outboundCounter}`;
4578
+ runtime2.log(
4579
+ `[grix:${account.accountId}] explicit mention fallback reply ${buildEventLogContext({
4580
+ eventId,
4581
+ sessionId,
4582
+ messageSid,
4583
+ clientMsgId: stableClientMsgId,
4584
+ outboundCounter
4585
+ })}`
4586
+ );
4587
+ const didSendFallback = await deliverAibotMessage({
4588
+ payload: {
4589
+ text: resolveGrixMentionFallbackText()
4590
+ },
4591
+ client,
4592
+ account,
4593
+ sessionId,
4594
+ abortSignal: runAbortController.signal,
4595
+ eventId,
4596
+ quotedMessageId: outboundQuotedMessageId,
4597
+ runtime: runtime2,
4598
+ statusSink,
4599
+ stableClientMsgId,
4600
+ tableMode
4601
+ });
4602
+ attemptHasOutbound = attemptHasOutbound || didSendFallback;
4603
+ if (didSendFallback) {
4604
+ markVisibleOutputSent();
4605
+ }
4606
+ }
4360
4607
  break;
4361
4608
  }
4362
4609
  if (!visibleOutputSent && !eventResultReported) {
@@ -4579,6 +4826,26 @@ function applySetupAccountConfig(params) {
4579
4826
  };
4580
4827
  }
4581
4828
 
4829
+ // src/group-adapter.ts
4830
+ function resolveGrixGroupRequireMention() {
4831
+ return false;
4832
+ }
4833
+ function resolveGrixGroupIntroHint() {
4834
+ return [
4835
+ "All Grix group messages are visible to you.",
4836
+ "If WasMentioned is true, you are the primary addressee and should respond.",
4837
+ "If WasMentioned is false, reply only when you add clear value; otherwise use NO_REPLY."
4838
+ ].join(" ");
4839
+ }
4840
+
4841
+ // src/group-tool-policy.ts
4842
+ var GROUP_DEFAULT_DENY = ["message"];
4843
+ function resolveGrixGroupToolPolicy() {
4844
+ return {
4845
+ deny: [...GROUP_DEFAULT_DENY]
4846
+ };
4847
+ }
4848
+
4582
4849
  // src/channel.ts
4583
4850
  var meta = {
4584
4851
  id: "grix",
@@ -4795,6 +5062,11 @@ var aibotPlugin = {
4795
5062
  hasRepliedRef
4796
5063
  })
4797
5064
  },
5065
+ groups: {
5066
+ resolveRequireMention: () => resolveGrixGroupRequireMention(),
5067
+ resolveGroupIntroHint: () => resolveGrixGroupIntroHint(),
5068
+ resolveToolPolicy: () => resolveGrixGroupToolPolicy()
5069
+ },
4798
5070
  agentPrompt: {
4799
5071
  messageToolHints: () => [
4800
5072
  "- Grix `action=unsend` is a silent cleanup action: unsend the target `messageId`, unsend the recall command message when applicable, then end with `NO_REPLY` and do not send any confirmation text. Omit `sessionId`/`to` only when targeting the current Grix chat."
@@ -4827,14 +5099,16 @@ var aibotPlugin = {
4827
5099
  }
4828
5100
  const sessionId = resolvedTarget.sessionId;
4829
5101
  const quotedMessageId = normalizeQuotedMessageId(replyToId);
5102
+ const envelope = buildAibotOutboundTextEnvelope(text);
4830
5103
  logAibotOutboundAdapter(
4831
- `sendText accountId=${account.accountId} rawTarget=${rawTarget} normalizedTarget=${resolvedTarget.normalizedTarget} resolvedSessionId=${sessionId} resolveSource=${resolvedTarget.resolveSource} textLen=${text.length} quotedMessageId=${quotedMessageId ?? "-"}`
5104
+ `sendText accountId=${account.accountId} rawTarget=${rawTarget} normalizedTarget=${resolvedTarget.normalizedTarget} resolvedSessionId=${sessionId} resolveSource=${resolvedTarget.resolveSource} textLen=${envelope.text.length} quotedMessageId=${quotedMessageId ?? "-"} cardKind=${envelope.cardKind ?? "none"}`
4832
5105
  );
4833
- const ack = await client.sendText(sessionId, text, {
4834
- quotedMessageId
5106
+ const ack = await client.sendText(sessionId, envelope.text, {
5107
+ quotedMessageId,
5108
+ extra: envelope.extra
4835
5109
  });
4836
5110
  logAibotOutboundAdapter(
4837
- `sendText ack accountId=${account.accountId} resolvedSessionId=${sessionId} messageId=${String(ack.msg_id ?? ack.client_msg_id ?? "-")}`
5111
+ `sendText ack accountId=${account.accountId} resolvedSessionId=${sessionId} messageId=${String(ack.msg_id ?? ack.client_msg_id ?? "-")} cardKind=${envelope.cardKind ?? "none"}`
4838
5112
  );
4839
5113
  return {
4840
5114
  channel: "grix",
@@ -6111,7 +6385,8 @@ async function createGrixApiAgent(params) {
6111
6385
  agentId,
6112
6386
  apiKeyPlaceholder: "<NEW_AGENT_API_KEY>"
6113
6387
  })}\``,
6114
- "Restart the gateway after adding the channel: `openclaw gateway restart`."
6388
+ "After binding the channel, apply agents/bindings/tools changes through `openclaw config set` or the bundled `grix_agent_bind.py configure-local-openclaw --apply` flow.",
6389
+ "Do not run `openclaw gateway restart` during an active install chat. Prefer a guarded apply flow that temporarily uses `gateway.reload.mode=hot` so the current conversation is not interrupted."
6115
6390
  ] : []
6116
6391
  };
6117
6392
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhf-openclaw/grix",
3
- "version": "0.4.14",
3
+ "version": "0.4.16",
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",
@@ -30,7 +30,8 @@ scripts/grix_agent_bind.py configure-local-openclaw \
30
30
  --apply
31
31
  ```
32
32
 
33
- 2. 可选执行检查:
33
+ 2. 绑定脚本会通过 `openclaw config set` 落配置,并在写入前临时把 `gateway.reload.mode` 切到 `hot`,避免当前安装对话被自动重启打断;不要在这条安装对话里追加 `openclaw gateway restart`。
34
+ 3. 执行完后必须再做一次检查:
34
35
 
35
36
  ```bash
36
37
  scripts/grix_agent_bind.py inspect-local-openclaw --agent-name <agent_name>
@@ -51,6 +52,7 @@ scripts/grix_agent_bind.py inspect-local-openclaw --agent-name <agent_name>
51
52
  2. 若缺失或为空,说明主通道还没完成,不做本模式,立刻切回 `grix-register`。
52
53
  3. 若已存在,再调用 `grix_agent_admin` 创建远端 agent(仅一次,不自动重试)。
53
54
  4. 创建成功后,执行本地绑定命令(同 Mode A)。
55
+ 5. 整个 `create-and-bind` 流程里不要执行 `openclaw gateway restart`;优先让绑定脚本用 `gateway.reload.mode=hot` 保护当前会话不被自动重启打断。
54
56
 
55
57
  ## Guardrails(两种模式都适用)
56
58
 
@@ -59,6 +61,8 @@ scripts/grix_agent_bind.py inspect-local-openclaw --agent-name <agent_name>
59
61
  3. 远端创建(Mode B)视为非幂等,不确认不自动重试。
60
62
  4. 完整 `api_key` 仅一次性回传,不要重复明文回显。
61
63
  5. 本地 `--apply` 没成功前,不得宣称配置完成。
64
+ 6. 安装私聊进行中时,禁止手工修改 `openclaw.json` 后再执行 `openclaw gateway restart`。
65
+ 7. 如果绑定脚本输出 `runtime_reload.restart_hint_detected=true`,说明当前 OpenClaw 版本仍提示“需要重启才生效”;此时不要自动重启,只能明确告诉用户“配置已写入,等待空闲时手动重启生效”。
62
66
 
63
67
  ## Error Handling Rules
64
68
 
@@ -79,15 +79,20 @@ After `code=0` (or when using `bind-local` mode), continue with local OpenClaw b
79
79
 
80
80
  1. apply local changes directly:
81
81
  - `scripts/grix_agent_bind.py configure-local-openclaw --agent-name <agent_name> --agent-id <agent_id> --api-endpoint '<api_endpoint>' --api-key '<api_key>' --apply`
82
- 2. optionally run inspect after apply when you need state verification:
82
+ 2. inspect after apply and use the result as the success gate:
83
83
  - `scripts/grix_agent_bind.py inspect-local-openclaw --agent-name <agent_name>`
84
+ 3. read `runtime_reload` from the apply result:
85
+ - `temporary_hot_mode=true` means the script temporarily guarded the write with `gateway.reload.mode=hot`
86
+ - `restart_hint_detected=true` means the running OpenClaw build still wants a later manual restart before the new config becomes live
84
87
 
85
- Local apply writes:
88
+ Local apply writes and validates:
86
89
 
87
90
  1. `agents.list` entry
88
91
  2. `channels.grix.accounts.<agent_name>` entry
89
92
  3. `bindings` route for `channel=grix`
90
- 4. required tools config and gateway restart
93
+ 4. required tools config
94
+ 5. the script temporarily guards the apply with `gateway.reload.mode=hot` so the install chat is not interrupted by auto-restart
95
+ 6. if `restart_hint_detected=true`, do not run `openclaw gateway restart` inside the install chat; tell the user the config is staged and needs a later manual restart to become live
91
96
 
92
97
  ## bind-local Input Contract
93
98
 
@@ -5,7 +5,6 @@ import os
5
5
  import shlex
6
6
  import subprocess
7
7
  import sys
8
- import uuid
9
8
 
10
9
 
11
10
  DEFAULT_OPENCLAW_CONFIG_PATH = "~/.openclaw/openclaw.json"
@@ -55,19 +54,6 @@ def load_json_file(path: str):
55
54
  return json.loads(raw)
56
55
 
57
56
 
58
- def write_json_file_with_backup(path: str, payload):
59
- os.makedirs(os.path.dirname(path), exist_ok=True)
60
- backup_path = ""
61
- if os.path.exists(path):
62
- backup_path = f"{path}.bak.{uuid.uuid4().hex[:8]}"
63
- with open(path, "rb") as src, open(backup_path, "wb") as dst:
64
- dst.write(src.read())
65
- with open(path, "w", encoding="utf-8") as handle:
66
- json.dump(payload, handle, ensure_ascii=False, indent=2)
67
- handle.write("\n")
68
- return backup_path
69
-
70
-
71
57
  def normalize_string_list(values):
72
58
  if not isinstance(values, list):
73
59
  return []
@@ -100,14 +86,23 @@ def redact_channel_account(account):
100
86
  return payload
101
87
 
102
88
 
89
+ def redact_accounts_map(accounts):
90
+ if not isinstance(accounts, dict):
91
+ return {}
92
+ return {
93
+ str(key): redact_channel_account(value if isinstance(value, dict) else {})
94
+ for key, value in accounts.items()
95
+ }
96
+
97
+
103
98
  def shell_command(cmd):
104
99
  return " ".join(shlex.quote(part) for part in cmd)
105
100
 
106
101
 
107
- def run_command_capture(cmd):
108
- proc = subprocess.run(cmd, capture_output=True, text=True)
102
+ def run_command_capture(cmd, env=None, display_command=None):
103
+ proc = subprocess.run(cmd, capture_output=True, text=True, env=env)
109
104
  return {
110
- "command": shell_command(cmd),
105
+ "command": display_command or shell_command(cmd),
111
106
  "returncode": proc.returncode,
112
107
  "stdout": proc.stdout.strip(),
113
108
  "stderr": proc.stderr.strip(),
@@ -122,8 +117,28 @@ def build_openclaw_base_cmd(args):
122
117
  return base_cmd
123
118
 
124
119
 
125
- def build_gateway_restart_command(args):
126
- return build_openclaw_base_cmd(args) + ["gateway", "restart"]
120
+ def build_openclaw_env(args):
121
+ env = os.environ.copy()
122
+ env["OPENCLAW_CONFIG_PATH"] = resolve_config_path(args)
123
+ return env
124
+
125
+
126
+ def build_openclaw_config_set_command(args, path: str, value):
127
+ return build_openclaw_base_cmd(args) + [
128
+ "config",
129
+ "set",
130
+ path,
131
+ json.dumps(value, ensure_ascii=False),
132
+ "--strict-json",
133
+ ]
134
+
135
+
136
+ def build_openclaw_config_validate_command(args):
137
+ return build_openclaw_base_cmd(args) + ["config", "validate"]
138
+
139
+
140
+ def build_openclaw_config_unset_command(args, path: str):
141
+ return build_openclaw_base_cmd(args) + ["config", "unset", path]
127
142
 
128
143
 
129
144
  def ensure_agent_entry(cfg, target_agent):
@@ -257,6 +272,172 @@ def ensure_required_tools(cfg):
257
272
  return next_cfg, changed
258
273
 
259
274
 
275
+ def resolve_reload_mode(cfg):
276
+ gateway = cfg.get("gateway") if isinstance(cfg, dict) else {}
277
+ if not isinstance(gateway, dict):
278
+ return ""
279
+ reload_cfg = gateway.get("reload")
280
+ if not isinstance(reload_cfg, dict):
281
+ return ""
282
+ return str(reload_cfg.get("mode", "")).strip()
283
+
284
+
285
+ def build_local_config_operations(next_cfg, change_flags, agent_name: str, target_account):
286
+ operations = []
287
+
288
+ if bool(change_flags.get("channel_account_updated")):
289
+ channels = next_cfg.get("channels") if isinstance(next_cfg, dict) else {}
290
+ grix = channels.get("grix") if isinstance(channels, dict) else {}
291
+ accounts = grix.get("accounts") if isinstance(grix, dict) else {}
292
+ operations.append(
293
+ {
294
+ "path": "channels.grix.enabled",
295
+ "value": bool(grix.get("enabled", True)),
296
+ }
297
+ )
298
+ operations.append(
299
+ {
300
+ "path": "channels.grix.accounts",
301
+ "value": dict(accounts or {}),
302
+ "display_value": redact_accounts_map(accounts or {}),
303
+ }
304
+ )
305
+
306
+ if bool(change_flags.get("agent_entry_updated")):
307
+ agents = next_cfg.get("agents") if isinstance(next_cfg, dict) else {}
308
+ operations.append(
309
+ {
310
+ "path": "agents.list",
311
+ "value": list(agents.get("list") or []),
312
+ }
313
+ )
314
+
315
+ if bool(change_flags.get("route_binding_updated")):
316
+ operations.append(
317
+ {
318
+ "path": "bindings",
319
+ "value": list(next_cfg.get("bindings") or []),
320
+ }
321
+ )
322
+
323
+ if bool(change_flags.get("tools_updated")):
324
+ tools = next_cfg.get("tools") if isinstance(next_cfg, dict) else {}
325
+ if not isinstance(tools, dict):
326
+ tools = {}
327
+ sessions = tools.get("sessions") if isinstance(tools.get("sessions"), dict) else {}
328
+ operations.extend(
329
+ [
330
+ {
331
+ "path": "tools.profile",
332
+ "value": str(tools.get("profile", "")).strip(),
333
+ },
334
+ {
335
+ "path": "tools.alsoAllow",
336
+ "value": normalize_string_list(tools.get("alsoAllow")),
337
+ },
338
+ {
339
+ "path": "tools.sessions.visibility",
340
+ "value": str(sessions.get("visibility", "")).strip(),
341
+ },
342
+ ]
343
+ )
344
+
345
+ return operations
346
+
347
+
348
+ def wrap_with_hot_reload_guard(cfg, operations):
349
+ if not operations:
350
+ return operations, {"before": resolve_reload_mode(cfg), "temporary_hot_mode": False}
351
+
352
+ previous_mode = resolve_reload_mode(cfg)
353
+ if previous_mode == "hot":
354
+ return operations, {"before": previous_mode, "temporary_hot_mode": False}
355
+
356
+ guarded_operations = [
357
+ {
358
+ "kind": "set",
359
+ "path": "gateway.reload.mode",
360
+ "value": "hot",
361
+ }
362
+ ]
363
+ guarded_operations.extend(operations)
364
+ if previous_mode:
365
+ guarded_operations.append(
366
+ {
367
+ "kind": "set",
368
+ "path": "gateway.reload.mode",
369
+ "value": previous_mode,
370
+ }
371
+ )
372
+ else:
373
+ guarded_operations.append(
374
+ {
375
+ "kind": "unset",
376
+ "path": "gateway.reload.mode",
377
+ }
378
+ )
379
+ return guarded_operations, {"before": previous_mode, "temporary_hot_mode": True}
380
+
381
+
382
+ def build_local_config_plan(args, operations):
383
+ plan = []
384
+ for operation in operations:
385
+ kind = str(operation.get("kind") or "set").strip() or "set"
386
+ path = str(operation.get("path") or "").strip()
387
+ if kind == "unset":
388
+ command = build_openclaw_config_unset_command(args, path)
389
+ plan.append(shell_command(command))
390
+ continue
391
+ display_value = operation.get("display_value", operation.get("value"))
392
+ command = build_openclaw_config_set_command(args, path, display_value)
393
+ plan.append(shell_command(command))
394
+ plan.append(shell_command(build_openclaw_config_validate_command(args)))
395
+ return plan
396
+
397
+
398
+ def apply_local_config_operations(args, operations):
399
+ env = build_openclaw_env(args)
400
+ command_results = []
401
+
402
+ for operation in operations:
403
+ kind = str(operation.get("kind") or "set").strip() or "set"
404
+ path = str(operation.get("path") or "").strip()
405
+ if kind == "unset":
406
+ command = build_openclaw_config_unset_command(args, path)
407
+ display_command = shell_command(command)
408
+ result = run_command_capture(command, env=env, display_command=display_command)
409
+ command_results.append(result)
410
+ if result["returncode"] != 0:
411
+ raise BindError(
412
+ f"openclaw config unset failed for {path}",
413
+ payload={"command_results": command_results},
414
+ )
415
+ continue
416
+
417
+ value = operation.get("value")
418
+ display_value = operation.get("display_value", value)
419
+ command = build_openclaw_config_set_command(args, path, value)
420
+ display_command = shell_command(build_openclaw_config_set_command(args, path, display_value))
421
+ result = run_command_capture(command, env=env, display_command=display_command)
422
+ command_results.append(result)
423
+ if result["returncode"] != 0:
424
+ raise BindError(
425
+ f"openclaw config set failed for {path}",
426
+ payload={"command_results": command_results},
427
+ )
428
+
429
+ validate_command = build_openclaw_config_validate_command(args)
430
+ validate_result = run_command_capture(validate_command, env=env)
431
+ command_results.append(validate_result)
432
+ if validate_result["returncode"] != 0:
433
+ raise BindError(
434
+ "openclaw config validate failed",
435
+ payload={"command_results": command_results},
436
+ )
437
+
438
+ return command_results
439
+
440
+
260
441
  def resolve_default_model(cfg, current_agent):
261
442
  if isinstance(current_agent, dict):
262
443
  model = str(current_agent.get("model", "")).strip()
@@ -449,6 +630,10 @@ def handle_configure_local(args):
449
630
  change_flags["tools_updated"] = changed
450
631
 
451
632
  needs_update = any(bool(value) for value in change_flags.values())
633
+ base_apply_operations = build_local_config_operations(
634
+ next_cfg, change_flags, agent_name, target_account
635
+ )
636
+ apply_operations, reload_guard = wrap_with_hot_reload_guard(cfg, base_apply_operations)
452
637
 
453
638
  payload = {
454
639
  "ok": True,
@@ -483,23 +668,33 @@ def handle_configure_local(args):
483
668
  },
484
669
  },
485
670
  },
486
- "planned_apply_commands": [] if bool(args.skip_gateway_restart) else [shell_command(build_gateway_restart_command(args))],
671
+ "planned_apply_commands": build_local_config_plan(args, apply_operations),
672
+ "runtime_reload": {
673
+ "mode": "guarded_hot_reload",
674
+ "previous_mode": reload_guard.get("before") or "default",
675
+ "temporary_hot_mode": bool(reload_guard.get("temporary_hot_mode")),
676
+ "summary": "The script temporarily switches gateway.reload.mode to hot while applying config so the active install chat is not interrupted by an automatic restart.",
677
+ },
487
678
  }
488
679
 
489
680
  if args.apply:
490
- backup_path = ""
491
- if needs_update:
492
- backup_path = write_json_file_with_backup(config_path, next_cfg)
493
681
  created_paths = []
494
682
  created_paths.extend(build_workspace_files(workspace, agent_name))
495
683
  os.makedirs(agent_dir, exist_ok=True)
496
684
 
497
685
  command_results = []
498
- if not bool(args.skip_gateway_restart):
499
- command_results.append(run_command_capture(build_gateway_restart_command(args)))
686
+ if needs_update:
687
+ command_results = apply_local_config_operations(args, apply_operations)
688
+ else:
689
+ command_results = [
690
+ run_command_capture(
691
+ build_openclaw_config_validate_command(args),
692
+ env=build_openclaw_env(args),
693
+ )
694
+ ]
500
695
  if command_results[-1]["returncode"] != 0:
501
696
  raise BindError(
502
- "openclaw gateway restart failed",
697
+ "openclaw config validate failed",
503
698
  payload={"command_results": command_results},
504
699
  )
505
700
 
@@ -507,10 +702,15 @@ def handle_configure_local(args):
507
702
  applied_state = extract_current_state(applied_cfg, agent_name)
508
703
  payload["config_write"] = {
509
704
  "changed": needs_update,
510
- "backup_path": backup_path,
705
+ "mode": "openclaw config set",
706
+ "applied_paths": [item.get("path") for item in apply_operations],
511
707
  }
512
708
  payload["created_workspace_files"] = created_paths
513
709
  payload["command_results"] = command_results
710
+ payload["runtime_reload"]["restart_hint_detected"] = any(
711
+ "restart the gateway to apply" in f"{item.get('stdout', '')} {item.get('stderr', '')}".lower()
712
+ for item in command_results
713
+ )
514
714
  payload["applied_state"] = {
515
715
  "agent_entry": (applied_state or {}).get("agent_entry"),
516
716
  "channel_account": redact_channel_account((applied_state or {}).get("channel_account") or {}),
@@ -550,7 +750,6 @@ def build_parser():
550
750
  configure_local.add_argument("--workspace", default="")
551
751
  configure_local.add_argument("--agent-dir", default="")
552
752
  configure_local.add_argument("--skip-tools-update", action="store_true")
553
- configure_local.add_argument("--skip-gateway-restart", action="store_true")
554
753
  configure_local.add_argument("--apply", action="store_true")
555
754
  configure_local.set_defaults(handler=handle_configure_local)
556
755
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: grix-egg
3
- description: 在虾塘触发的安装私聊中处理 egg 安装。适用于主 OpenClaw agent 收到包含 install_id、egg、install、main_agent 的安装上下文时,负责与用户多轮确认、执行 persona.zip OpenClaw agent skill.zip Claude agent 的正规安装流程,并在当前私聊里持续回报进度、失败原因和最终结果。
3
+ description: 在虾塘触发的安装私聊中处理 egg 安装。适用于主 OpenClaw agent 收到包含 install_id、egg、install、main_agent 的安装上下文时,按 install.route 指定的路线执行安装:把 persona/openclaw 包安装到 OpenClaw agent,或把 skill.zip 安装到 Claude agent,并在当前私聊里持续回报进度、失败原因和最终结果。
4
4
  ---
5
5
 
6
6
  # Egg Install
@@ -9,24 +9,34 @@ description: 在虾塘触发的安装私聊中处理 egg 安装。适用于主 O
9
9
 
10
10
  这类会话通常会先收到一条系统代发的安装请求,正文里会带一个 JSON 上下文块,至少包含:
11
11
 
12
- - `install_id` — 安装单唯一 ID,所有状态指令必须使用原值
12
+ - `install_id` — 安装单唯一 ID,所有状态消息必须使用原值
13
13
  - `egg` — egg 包标识,格式为 `<name>@<version>` 或带完整下载 URL 的对象
14
- - `install` — 安装模式,枚举值:`create_new`(新建目标 agent)或 `existing_agent`(安装到已有 agent)
14
+ - `install.mode` — 安装模式,枚举值:`create_new`(新建目标 agent)或 `existing_agent`(安装到已有 agent)
15
+ - `install.route` — 当前实际路线,优先使用它判断要走哪条分支。当前可见值:
16
+ - `openclaw_create_new`
17
+ - `openclaw_existing`
18
+ - `claude_existing`
19
+ - `install.target_client_type` — 本次目标类型,通常为 `openclaw` 或 `claude`
20
+ - `install.artifact_package` — 本次应安装的包类型,通常为 `persona_zip` 或 `skill_zip`
21
+ - `install.suggested_agent_name` — `openclaw_create_new` 时优先使用的默认短名字;若缺失,再按 egg 标题或 egg ID 自行取一个简短名字继续,不要回头追问用户命名和用途
15
22
  - `main_agent` — 发起安装的主 OpenClaw agent ID,安装完成后如需通知或回调,使用该 ID
16
23
 
17
24
  你的职责不是返回隐藏协议结果,而是直接在当前私聊里把安装流程做完。
18
25
 
19
26
  ## 工作方式
20
27
 
21
- 1. 先读完整个 JSON 上下文,确认 egg、版本、安装模式和目标类型。
22
- 2. 在真正动手前,先在当前私聊里和用户确认关键目标。
23
- 3. `create_new` 时,先确认新 agent 的命名和用途,再继续创建。
24
- 4. `existing_agent` 时,只能操作上下文里指定的目标 agent,不要私自换目标。
28
+ 1. 先读完整个 JSON 上下文,优先确认 `install.route`、`install.target_client_type`、`install.artifact_package`。
29
+ 2. 默认直接按上下文执行;只有缺少关键信息、上下文互相冲突或执行被阻塞时,才在当前私聊里向用户确认。
30
+ 3. `openclaw_create_new` 时,优先使用 `install.suggested_agent_name` 作为新 OpenClaw agent 的默认短名字;若缺失,再按 egg 标题或 egg ID 自行取一个简短名字继续创建,不要回头追问用户命名和用途。
31
+ 4. `openclaw_existing` / `claude_existing` 时,只能操作上下文里指定的目标 agent,不要私自换目标;如果目标已经明确,就直接继续,不要重复确认。
25
32
  5. 安装过程中,每完成一个关键动作就用自然语言回报一次进度。
26
- 6. "包下载完成"和"安装内容落位完成待校验"两个节点,各额外发送一条 `status=running` 的**独立安装状态指令消息**。
27
- 7. 最终成功或失败时,必须发送一条 `status=success` 或 `status=failed` 的**独立安装状态指令消息**。
28
- 8. 出错时,直接说明失败点、影响和下一步建议,不要模糊带过。
29
- 9. 最终明确告诉用户:装到了哪个 agent、结果成功还是失败、后续是否还要继续操作。
33
+ 6. `openclaw_create_new` 路线在"远端 agent 创建完成"后,必须额外发送一条 `status=running`、`step=agent_created` 的**独立结构化安装状态消息**。
34
+ 7. 在"包下载完成"和"安装内容落位完成待校验"两个节点,各额外发送一条 `status=running` 的**独立结构化安装状态消息**。
35
+ 8. 最终成功或失败时,必须发送一条 `status=success` 或 `status=failed` 的**独立结构化安装状态消息**。
36
+ 9. 安装成功后,必须再单独发送一条目标 agent 的**结构化资料卡消息**,并且 `peer_type=2`。
37
+ 10. 发送完资料卡后,再发一条普通文字,明确告诉用户可以点开资料卡查看 agent 资料,并从资料页继续与它对话。
38
+ 11. 出错时,直接说明失败点、影响和下一步建议,不要模糊带过。
39
+ 12. 最终明确告诉用户:装到了哪个 agent、结果成功还是失败、后续是否还要继续操作。
30
40
 
31
41
  ## 绝对规则
32
42
 
@@ -36,37 +46,62 @@ description: 在虾塘触发的安装私聊中处理 egg 安装。适用于主 O
36
46
  - 所有远端 API 通讯都必须走统一工具入口:`grix_query` / `grix_group` / `grix_agent_admin`,禁止在对话里自行发 HTTP 请求。
37
47
  - 禁止使用 `curl`、`fetch`、`axios` 或临时脚本直连 `/v1/agent-api`。
38
48
  - 单个业务动作只调用一次对应工具,失败后先说明原因,不要静默重试。
39
- - `persona.zip` 只能面向 OpenClaw 目标。
40
- - `skill.zip` 只能面向 Claude 目标。
49
+ - 必须以 `install.route` 为准执行,不要自己重新选路线。
50
+ - `openclaw_create_new` / `openclaw_existing` 只能安装 persona/openclaw 包。
51
+ - `claude_existing` 只能安装 skill.zip。
41
52
  - 不要自动新建 Claude 目标 agent。
42
53
  - 没完成校验前,绝不能宣称安装成功。
43
54
  - 如果新建目标后又失败了,能安全回滚就先回滚;不能回滚就如实告诉用户当前残留状态。
44
- - 最终成功或失败时,必须发送一条独立的 `egg-install-status` 指令消息。
45
- - 状态指令消息必须单独发送,不要和自然语言解释混在同一条里。
46
- - 用户拒绝确认或主动取消时,必须发送 `status=failed`、`error_code=user_cancelled` 的状态指令后再结束。
55
+ - 上下文已经给出 `install.target_agent_id` 或 `install.suggested_agent_name` 时,直接继续执行,不要再向用户确认目标、命名或用途;只有信息缺失、冲突或执行阻塞时才提问。
56
+ - 安装私聊进行中时,禁止执行 `openclaw gateway restart`;本流程涉及的 `channels.*`、`agents`、`bindings`、`tools` 配置必须先切到 `gateway.reload.mode=hot` 再写入,避免自动重启打断当前任务。
57
+ - 最终成功或失败时,必须发送一条独立的结构化安装状态消息。
58
+ - 安装成功后,必须按顺序继续发送:目标 agent 的结构化资料卡消息,然后再发一条普通文字的下一步指引。
59
+ - 结构化安装状态消息必须单独发送,不要和自然语言解释混在同一条里。
60
+ - 用户拒绝确认或主动取消时,必须发送 `status=failed`、`error_code=user_cancelled` 的结构化状态消息后再结束。
47
61
 
48
- ## 安装状态指令
62
+ ## 安装状态消息(OpenClaw ReplyPayload 风格)
49
63
 
50
- server 不会猜自然语言。要让安装单进入"进行中 / 成功 / 失败",你必须发送这类单行消息:
64
+ server 不会猜自然语言。要让安装单进入"进行中 / 成功 / 失败",你必须单独发送一条**单行 JSON**,格式参考 OpenClaw 的 ReplyPayload:
51
65
 
52
- ```text
53
- [[egg-install-status|install_id=<INSTALL_ID>|status=<running|success|failed>|step=<STEP>|summary=<URI_ENCODED_SUMMARY>]]
66
+ ```json
67
+ {"text":"<给用户看的摘要>","channelData":{"grix":{"eggInstall":{"install_id":"<INSTALL_ID>","status":"<running|success|failed>","step":"<STEP>","summary":"<与 text 一致或更精确的摘要>"}}}}
54
68
  ```
55
69
 
56
70
  常用可选字段:
57
71
 
58
- - `target_agent_id=<AGENT_ID>`:成功时尽量带上,尤其是 `create_new`。
59
- - `detail_text=<URI_ENCODED_DETAIL>`:补充更长说明。
60
- - `error_code=<ERROR_CODE>`:失败时建议带上。
61
- - `error_msg=<URI_ENCODED_ERROR_MSG>`:失败时建议带上。
72
+ - `target_agent_id`:成功时尽量带上,尤其是 `create_new`。
73
+ - `detail_text`:补充更长说明。
74
+ - `error_code`:失败时建议带上。
75
+ - `error_msg`:失败时建议带上。
62
76
 
63
77
  规则:
64
78
 
65
79
  1. `install_id` 必须使用上下文里的原值。
66
80
  2. `status` 只能是 `running`、`success`、`failed`。
67
- 3. `summary`、`detail_text`、`error_msg` 如果有空格、中文或特殊字符,按 URI component 编码。
68
- 4. 这条指令只负责状态收口;如果要跟用户解释原因,另发一条正常文字消息。
69
- 5. `create_new` 成功时,必须尽量带 `target_agent_id`,否则 server 可能无法通过最终校验。
81
+ 3. `text` 必须是给用户看的简短摘要;`summary` 应与 `text` 一致,或在不冲突的前提下更精确。
82
+ 4. 不要做 URI 编码;直接输出合法 JSON 字符串。
83
+ 5. 这条消息只负责状态收口;如果要跟用户解释原因,另发一条正常文字消息。
84
+ 6. 顶层只放 `text` 和 `channelData`,不要自己拼前端内部 `biz_card`。
85
+ 7. 这条 JSON 必须单独发送,不要前后夹带自然语言。
86
+ 8. `openclaw_create_new` 成功时,必须尽量带 `target_agent_id`,否则 server 可能无法通过最终校验。
87
+ 9. 如果上下文缺少 `install.route` 但仍有 `install.mode` 和目标 agent 信息,先按上下文能明确推出的路线执行;若仍有歧义,先在当前私聊里确认,再继续。
88
+
89
+ ## Agent 资料卡消息(OpenClaw ReplyPayload 风格)
90
+
91
+ 安装成功后,必须再单独发送一条 agent 资料卡消息,格式如下:
92
+
93
+ ```json
94
+ {"text":"查看 Agent 资料","channelData":{"grix":{"userProfile":{"user_id":"<TARGET_AGENT_ID>","peer_type":2,"nickname":"<AGENT_NAME>","avatar_url":"<可选>"}}}}
95
+ ```
96
+
97
+ 规则:
98
+
99
+ 1. `user_id` 必须使用最终目标 agent 的 ID。
100
+ 2. `peer_type` 必须固定为 `2`。
101
+ 3. `nickname` 必须使用目标 agent 的显示名称。
102
+ 4. `avatar_url` 有就带,没有可以省略。
103
+ 5. 这条 JSON 也必须单独发送,不要和解释文字混在一起。
104
+ 6. 发完资料卡后,再补一条普通文字,告诉用户可以点开资料卡查看资料,并从资料页继续与该 agent 对话。
70
105
 
71
106
  ## 统一 API 请求机制
72
107
 
@@ -84,33 +119,41 @@ server 不会猜自然语言。要让安装单进入"进行中 / 成功 / 失败
84
119
 
85
120
  ## 推荐流程
86
121
 
87
- ### `persona.zip` -> OpenClaw
88
-
89
- 1. 读取上下文,确认是 `create_new` 还是 `existing_agent`。
90
- 2. 和用户确认目标 agent 或新 agent 命名;**用户拒绝则发 `failed/user_cancelled` 指令后结束**。
91
- 3. 如果需要新建远端 API agent,用 `grix_agent_admin` 创建。
92
- 4. OpenClaw 正规步骤准备本地目标目录和配置。
93
- 5. 下载 egg 包,并校验 hash / manifest(如果上下文提供)。
94
- 6. 发送 `status=running`、`step=downloaded` 状态指令。
95
- 7. 安装 persona 内容。
96
- 8. 发送 `status=running`、`step=installed` 状态指令。
97
- 9. 按需刷新或重启本地运行时。
98
- 10. 校验目标 agent 仍然可用。
99
- - 校验失败 尝试回滚(含步骤3新建的远端 agent),无法回滚则如实告知残留状态 `failed` 指令后结束。
100
- 11. 发送 `status=success` 状态指令(带 `target_agent_id`),再向用户汇报完成。
101
-
102
- ### `skill.zip` -> Claude
103
-
104
- 1. 确认上下文指定的 Claude 目标 agent 存在;**不存在则发 `failed/target_not_found` 指令后结束**。
105
- 2. 和用户确认目标 agent;**用户拒绝则发 `failed/user_cancelled` 指令后结束**。
122
+ ### `openclaw_create_new` / `openclaw_existing`
123
+
124
+ 1. 读取上下文,确认 route 是 `openclaw_create_new` 还是 `openclaw_existing`。
125
+ 2. 如果 route=`openclaw_create_new`,直接使用 `install.suggested_agent_name` 作为默认短名字;若缺失,再按 egg 标题或 egg ID 自行取一个简短名字继续。只有名字或目标信息真的缺失、冲突或执行被阻塞时,才向用户确认;**用户主动取消时发 `failed/user_cancelled` 结构化状态消息后结束**。
126
+ 3. 如果 route=`openclaw_existing`,直接使用 `install.target_agent_id` 指定的目标 agent 继续,不要重复确认目标。
127
+ 4. 如果 route=`openclaw_create_new`,需要新建远端 API agent,用 `grix_agent_admin` 创建。
128
+ 5. 如果 route=`openclaw_create_new` 且远端 agent 已创建成功,立即发送 `status=running`、`step=agent_created` 结构化状态消息。
129
+ 6. OpenClaw 正规步骤准备本地目标目录和配置。
130
+ 7. 下载 persona/openclaw 包,并校验 hash / manifest(如果上下文提供)。
131
+ 8. 发送 `status=running`、`step=downloaded` 结构化状态消息。
132
+ 9. 安装 persona/openclaw 内容。
133
+ 10. 发送 `status=running`、`step=installed` 结构化状态消息。
134
+ 11. OpenClaw 正规配置方式落地本地变更:优先执行 `scripts/grix_agent_bind.py configure-local-openclaw --apply`。该脚本会在写入前临时把 `gateway.reload.mode` 切到 `hot`,避免当前安装对话被自动重启打断;不要手工改完 `openclaw.json` 后再重启 gateway。
135
+ 12. 校验目标 agent 仍然可用。
136
+ - 如果绑定脚本结果里 `runtime_reload.restart_hint_detected=true`,说明当前 OpenClaw 版本仍要求后续重启才能真正启用新配置。此时不要自动重启;如实告诉用户“配置已写入,等待空闲时手动重启生效”,并发 `status=failed`、`error_code=manual_restart_required` 的结构化状态消息后结束。
137
+ - 其他校验失败 → 尝试回滚(含步骤4新建的远端 agent),无法回滚则如实告知残留状态 → 发 `failed` 结构化状态消息后结束。
138
+ 13. 发送 `status=success` 结构化状态消息(带 `target_agent_id`)。
139
+ 14. 单独发送目标 agent 的结构化资料卡消息。
140
+ 15. 再发一条普通文字,告诉用户可以点开资料卡查看 agent 资料,并从资料页继续与它对话。
141
+
142
+ ### `claude_existing`
143
+
144
+ 1. 确认上下文 route 为 `claude_existing`,且指定的 Claude 目标 agent 存在;**不存在则发 `failed/target_not_found` 结构化状态消息后结束**。
145
+ 2. 直接使用 `install.target_agent_id` 指定的 Claude 目标 agent 继续安装,不要重复确认目标;只有目标信息缺失、冲突或执行被阻塞时,才向用户确认。**用户主动取消时发 `failed/user_cancelled` 结构化状态消息后结束**。
106
146
  3. 下载 skill 包,并校验 hash / manifest(如果上下文提供)。
107
- 4. 发送 `status=running`、`step=downloaded` 状态指令。
147
+ 4. 发送 `status=running`、`step=downloaded` 结构化状态消息。
108
148
  5. 用 Claude 正规步骤安装 skill 包。
109
- 6. 发送 `status=running`、`step=installed` 状态指令。
110
- 7. 按需刷新配置或重载运行时。
149
+ 6. 发送 `status=running`、`step=installed` 结构化状态消息。
150
+ 7. 如需写 OpenClaw 配置,只能走“先切 `gateway.reload.mode=hot` 再写入”的路径;不要在安装对话中执行 `openclaw gateway restart`。
111
151
  8. 校验目标 agent 仍然可用。
112
- - 校验失败 如实告知用户 → 发 `failed` 指令后结束。
113
- 9. 发送 `status=success` 状态指令(带 `target_agent_id`),再向用户汇报完成。
152
+ - 如果绑定脚本结果里 `runtime_reload.restart_hint_detected=true`,说明当前版本仍需要后续手动重启才能生效。不要自动重启;明确告诉用户配置已写入但待空闲时生效,并发 `failed/manual_restart_required` 结构化状态消息后结束。
153
+ - 其他校验失败 → 如实告知用户 → 发 `failed` 结构化状态消息后结束。
154
+ 9. 发送 `status=success` 结构化状态消息(带 `target_agent_id`)。
155
+ 10. 单独发送目标 agent 的结构化资料卡消息。
156
+ 11. 再发一条普通文字,告诉用户可以点开资料卡查看 agent 资料,并从资料页继续与它对话。
114
157
 
115
158
  ## 每次安装至少校验这些点
116
159
 
@@ -119,43 +162,56 @@ server 不会猜自然语言。要让安装单进入"进行中 / 成功 / 失败
119
162
  - hash / manifest 校验通过(如果提供)
120
163
  - 安装内容已经落到目标位置
121
164
  - 目标 agent 安装后仍然可用
165
+ - 实际安装路线没有偏离 `install.route`
122
166
 
123
- ## 指令示例
167
+ ## 消息示例
168
+
169
+ 进行中(远端 agent 创建完成):
170
+
171
+ ```json
172
+ {"text":"已创建远端 Agent","channelData":{"grix":{"eggInstall":{"install_id":"eggins_20370001","status":"running","step":"agent_created","summary":"已创建远端 Agent"}}}}
173
+ ```
124
174
 
125
175
  进行中(下载完成):
126
176
 
127
- ```text
128
- [[egg-install-status|install_id=eggins_20370001|status=running|step=downloaded|summary=%E5%B7%B2%E4%B8%8B%E8%BD%BD%E5%B9%B6%E9%AA%8C%E8%AF%81%E5%AE%89%E8%A3%85%E5%8C%85]]
177
+ ```json
178
+ {"text":"已下载并验证安装包","channelData":{"grix":{"eggInstall":{"install_id":"eggins_20370001","status":"running","step":"downloaded","summary":"已下载并验证安装包"}}}}
129
179
  ```
130
180
 
131
181
  进行中(安装落位完成):
132
182
 
133
- ```text
134
- [[egg-install-status|install_id=eggins_20370001|status=running|step=installed|summary=%E5%AE%89%E8%A3%85%E5%86%85%E5%AE%B9%E5%B7%B2%E8%90%BD%E4%BD%8D%EF%BC%8C%E6%A0%A1%E9%AA%8C%E4%B8%AD]]
183
+ ```json
184
+ {"text":"安装内容已落位,校验中","channelData":{"grix":{"eggInstall":{"install_id":"eggins_20370001","status":"running","step":"installed","summary":"安装内容已落位,校验中"}}}}
135
185
  ```
136
186
 
137
187
  成功:
138
188
 
139
- ```text
140
- [[egg-install-status|install_id=eggins_20370001|status=success|step=completed|target_agent_id=2035123456789012345|summary=%E5%B7%B2%E5%AE%8C%E6%88%90%E5%AE%89%E8%A3%85]]
189
+ ```json
190
+ {"text":"已完成安装","channelData":{"grix":{"eggInstall":{"install_id":"eggins_20370001","status":"success","step":"completed","target_agent_id":"2035123456789012345","summary":"已完成安装"}}}}
191
+ ```
192
+
193
+ 成功后的资料卡:
194
+
195
+ ```json
196
+ {"text":"查看 Agent 资料","channelData":{"grix":{"userProfile":{"user_id":"2035123456789012345","peer_type":2,"nickname":"writer-openclaw"}}}}
141
197
  ```
142
198
 
143
199
  失败(用户取消):
144
200
 
145
- ```text
146
- [[egg-install-status|install_id=eggins_20370001|status=failed|step=user_cancelled|error_code=user_cancelled|summary=%E7%94%A8%E6%88%B7%E5%8F%96%E6%B6%88%E5%AE%89%E8%A3%85]]
201
+ ```json
202
+ {"text":"用户取消安装","channelData":{"grix":{"eggInstall":{"install_id":"eggins_20370001","status":"failed","step":"user_cancelled","error_code":"user_cancelled","summary":"用户取消安装"}}}}
147
203
  ```
148
204
 
149
205
  失败(目标不存在):
150
206
 
151
- ```text
152
- [[egg-install-status|install_id=eggins_20370001|status=failed|step=target_not_found|error_code=target_not_found|error_msg=%E6%8C%87%E5%AE%9A%E7%9A%84%20Claude%20agent%20%E4%B8%8D%E5%AD%98%E5%9C%A8|summary=%E5%AE%89%E8%A3%85%E5%A4%B1%E8%B4%A5]]
207
+ ```json
208
+ {"text":"安装失败","channelData":{"grix":{"eggInstall":{"install_id":"eggins_20370001","status":"failed","step":"target_not_found","error_code":"target_not_found","error_msg":"指定的 Claude agent 不存在","summary":"安装失败"}}}}
153
209
  ```
154
210
 
155
211
  失败(下载失败):
156
212
 
157
- ```text
158
- [[egg-install-status|install_id=eggins_20370001|status=failed|step=download_failed|error_code=download_failed|error_msg=%E4%B8%8B%E8%BD%BD%E5%AE%89%E8%A3%85%E5%8C%85%E5%A4%B1%E8%B4%A5|summary=%E5%AE%89%E8%A3%85%E5%A4%B1%E8%B4%A5]]
213
+ ```json
214
+ {"text":"安装失败","channelData":{"grix":{"eggInstall":{"install_id":"eggins_20370001","status":"failed","step":"download_failed","error_code":"download_failed","error_msg":"下载安装包失败","summary":"安装失败"}}}}
159
215
  ```
160
216
 
161
217
  ## 回复风格
@@ -2,14 +2,14 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- Unify remote API communication in `egg-install` with the same typed tool pathway used by other grix-admin skills.
5
+ Unify remote API communication in `grix-egg` with the same typed tool pathway used by other grix-admin skills.
6
6
 
7
7
  ## Base Rules
8
8
 
9
9
  1. Base path is `/v1/agent-api`.
10
10
  2. Auth is `Authorization: Bearer <agent_api_key>`.
11
11
  3. Caller must be `provider_type=3` and `status=active`.
12
- 4. `egg-install` must not send direct HTTP requests to Grix by itself.
12
+ 4. `grix-egg` must not send direct HTTP requests to Grix by itself.
13
13
 
14
14
  ## Unified Tool Path
15
15
 
@@ -24,6 +24,8 @@ Use only these entry points for remote communication:
24
24
  Local binding remains a local operation via bundled script:
25
25
 
26
26
  - `scripts/grix_agent_bind.py configure-local-openclaw ... --apply`
27
+ - 该脚本会先临时切换 `gateway.reload.mode=hot`,再通过 `openclaw config set` 落配置,避免安装私聊被自动重启打断
28
+ - 如果脚本结果里 `runtime_reload.restart_hint_detected=true`,说明当前版本仍要求后续手动重启才能真正生效;安装私聊里不要执行 `openclaw gateway restart`
27
29
 
28
30
  ## Prohibited Paths
29
31
 
@@ -35,4 +37,5 @@ Local binding remains a local operation via bundled script:
35
37
 
36
38
  1. Scope/auth/parameter errors: no automatic retry.
37
39
  2. Transient network failure: at most one retry, and only after explicit confirmation.
38
- 3. Installation status directives (`egg-install-status`) must still be emitted on terminal success/failure.
40
+ 3. Installation status payloads (`channelData.grix.eggInstall`) must still be emitted on terminal success/failure.
41
+ 4. On terminal success, emit one additional agent profile payload (`channelData.grix.userProfile`) for the final target agent before sending the plain-language next-step guidance.
@@ -14,7 +14,7 @@ description: 仅用于初次安装阶段,完成 Grix 环境的账号注册/登
14
14
 
15
15
  1. 本技能**只能**做账号与云端 Agent 参数准备。
16
16
  2. 本技能**不能**执行 `openclaw` 命令,也不能修改本地 `openclaw.json`。
17
- 3. 涉及本地配置、插件安装、工具权限、重启网关,一律交给 `grix-admin`。
17
+ 3. 涉及本地配置、插件安装、工具权限、热加载校验,一律交给 `grix-admin`。
18
18
 
19
19
  ### 1. 询问邮箱并发送验证码
20
20
 
@@ -21,4 +21,4 @@
21
21
  1. `mode` 固定为 `bind-local`。
22
22
  2. `agent_name`、`agent_id`、`api_endpoint`、`api_key` 必填。
23
23
  3. `grix-register` 只负责生成以上参数,不执行本地配置命令。
24
- 4. 本地写入、插件处理、工具权限、gateway 重启都由 `grix-admin` 负责。
24
+ 4. 本地写入、插件处理、工具权限、热加载校验都由 `grix-admin` 负责。
@@ -1,6 +1,6 @@
1
1
  # OpenClaw Setup Ownership
2
2
 
3
3
  `grix-register` 不负责 OpenClaw 本地配置。
4
- 所有 OpenClaw 配置相关动作(插件安装、channel 写入、tools 权限、gateway 重启)都属于 `grix-admin`。
4
+ 所有 OpenClaw 配置相关动作(插件安装、channel 写入、tools 权限、热加载校验)都属于 `grix-admin`。
5
5
 
6
6
  本目录保留此文件仅用于职责说明,避免误用。