@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 +338 -63
- package/package.json +1 -1
- package/skills/grix-admin/SKILL.md +5 -1
- package/skills/grix-admin/references/api-contract.md +8 -3
- package/skills/grix-admin/scripts/grix_agent_bind.py +227 -28
- package/skills/grix-egg/SKILL.md +120 -64
- package/skills/grix-egg/references/api-contract.md +6 -3
- package/skills/grix-register/SKILL.md +1 -1
- package/skills/grix-register/references/handoff-contract.md +1 -1
- package/skills/grix-register/references/openclaw-setup.md +1 -1
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
|
|
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
|
-
|
|
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
|
|
2916
|
-
const rawText =
|
|
2917
|
-
|
|
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
|
-
|
|
2922
|
-
|
|
2910
|
+
let embeddedReply;
|
|
2911
|
+
try {
|
|
2912
|
+
embeddedReply = JSON.parse(rawText);
|
|
2913
|
+
} catch {
|
|
2923
2914
|
return null;
|
|
2924
2915
|
}
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
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:
|
|
2944
|
-
status:
|
|
2945
|
-
step:
|
|
2946
|
-
summary:
|
|
2947
|
-
detail_text:
|
|
2948
|
-
target_agent_id:
|
|
2949
|
-
error_code:
|
|
2950
|
-
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) ??
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 (
|
|
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
|
-
"
|
|
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
|
@@ -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.
|
|
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
|
|
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
|
|
126
|
-
|
|
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":
|
|
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
|
|
499
|
-
command_results
|
|
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
|
|
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
|
-
"
|
|
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
|
|
package/skills/grix-egg/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: grix-egg
|
|
3
|
-
description: 在虾塘触发的安装私聊中处理 egg 安装。适用于主 OpenClaw agent 收到包含 install_id、egg、install、main_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
|
|
22
|
-
2.
|
|
23
|
-
3. `
|
|
24
|
-
4. `
|
|
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.
|
|
27
|
-
7.
|
|
28
|
-
8.
|
|
29
|
-
9.
|
|
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
|
-
- `
|
|
40
|
-
- `
|
|
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
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
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
|
-
```
|
|
53
|
-
|
|
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
|
|
59
|
-
- `detail_text
|
|
60
|
-
- `error_code
|
|
61
|
-
- `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
|
|
68
|
-
4.
|
|
69
|
-
5.
|
|
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
|
-
### `
|
|
88
|
-
|
|
89
|
-
1.
|
|
90
|
-
2.
|
|
91
|
-
3.
|
|
92
|
-
4.
|
|
93
|
-
5.
|
|
94
|
-
6.
|
|
95
|
-
7.
|
|
96
|
-
8. 发送 `status=running`、`step=
|
|
97
|
-
9.
|
|
98
|
-
10.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
-
|
|
113
|
-
|
|
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
|
-
```
|
|
128
|
-
|
|
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
|
-
```
|
|
134
|
-
|
|
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
|
-
```
|
|
140
|
-
|
|
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
|
-
```
|
|
146
|
-
|
|
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
|
-
```
|
|
152
|
-
|
|
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
|
-
```
|
|
158
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|