dsh-caduceus 0.1.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/README.zh.md +9 -0
- package/dist/bridge/bridge.d.ts +9 -0
- package/dist/bridge/bridge.js +155 -5
- package/dist/bridge/types.d.ts +22 -0
- package/dist/commands/parse.d.ts +0 -11
- package/dist/commands/parse.js +5 -1
- package/dist/config.d.ts +52 -1
- package/dist/config.js +29 -0
- package/dist/gate.d.ts +3 -1
- package/dist/gate.js +6 -5
- package/dist/index.js +20 -1
- package/dist/interactive/approval.d.ts +9 -0
- package/dist/interactive/approval.js +85 -0
- package/dist/interactive/cards.d.ts +36 -0
- package/dist/interactive/cards.js +125 -0
- package/dist/interactive/pending.d.ts +29 -0
- package/dist/interactive/pending.js +39 -0
- package/dist/interactive/question.d.ts +25 -0
- package/dist/interactive/question.js +86 -0
- package/dist/interactive/runtime.d.ts +44 -0
- package/dist/interactive/runtime.js +147 -0
- package/dist/interactive/types.d.ts +50 -0
- package/dist/interactive/types.js +25 -0
- package/dist/mentions.d.ts +15 -0
- package/dist/mentions.js +21 -0
- package/dist/transport/feishu-api.d.ts +9 -1
- package/dist/transport/lark.d.ts +7 -1
- package/dist/transport/lark.js +34 -2
- package/dist/transport/normalize.d.ts +22 -22
- package/dist/transport/normalize.js +83 -3
- package/dist/transport/reliable.d.ts +5 -1
- package/dist/transport/reliable.js +12 -0
- package/dist/transport/types.d.ts +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,6 +78,15 @@ onlyBuiltDependencies:
|
|
|
78
78
|
| `personaAutoSync` | `true` | Sync SOUL into `$DSH_HOME/AGENTS.md` at startup |
|
|
79
79
|
| `agentPreset` | `standard` | dsh agent preset mounted for new sessions |
|
|
80
80
|
| `logLevel` | `info` | File log level: `info` / `debug` |
|
|
81
|
+
| `approvalCard` | `true` | Send interactive approval cards (`allow once` / `reject`) |
|
|
82
|
+
| `questionCard` | `true` | Send interactive question cards; single-select ≤4 options get buttons |
|
|
83
|
+
| `interactiveTimeoutMs` | `300000` | How long an approval/question card waits before it expires |
|
|
84
|
+
| `approvalPolicy` | `ask` | Session approval policy: `ask` (card confirmation, default) or `never` |
|
|
85
|
+
| `reactions` | `true` | Emoji reactions on messages: `OnIt` while running, `DONE`/`CROSS_MARK` at turn end |
|
|
86
|
+
| `reactionOnStart` | `OnIt` | Reaction added when a user message is accepted |
|
|
87
|
+
| `reactionOnDone` | `DONE` | Reaction added when a turn completes |
|
|
88
|
+
| `reactionOnFail` | `CROSS_MARK` | Reaction added when a turn errors/interrupts (falls back to `THINKING`) |
|
|
89
|
+
| `gateRequireBotIdentity` | `true` | Group @ gate fails closed when the bot open id is unknown |
|
|
81
90
|
|
|
82
91
|
## Commands
|
|
83
92
|
|
package/README.zh.md
CHANGED
|
@@ -77,6 +77,15 @@ onlyBuiltDependencies:
|
|
|
77
77
|
| `personaAutoSync` | `true` | 启动时把 SOUL 同步到 `$DSH_HOME/AGENTS.md` |
|
|
78
78
|
| `agentPreset` | `standard` | 新会话挂载的 dsh agent preset |
|
|
79
79
|
| `logLevel` | `info` | 文件日志级别:`info` / `debug` |
|
|
80
|
+
| `approvalCard` | `true` | 发交互审批卡(允许一次 / 拒绝) |
|
|
81
|
+
| `questionCard` | `true` | 发交互选项卡;单选且 ≤4 个选项时显示按钮 |
|
|
82
|
+
| `interactiveTimeoutMs` | `300000` | 审批/选项卡等待多久后过期 |
|
|
83
|
+
| `approvalPolicy` | `ask` | 会话审批策略:`ask`(弹卡确认,默认)或 `never` |
|
|
84
|
+
| `reactions` | `true` | 消息表情反馈:处理中 `OnIt`,结束 `DONE`/`CROSS_MARK` |
|
|
85
|
+
| `reactionOnStart` | `OnIt` | 收到用户消息时添加的表情 |
|
|
86
|
+
| `reactionOnDone` | `DONE` | 回合完成时添加的表情 |
|
|
87
|
+
| `reactionOnFail` | `CROSS_MARK` | 回合出错/中断时添加的表情(失败退回 `THINKING`) |
|
|
88
|
+
| `gateRequireBotIdentity` | `true` | bot open id 未知时群 @ 门禁 fail-closed(false 恢复旧放行) |
|
|
80
89
|
|
|
81
90
|
## 命令
|
|
82
91
|
|
package/dist/bridge/bridge.d.ts
CHANGED
|
@@ -19,6 +19,15 @@ export declare class Bridge {
|
|
|
19
19
|
handleSessionEvent(session: {
|
|
20
20
|
readonly id: string;
|
|
21
21
|
}, event: StatusEventLike): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Send an interactive card the same way process cards go out: reply into the
|
|
24
|
+
* originating topic/thread when the session still has a live scope. Falls
|
|
25
|
+
* back to a chat send only when the scope state is gone.
|
|
26
|
+
*/
|
|
27
|
+
sendInteractiveCard(sessionId: string, card: FeishuCard): Promise<{
|
|
28
|
+
readonly messageId: string;
|
|
29
|
+
readonly threadId?: string;
|
|
30
|
+
}>;
|
|
22
31
|
}
|
|
23
32
|
/** Build the process card for one snapshot (exported for render tests). */
|
|
24
33
|
export declare function progressCardForSnapshot(snapshot: StatusSnapshot, sessionTitle: string | undefined, recentEvent: string, tail: string, now: number, presetMissing?: boolean): FeishuCard;
|
package/dist/bridge/bridge.js
CHANGED
|
@@ -81,6 +81,8 @@ export class Bridge {
|
|
|
81
81
|
#selfCheck;
|
|
82
82
|
#agentCtxBySession = new Map();
|
|
83
83
|
#toolsCheckedSessions = new Set();
|
|
84
|
+
#approvalMissingWarned = false;
|
|
85
|
+
#botIdentityErrorLogged = false;
|
|
84
86
|
/** Last fail-id set already notified into the conversation. */
|
|
85
87
|
#selfCheckNotifiedSignature;
|
|
86
88
|
/** Session ids whose preset mount failed (shown as `(preset 未挂载)`). */
|
|
@@ -88,6 +90,7 @@ export class Bridge {
|
|
|
88
90
|
#sessionTitleProvider;
|
|
89
91
|
#queue = new ScopeQueue();
|
|
90
92
|
#states = new Map();
|
|
93
|
+
#chatIdBySession = new Map();
|
|
91
94
|
#scopeBySession = new Map();
|
|
92
95
|
constructor(options) {
|
|
93
96
|
this.#options = options;
|
|
@@ -117,6 +120,12 @@ export class Bridge {
|
|
|
117
120
|
if (sessionId !== undefined)
|
|
118
121
|
this.#scopeBySession.set(sessionId, key);
|
|
119
122
|
}
|
|
123
|
+
if (options.interactive !== undefined && options.rootCtx !== undefined) {
|
|
124
|
+
options.interactive.registerRoot(options.rootCtx, {
|
|
125
|
+
ownsSession: (sessionId) => this.ownsSession(sessionId),
|
|
126
|
+
contextFor: (sessionId) => this.#interactiveContext(sessionId),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
120
129
|
}
|
|
121
130
|
/** True when a session id belongs to a scope this bridge renders. */
|
|
122
131
|
ownsSession(sessionId) {
|
|
@@ -124,6 +133,17 @@ export class Bridge {
|
|
|
124
133
|
}
|
|
125
134
|
/** Process one inbound message (serialized per scope). */
|
|
126
135
|
async handleInbound(message) {
|
|
136
|
+
// Bot identity may have failed at startup; retry on the next group message
|
|
137
|
+
// before deciding, so a late bot/v3/info success restores normal gating.
|
|
138
|
+
if (message.chatType === 'group' && (this.#transport.getBotOpenId?.() ?? '').length === 0) {
|
|
139
|
+
try {
|
|
140
|
+
await this.#transport.refreshBotOpenId?.();
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
this.#logger.warn(`bot identity refresh failed: ${errorMessage(error)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const botOpenId = this.#transport.getBotOpenId?.();
|
|
127
147
|
const decision = evaluateGate({
|
|
128
148
|
chatType: message.chatType,
|
|
129
149
|
chatId: message.chatId,
|
|
@@ -131,9 +151,8 @@ export class Bridge {
|
|
|
131
151
|
senderOpenId: message.senderOpenId,
|
|
132
152
|
text: message.text,
|
|
133
153
|
...(message.mentions === undefined ? {} : { mentions: message.mentions }),
|
|
134
|
-
...(
|
|
135
|
-
|
|
136
|
-
: { botOpenId: this.#transport.getBotOpenId?.() }),
|
|
154
|
+
...(botOpenId === undefined ? {} : { botOpenId }),
|
|
155
|
+
requireBotIdentity: this.#config.gateRequireBotIdentity !== false,
|
|
137
156
|
allowedChats: this.#config.allowedChats,
|
|
138
157
|
allowedUsers: this.#config.allowedUsers,
|
|
139
158
|
});
|
|
@@ -142,6 +161,12 @@ export class Bridge {
|
|
|
142
161
|
...(message.threadId === undefined ? {} : { threadId: message.threadId }),
|
|
143
162
|
senderOpenId: message.senderOpenId,
|
|
144
163
|
});
|
|
164
|
+
if (!decision.accept && decision.reason === 'bot-identity-unknown') {
|
|
165
|
+
if (!this.#botIdentityErrorLogged) {
|
|
166
|
+
this.#botIdentityErrorLogged = true;
|
|
167
|
+
this.#logger.error('gate: bot identity unknown; group @ messages are rejected until resolved');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
145
170
|
if (!decision.accept)
|
|
146
171
|
return;
|
|
147
172
|
await this.#queue.run(scopeKey(message.chatType, message.chatId, message.threadId, message.senderOpenId, this.#config.groupSessionsPerUser), () => this.#processInbound(message));
|
|
@@ -218,6 +243,7 @@ export class Bridge {
|
|
|
218
243
|
async #processInbound(message) {
|
|
219
244
|
const state = this.#stateFor(message);
|
|
220
245
|
state.lastInboundMessageId = message.messageId;
|
|
246
|
+
state.ownerOpenId = message.senderOpenId;
|
|
221
247
|
// Startup-stage failures have no session to notify yet; the first message
|
|
222
248
|
// with a session catches up here (once per fail combination).
|
|
223
249
|
await this.#maybeNotifySelfCheck(state);
|
|
@@ -226,6 +252,8 @@ export class Bridge {
|
|
|
226
252
|
await this.#handleSlash(state, message.text);
|
|
227
253
|
return;
|
|
228
254
|
}
|
|
255
|
+
state.reactionQueue.push(message.messageId);
|
|
256
|
+
this.#startReaction(state, message.messageId);
|
|
229
257
|
await this.#deliverTurn(state, message.text);
|
|
230
258
|
}
|
|
231
259
|
async #handleSlash(state, raw) {
|
|
@@ -447,6 +475,7 @@ export class Bridge {
|
|
|
447
475
|
if (notice !== undefined)
|
|
448
476
|
await this.#replyText(state, notice);
|
|
449
477
|
}
|
|
478
|
+
await this.#finishReactions(state, kind);
|
|
450
479
|
state.lastOutput = '';
|
|
451
480
|
}
|
|
452
481
|
#finalCardInput(state, snapshot, body) {
|
|
@@ -698,11 +727,20 @@ export class Bridge {
|
|
|
698
727
|
async #switchSession(state, target, verb = '已恢复会话') {
|
|
699
728
|
const current = state.sessionId ?? this.#sessionMap.get(state.key);
|
|
700
729
|
try {
|
|
701
|
-
const
|
|
730
|
+
const persistedPreset = this.#sessionPresetLookup?.(target);
|
|
731
|
+
const { agent } = await this.#agents.resume({
|
|
732
|
+
resumeSessionId: target,
|
|
733
|
+
setup: async (agentCtx) => {
|
|
734
|
+
this.#agentCtxBySession.set(target, agentCtx);
|
|
735
|
+
if (persistedPreset === undefined)
|
|
736
|
+
await this.#mountPreset(target, agentCtx);
|
|
737
|
+
},
|
|
738
|
+
});
|
|
702
739
|
if (current !== undefined && current !== target) {
|
|
703
740
|
state.previousSessionId = current;
|
|
704
741
|
state.previousSessionAt = this.#lastActivityAt(state) ?? this.#now();
|
|
705
742
|
}
|
|
743
|
+
this.#applyApprovalPolicy(agent);
|
|
706
744
|
this.#sessionMap.set(state.key, target);
|
|
707
745
|
state.sessionId = target;
|
|
708
746
|
state.pendingCreate = false;
|
|
@@ -775,6 +813,45 @@ export class Bridge {
|
|
|
775
813
|
this.#agentPresetsCache = service;
|
|
776
814
|
return service;
|
|
777
815
|
}
|
|
816
|
+
/**
|
|
817
|
+
* Apply the configured session approval policy to a freshly created/resumed
|
|
818
|
+
* agent. Missing service warns once; setPolicy errors never block the turn.
|
|
819
|
+
*/
|
|
820
|
+
#applyApprovalPolicy(agent) {
|
|
821
|
+
const policy = this.#options.approvalPolicy ?? 'ask';
|
|
822
|
+
this.#logger.info(`agent approval policy=${policy}`);
|
|
823
|
+
const provider = this.#options.approvalProvider;
|
|
824
|
+
if (provider === undefined)
|
|
825
|
+
return;
|
|
826
|
+
const service = provider();
|
|
827
|
+
if (service === undefined) {
|
|
828
|
+
if (!this.#approvalMissingWarned) {
|
|
829
|
+
this.#approvalMissingWarned = true;
|
|
830
|
+
this.#logger.warn('approval service unavailable; session approval policy not applied');
|
|
831
|
+
}
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
try {
|
|
835
|
+
service.setPolicy(agent, policy);
|
|
836
|
+
}
|
|
837
|
+
catch (error) {
|
|
838
|
+
this.#logger.warn(`agent approval policy=${policy} failed: ${errorMessage(error)}`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
#interactiveContext(sessionId) {
|
|
842
|
+
const key = this.#scopeBySession.get(sessionId);
|
|
843
|
+
if (key === undefined)
|
|
844
|
+
return undefined;
|
|
845
|
+
const state = this.#states.get(key);
|
|
846
|
+
if (state === undefined || state.ownerOpenId === undefined)
|
|
847
|
+
return undefined;
|
|
848
|
+
return {
|
|
849
|
+
scopeKey: state.key,
|
|
850
|
+
chatId: state.chatId,
|
|
851
|
+
ownerOpenId: state.ownerOpenId,
|
|
852
|
+
sessionId,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
778
855
|
/**
|
|
779
856
|
* Mount the configured agent preset in the unpublished agent scope.
|
|
780
857
|
* Idempotent for resumes: a context that already reports a composed preset is
|
|
@@ -827,6 +904,7 @@ export class Bridge {
|
|
|
827
904
|
catch (error) {
|
|
828
905
|
this.#logger.warn(`ownership write failed for ${sessionId}: ${errorMessage(error)}`);
|
|
829
906
|
}
|
|
907
|
+
this.#applyApprovalPolicy(agent);
|
|
830
908
|
await this.#runSelfCheckStageB(state, sessionId);
|
|
831
909
|
return { agent };
|
|
832
910
|
}
|
|
@@ -1020,6 +1098,7 @@ export class Bridge {
|
|
|
1020
1098
|
},
|
|
1021
1099
|
});
|
|
1022
1100
|
this.#bindSession(state, String(agent.session.id));
|
|
1101
|
+
this.#applyApprovalPolicy(agent);
|
|
1023
1102
|
await this.#runSelfCheckStageB(state, sessionId);
|
|
1024
1103
|
return { agent, sessionId, cwd };
|
|
1025
1104
|
}
|
|
@@ -1045,8 +1124,77 @@ export class Bridge {
|
|
|
1045
1124
|
this.#scopeBySession.delete(id);
|
|
1046
1125
|
}
|
|
1047
1126
|
this.#scopeBySession.set(sessionId, state.key);
|
|
1127
|
+
this.#chatIdBySession.set(sessionId, state.chatId);
|
|
1048
1128
|
state.sessionId = sessionId;
|
|
1049
1129
|
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Send an interactive card the same way process cards go out: reply into the
|
|
1132
|
+
* originating topic/thread when the session still has a live scope. Falls
|
|
1133
|
+
* back to a chat send only when the scope state is gone.
|
|
1134
|
+
*/
|
|
1135
|
+
async sendInteractiveCard(sessionId, card) {
|
|
1136
|
+
const key = this.#scopeBySession.get(sessionId);
|
|
1137
|
+
const state = key === undefined ? undefined : this.#states.get(key);
|
|
1138
|
+
if (state === undefined) {
|
|
1139
|
+
const chatId = this.#chatIdBySession.get(sessionId) ?? '';
|
|
1140
|
+
this.#logger.warn(`interactive: no scope state for ${sessionId}; falling back to chat send`);
|
|
1141
|
+
return this.#transport.sendCard(chatId, card);
|
|
1142
|
+
}
|
|
1143
|
+
const sent = await this.#sendNewCard(state, card);
|
|
1144
|
+
return {
|
|
1145
|
+
...sent,
|
|
1146
|
+
...(state.threadId === undefined ? {} : { threadId: state.threadId }),
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
#startReaction(state, messageId) {
|
|
1150
|
+
if (this.#config.reactions === false)
|
|
1151
|
+
return;
|
|
1152
|
+
const promise = this.#safeAddReaction(messageId, this.#config.reactionOnStart ?? 'OnIt');
|
|
1153
|
+
state.reactionStarts.set(messageId, promise);
|
|
1154
|
+
void promise;
|
|
1155
|
+
}
|
|
1156
|
+
/** Remove the start reaction, then add the terminal reaction (best effort). */
|
|
1157
|
+
async #finishReactions(state, turnKind) {
|
|
1158
|
+
if (this.#config.reactions === false)
|
|
1159
|
+
return;
|
|
1160
|
+
const messageId = state.reactionQueue.shift();
|
|
1161
|
+
if (messageId === undefined)
|
|
1162
|
+
return;
|
|
1163
|
+
const start = state.reactionStarts.get(messageId);
|
|
1164
|
+
state.reactionStarts.delete(messageId);
|
|
1165
|
+
const reactionId = start === undefined ? undefined : await start;
|
|
1166
|
+
if (reactionId !== undefined)
|
|
1167
|
+
await this.#safeRemoveReaction(messageId, reactionId);
|
|
1168
|
+
const completed = turnKind === 'completed';
|
|
1169
|
+
const emoji = completed
|
|
1170
|
+
? (this.#config.reactionOnDone ?? 'DONE')
|
|
1171
|
+
: (this.#config.reactionOnFail ?? 'CROSS_MARK');
|
|
1172
|
+
const added = await this.#safeAddReaction(messageId, emoji);
|
|
1173
|
+
if (!completed && added === undefined && emoji !== 'THINKING') {
|
|
1174
|
+
await this.#safeAddReaction(messageId, 'THINKING');
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
async #safeAddReaction(messageId, emojiType) {
|
|
1178
|
+
try {
|
|
1179
|
+
return await this.#transport.addReaction(messageId, emojiType);
|
|
1180
|
+
}
|
|
1181
|
+
catch (error) {
|
|
1182
|
+
this.#logReactionSkipped(error);
|
|
1183
|
+
return undefined;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
async #safeRemoveReaction(messageId, reactionId) {
|
|
1187
|
+
try {
|
|
1188
|
+
await this.#transport.removeReaction(messageId, reactionId);
|
|
1189
|
+
}
|
|
1190
|
+
catch (error) {
|
|
1191
|
+
this.#logReactionSkipped(error);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
#logReactionSkipped(error) {
|
|
1195
|
+
const code = error.code;
|
|
1196
|
+
this.#logger.debug(`reaction skipped: ${typeof code === 'number' ? code : 'unknown'} ${errorMessage(error)}`);
|
|
1197
|
+
}
|
|
1050
1198
|
#stateFor(message) {
|
|
1051
1199
|
const key = scopeKey(message.chatType, message.chatId, message.threadId, message.senderOpenId, this.#config.groupSessionsPerUser);
|
|
1052
1200
|
const existing = this.#states.get(key);
|
|
@@ -1061,6 +1209,8 @@ export class Bridge {
|
|
|
1061
1209
|
running: false,
|
|
1062
1210
|
status: new StatusTracker(),
|
|
1063
1211
|
lastOutput: '',
|
|
1212
|
+
reactionQueue: [],
|
|
1213
|
+
reactionStarts: new Map(),
|
|
1064
1214
|
};
|
|
1065
1215
|
this.#states.set(key, created);
|
|
1066
1216
|
return created;
|
|
@@ -1080,7 +1230,7 @@ export class Bridge {
|
|
|
1080
1230
|
}
|
|
1081
1231
|
async #sendNewCard(state, card) {
|
|
1082
1232
|
if (state.lastInboundMessageId !== undefined) {
|
|
1083
|
-
return this.#transport.reply(state.chatId, state.lastInboundMessageId, { kind: 'card', card }, { threadId: state.threadId });
|
|
1233
|
+
return this.#transport.reply(state.chatId, state.lastInboundMessageId, { kind: 'card', card }, state.threadId === undefined ? {} : { threadId: state.threadId });
|
|
1084
1234
|
}
|
|
1085
1235
|
return this.#transport.sendCard(state.chatId, card);
|
|
1086
1236
|
}
|
package/dist/bridge/types.d.ts
CHANGED
|
@@ -13,10 +13,17 @@ import type { HarnessCommandResult } from '../commands/harness.js';
|
|
|
13
13
|
import type { LoggerLike } from '../logger.js';
|
|
14
14
|
import type { Clock } from '../render/stream.js';
|
|
15
15
|
import type { DoctorInputs, DoctorSessionInfo } from '../doctor.js';
|
|
16
|
+
import type { InteractiveRuntime } from '../interactive/runtime.js';
|
|
16
17
|
import type { SessionStore } from '../session-key.js';
|
|
17
18
|
import type { Transport } from '../transport/types.js';
|
|
18
19
|
/** Busy-turn delivery policy (design doc §6 L3). */
|
|
19
20
|
export type BusyMode = 'queue' | 'steer' | 'interrupt';
|
|
21
|
+
/** Session approval policy understood by dsh's approval service. */
|
|
22
|
+
export type ApprovalPolicy = 'ask' | 'never';
|
|
23
|
+
/** The slice of dsh's approval service the bridge needs. */
|
|
24
|
+
export interface ApprovalServiceLike {
|
|
25
|
+
setPolicy(agent: AgentLike, policy: ApprovalPolicy): void;
|
|
26
|
+
}
|
|
20
27
|
/** Structural subset of a dsh session. */
|
|
21
28
|
export interface SessionLike {
|
|
22
29
|
readonly id: string;
|
|
@@ -86,6 +93,13 @@ export interface BridgeConfig {
|
|
|
86
93
|
readonly groupSessionsPerUser: boolean;
|
|
87
94
|
/** Agent preset mounted for caduceus sessions (default `standard`). */
|
|
88
95
|
readonly agentPreset?: string;
|
|
96
|
+
/** Emoji reactions enabled (default true). */
|
|
97
|
+
readonly reactions?: boolean;
|
|
98
|
+
readonly reactionOnStart?: string;
|
|
99
|
+
readonly reactionOnDone?: string;
|
|
100
|
+
readonly reactionOnFail?: string;
|
|
101
|
+
/** Default true: group @ gate fails closed when bot open id is unknown. */
|
|
102
|
+
readonly gateRequireBotIdentity?: boolean;
|
|
89
103
|
}
|
|
90
104
|
/** Everything the bridge needs; unit tests provide fakes for every seam. */
|
|
91
105
|
export interface BridgeOptions {
|
|
@@ -111,6 +125,14 @@ export interface BridgeOptions {
|
|
|
111
125
|
readonly persona?: (action: 'show' | 'sync') => Promise<PersonaCommandResult>;
|
|
112
126
|
/** Time source for elapsed rendering; defaults to the clock. */
|
|
113
127
|
readonly now?: () => number;
|
|
128
|
+
/** Interactive approval/question cards (wired by the gateway). */
|
|
129
|
+
readonly interactive?: InteractiveRuntime;
|
|
130
|
+
/** Plugin root Cordis context; answerers register here once, not per agent. */
|
|
131
|
+
readonly rootCtx?: unknown;
|
|
132
|
+
/** Lazily resolve dsh's approval service (agent-scoped setPolicy). */
|
|
133
|
+
readonly approvalProvider?: () => ApprovalServiceLike | undefined;
|
|
134
|
+
/** Desired session approval policy (default `ask`). */
|
|
135
|
+
readonly approvalPolicy?: ApprovalPolicy;
|
|
114
136
|
/** `/doctor` data collector (wired by the gateway; not available in tests by default). */
|
|
115
137
|
readonly doctorInputs?: (session: DoctorSessionInfo) => DoctorInputs | Promise<DoctorInputs>;
|
|
116
138
|
/** Optional self-check controller (`/status`, `/selfcheck`, stage B). */
|
package/dist/commands/parse.d.ts
CHANGED
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Slash-command parsing for the Feishu surface. Pure: no I/O, no side effects.
|
|
3
|
-
*
|
|
4
|
-
* Eleven commands are owned by dsh-caduceus: `/new`, `/sessions`, `/rename`,
|
|
5
|
-
* `/resume`, `/adopt`, `/stop`, `/status`, `/persona`, `/help`. Everything else — unknown slash
|
|
6
|
-
* commands such as `/compact` and all ordinary user text — comes back as
|
|
7
|
-
* `{ kind: 'passthrough', raw }` so the caller can forward it unchanged to the
|
|
8
|
-
* dsh command registry or the agent (design doc §6 L3).
|
|
9
|
-
*
|
|
10
|
-
* @module dsh-caduceus/commands/parse
|
|
11
|
-
*/
|
|
12
1
|
/** Commands dsh-caduceus owns. Keep in sync with README's command table. */
|
|
13
2
|
export declare const COMMAND_NAMES: readonly ["new", "sessions", "rename", "resume", "adopt", "selfcheck", "doctor", "stop", "status", "persona", "help"];
|
|
14
3
|
export type CommandName = (typeof COMMAND_NAMES)[number];
|
package/dist/commands/parse.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module dsh-caduceus/commands/parse
|
|
11
11
|
*/
|
|
12
|
+
import { stripLeadingMentions } from '../mentions.js';
|
|
12
13
|
/** Commands dsh-caduceus owns. Keep in sync with README's command table. */
|
|
13
14
|
export const COMMAND_NAMES = ['new', 'sessions', 'rename', 'resume', 'adopt', 'selfcheck', 'doctor', 'stop', 'status', 'persona', 'help'];
|
|
14
15
|
/** Command metadata; `helpText()` and the README command table both derive from it. */
|
|
@@ -37,7 +38,10 @@ function usageResult(command, reason) {
|
|
|
37
38
|
* never rewrites a user's text.
|
|
38
39
|
*/
|
|
39
40
|
export function parseCommand(raw) {
|
|
40
|
-
|
|
41
|
+
// A topic/group message may arrive as "@_user_1 /stop" (Feishu's mention
|
|
42
|
+
// placeholder). Strip only the leading mentions before command detection;
|
|
43
|
+
// passthrough still forwards the ORIGINAL bytes.
|
|
44
|
+
const trimmed = stripLeadingMentions(raw).trim();
|
|
41
45
|
if (!trimmed.startsWith('/'))
|
|
42
46
|
return { kind: 'passthrough', raw };
|
|
43
47
|
const match = /^\/(\S+)(?:\s+([\s\S]*))?$/.exec(trimmed);
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import z from '@deepseek-ai/schemastery';
|
|
2
|
-
import type { BridgeConfig, BusyMode } from './bridge/types.js';
|
|
2
|
+
import type { ApprovalPolicy, BridgeConfig, BusyMode } from './bridge/types.js';
|
|
3
3
|
/** Raw entry config as written in the profile. */
|
|
4
4
|
export interface GatewayConfig {
|
|
5
5
|
readonly appId?: string;
|
|
@@ -17,6 +17,24 @@ export interface GatewayConfig {
|
|
|
17
17
|
readonly personaAutoSync?: boolean;
|
|
18
18
|
/** Agent preset mounted for caduceus-created sessions (default `standard`). */
|
|
19
19
|
readonly agentPreset?: string;
|
|
20
|
+
/** Send interactive approval cards (default true). */
|
|
21
|
+
readonly approvalCard?: boolean;
|
|
22
|
+
/** Send interactive question cards (default true). */
|
|
23
|
+
readonly questionCard?: boolean;
|
|
24
|
+
/** Interactive card timeout in ms (default 300000). */
|
|
25
|
+
readonly interactiveTimeoutMs?: number;
|
|
26
|
+
/** Session approval policy; IM humans are present, so default `ask`. */
|
|
27
|
+
readonly approvalPolicy?: ApprovalPolicy;
|
|
28
|
+
/** Emoji reactions on inbound/turn-end (default true). */
|
|
29
|
+
readonly reactions?: boolean;
|
|
30
|
+
/** Emoji added on accept (default `OnIt`). */
|
|
31
|
+
readonly reactionOnStart?: string;
|
|
32
|
+
/** Emoji added on completed turn (default `DONE`). */
|
|
33
|
+
readonly reactionOnDone?: string;
|
|
34
|
+
/** Emoji added on failed/interrupted turn (default `CROSS_MARK`). */
|
|
35
|
+
readonly reactionOnFail?: string;
|
|
36
|
+
/** Require the bot's own open id for the group @ gate (default true). */
|
|
37
|
+
readonly gateRequireBotIdentity?: boolean;
|
|
20
38
|
}
|
|
21
39
|
/** Schemastery schema for the `caduceus-gateway` entry. */
|
|
22
40
|
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
@@ -31,6 +49,15 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
31
49
|
logLevel: z<"info" | "debug", "info" | "debug", "plain">;
|
|
32
50
|
personaAutoSync: z<boolean, boolean, "plain">;
|
|
33
51
|
agentPreset: z<string, string, "plain">;
|
|
52
|
+
approvalCard: z<boolean, boolean, "plain">;
|
|
53
|
+
questionCard: z<boolean, boolean, "plain">;
|
|
54
|
+
interactiveTimeoutMs: z<number, number, "plain">;
|
|
55
|
+
approvalPolicy: z<"ask" | "never", "ask" | "never", "plain">;
|
|
56
|
+
reactions: z<boolean, boolean, "plain">;
|
|
57
|
+
reactionOnStart: z<string, string, "plain">;
|
|
58
|
+
reactionOnDone: z<string, string, "plain">;
|
|
59
|
+
reactionOnFail: z<string, string, "plain">;
|
|
60
|
+
gateRequireBotIdentity: z<boolean, boolean, "plain">;
|
|
34
61
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
35
62
|
appId: z<string, string, "plain">;
|
|
36
63
|
appSecret: z<string, string, "plain">;
|
|
@@ -43,6 +70,15 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
43
70
|
logLevel: z<"info" | "debug", "info" | "debug", "plain">;
|
|
44
71
|
personaAutoSync: z<boolean, boolean, "plain">;
|
|
45
72
|
agentPreset: z<string, string, "plain">;
|
|
73
|
+
approvalCard: z<boolean, boolean, "plain">;
|
|
74
|
+
questionCard: z<boolean, boolean, "plain">;
|
|
75
|
+
interactiveTimeoutMs: z<number, number, "plain">;
|
|
76
|
+
approvalPolicy: z<"ask" | "never", "ask" | "never", "plain">;
|
|
77
|
+
reactions: z<boolean, boolean, "plain">;
|
|
78
|
+
reactionOnStart: z<string, string, "plain">;
|
|
79
|
+
reactionOnDone: z<string, string, "plain">;
|
|
80
|
+
reactionOnFail: z<string, string, "plain">;
|
|
81
|
+
gateRequireBotIdentity: z<boolean, boolean, "plain">;
|
|
46
82
|
}>>, "plain">;
|
|
47
83
|
/** Default card patch throttle: half of Feishu's 5 QPS budget. */
|
|
48
84
|
export declare const DEFAULT_CARD_THROTTLE_MS = 400;
|
|
@@ -56,6 +92,17 @@ export declare const DEFAULT_LOG_LEVEL: 'info' | 'debug';
|
|
|
56
92
|
export declare const DEFAULT_PERSONA_AUTO_SYNC = true;
|
|
57
93
|
/** Default agent preset for caduceus sessions. */
|
|
58
94
|
export declare const DEFAULT_AGENT_PRESET = "standard";
|
|
95
|
+
/** Default message reactions. */
|
|
96
|
+
export declare const DEFAULT_REACTIONS = true;
|
|
97
|
+
export declare const DEFAULT_REACTION_ON_START = "OnIt";
|
|
98
|
+
export declare const DEFAULT_REACTION_ON_DONE = "DONE";
|
|
99
|
+
export declare const DEFAULT_REACTION_ON_FAIL = "CROSS_MARK";
|
|
100
|
+
/** Default session approval policy: ask, so dangerous actions prompt a card. */
|
|
101
|
+
export declare const DEFAULT_APPROVAL_POLICY: ApprovalPolicy;
|
|
102
|
+
/** Default interactive-card switches/timeout. */
|
|
103
|
+
export declare const DEFAULT_APPROVAL_CARD = true;
|
|
104
|
+
export declare const DEFAULT_QUESTION_CARD = true;
|
|
105
|
+
export declare const DEFAULT_INTERACTIVE_TIMEOUT_MS = 300000;
|
|
59
106
|
export interface ResolvedGatewayConfig {
|
|
60
107
|
readonly credentials?: {
|
|
61
108
|
readonly appId: string;
|
|
@@ -65,6 +112,10 @@ export interface ResolvedGatewayConfig {
|
|
|
65
112
|
readonly logLevel: 'info' | 'debug';
|
|
66
113
|
readonly personaAutoSync: boolean;
|
|
67
114
|
readonly agentPreset: string;
|
|
115
|
+
readonly approvalCard: boolean;
|
|
116
|
+
readonly questionCard: boolean;
|
|
117
|
+
readonly interactiveTimeoutMs: number;
|
|
118
|
+
readonly approvalPolicy: ApprovalPolicy;
|
|
68
119
|
}
|
|
69
120
|
export interface ConfigEnv {
|
|
70
121
|
readonly FEISHU_APP_ID?: string;
|
package/dist/config.js
CHANGED
|
@@ -23,6 +23,15 @@ export const Config = z.object({
|
|
|
23
23
|
logLevel: z.union([z.const('info'), z.const('debug')]).required(false),
|
|
24
24
|
personaAutoSync: z.boolean().required(false),
|
|
25
25
|
agentPreset: z.string().required(false),
|
|
26
|
+
approvalCard: z.boolean().required(false),
|
|
27
|
+
questionCard: z.boolean().required(false),
|
|
28
|
+
interactiveTimeoutMs: z.natural().min(1).required(false),
|
|
29
|
+
approvalPolicy: z.union([z.const('ask'), z.const('never')]).required(false),
|
|
30
|
+
reactions: z.boolean().required(false),
|
|
31
|
+
reactionOnStart: z.string().required(false),
|
|
32
|
+
reactionOnDone: z.string().required(false),
|
|
33
|
+
reactionOnFail: z.string().required(false),
|
|
34
|
+
gateRequireBotIdentity: z.boolean().required(false),
|
|
26
35
|
});
|
|
27
36
|
/** Default card patch throttle: half of Feishu's 5 QPS budget. */
|
|
28
37
|
export const DEFAULT_CARD_THROTTLE_MS = 400;
|
|
@@ -36,6 +45,17 @@ export const DEFAULT_LOG_LEVEL = 'info';
|
|
|
36
45
|
export const DEFAULT_PERSONA_AUTO_SYNC = true;
|
|
37
46
|
/** Default agent preset for caduceus sessions. */
|
|
38
47
|
export const DEFAULT_AGENT_PRESET = 'standard';
|
|
48
|
+
/** Default message reactions. */
|
|
49
|
+
export const DEFAULT_REACTIONS = true;
|
|
50
|
+
export const DEFAULT_REACTION_ON_START = 'OnIt';
|
|
51
|
+
export const DEFAULT_REACTION_ON_DONE = 'DONE';
|
|
52
|
+
export const DEFAULT_REACTION_ON_FAIL = 'CROSS_MARK';
|
|
53
|
+
/** Default session approval policy: ask, so dangerous actions prompt a card. */
|
|
54
|
+
export const DEFAULT_APPROVAL_POLICY = 'ask';
|
|
55
|
+
/** Default interactive-card switches/timeout. */
|
|
56
|
+
export const DEFAULT_APPROVAL_CARD = true;
|
|
57
|
+
export const DEFAULT_QUESTION_CARD = true;
|
|
58
|
+
export const DEFAULT_INTERACTIVE_TIMEOUT_MS = 300_000;
|
|
39
59
|
function firstNonEmpty(...values) {
|
|
40
60
|
for (const value of values) {
|
|
41
61
|
if (value !== undefined && value.length > 0)
|
|
@@ -61,6 +81,11 @@ export function resolveGatewayConfig(config = {}, env = process.env, cwd = proce
|
|
|
61
81
|
busyMode: config.busyMode ?? DEFAULT_BUSY_MODE,
|
|
62
82
|
groupSessionsPerUser: config.groupSessionsPerUser ?? DEFAULT_GROUP_SESSIONS_PER_USER,
|
|
63
83
|
agentPreset: config.agentPreset ?? DEFAULT_AGENT_PRESET,
|
|
84
|
+
reactions: config.reactions ?? DEFAULT_REACTIONS,
|
|
85
|
+
reactionOnStart: config.reactionOnStart ?? DEFAULT_REACTION_ON_START,
|
|
86
|
+
reactionOnDone: config.reactionOnDone ?? DEFAULT_REACTION_ON_DONE,
|
|
87
|
+
reactionOnFail: config.reactionOnFail ?? DEFAULT_REACTION_ON_FAIL,
|
|
88
|
+
gateRequireBotIdentity: config.gateRequireBotIdentity ?? true,
|
|
64
89
|
};
|
|
65
90
|
return {
|
|
66
91
|
...(appId === undefined || appSecret === undefined ? {} : { credentials: { appId, appSecret } }),
|
|
@@ -68,5 +93,9 @@ export function resolveGatewayConfig(config = {}, env = process.env, cwd = proce
|
|
|
68
93
|
logLevel: config.logLevel ?? DEFAULT_LOG_LEVEL,
|
|
69
94
|
personaAutoSync: config.personaAutoSync ?? DEFAULT_PERSONA_AUTO_SYNC,
|
|
70
95
|
agentPreset: config.agentPreset ?? DEFAULT_AGENT_PRESET,
|
|
96
|
+
approvalCard: config.approvalCard ?? DEFAULT_APPROVAL_CARD,
|
|
97
|
+
questionCard: config.questionCard ?? DEFAULT_QUESTION_CARD,
|
|
98
|
+
interactiveTimeoutMs: config.interactiveTimeoutMs ?? DEFAULT_INTERACTIVE_TIMEOUT_MS,
|
|
99
|
+
approvalPolicy: config.approvalPolicy ?? DEFAULT_APPROVAL_POLICY,
|
|
71
100
|
};
|
|
72
101
|
}
|
package/dist/gate.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import type { LoggerLike } from './logger.js';
|
|
18
18
|
import type { ChatType } from './transport/types.js';
|
|
19
19
|
export type GateAcceptReason = 'p2p' | 'mentioned';
|
|
20
|
-
export type GateDropReason = 'bot-sender' | 'not-allowed-chat' | 'not-allowed-user' | 'empty-text' | 'no-mention';
|
|
20
|
+
export type GateDropReason = 'bot-sender' | 'not-allowed-chat' | 'not-allowed-user' | 'empty-text' | 'no-mention' | 'bot-identity-unknown';
|
|
21
21
|
export type GateDecision = {
|
|
22
22
|
readonly accept: true;
|
|
23
23
|
readonly reason: GateAcceptReason;
|
|
@@ -35,6 +35,8 @@ export interface GateInput {
|
|
|
35
35
|
readonly text: string;
|
|
36
36
|
readonly mentions?: readonly string[];
|
|
37
37
|
readonly botOpenId?: string;
|
|
38
|
+
/** Default true: fail closed when the bot's own open id is unknown. */
|
|
39
|
+
readonly requireBotIdentity?: boolean;
|
|
38
40
|
readonly allowedChats: readonly string[];
|
|
39
41
|
readonly allowedUsers: readonly string[];
|
|
40
42
|
}
|
package/dist/gate.js
CHANGED
|
@@ -22,13 +22,14 @@ export function evaluateGate(input) {
|
|
|
22
22
|
if (botOpenId !== undefined && botOpenId.length > 0) {
|
|
23
23
|
if (mentions.includes(botOpenId))
|
|
24
24
|
return { accept: true, reason: 'mentioned' };
|
|
25
|
+
return { accept: false, reason: 'no-mention' };
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// Identity unknown: fail closed by default (never let an arbitrary @ through).
|
|
28
|
+
if (mentions.length === 0)
|
|
29
|
+
return { accept: false, reason: 'no-mention' };
|
|
30
|
+
if (input.requireBotIdentity === false)
|
|
29
31
|
return { accept: true, reason: 'mentioned' };
|
|
30
|
-
}
|
|
31
|
-
return { accept: false, reason: 'no-mention' };
|
|
32
|
+
return { accept: false, reason: 'bot-identity-unknown' };
|
|
32
33
|
}
|
|
33
34
|
function prefix(value, length = 12) {
|
|
34
35
|
const text = (value ?? '').slice(0, length);
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { adaptLogger, teeLoggers } from './logger.js';
|
|
|
23
23
|
import { formatStartupLine } from './observability.js';
|
|
24
24
|
import { autoSyncPersona } from './persona/auto-sync.js';
|
|
25
25
|
import { runPersonaCommand } from './persona/command.js';
|
|
26
|
+
import { InteractiveRuntime } from './interactive/runtime.js';
|
|
26
27
|
import { SelfChecker } from './selfcheck.js';
|
|
27
28
|
import { hasSessionLog } from './session-files.js';
|
|
28
29
|
import { SESSION_MAP_VERSION, SessionStore } from './session-key.js';
|
|
@@ -164,7 +165,21 @@ export function apply(ctx, config = {}) {
|
|
|
164
165
|
logger,
|
|
165
166
|
});
|
|
166
167
|
const transport = new ReliableTransport(api, { logger });
|
|
167
|
-
|
|
168
|
+
let bridge;
|
|
169
|
+
const interactive = new InteractiveRuntime({
|
|
170
|
+
config: {
|
|
171
|
+
approvalCard: resolved.approvalCard,
|
|
172
|
+
questionCard: resolved.questionCard,
|
|
173
|
+
timeoutMs: resolved.interactiveTimeoutMs,
|
|
174
|
+
},
|
|
175
|
+
logger,
|
|
176
|
+
sendCard: (sessionId, card) => bridge.sendInteractiveCard(sessionId, card),
|
|
177
|
+
updateCard: (messageId, card) => transport.updateCard(messageId, card),
|
|
178
|
+
});
|
|
179
|
+
transport.onCardAction?.((action) => {
|
|
180
|
+
void interactive.handleCardAction(action);
|
|
181
|
+
});
|
|
182
|
+
bridge = new Bridge({
|
|
168
183
|
transport,
|
|
169
184
|
sessionMap,
|
|
170
185
|
agents: makeAgents(ctx),
|
|
@@ -175,6 +190,10 @@ export function apply(ctx, config = {}) {
|
|
|
175
190
|
// Lazy per use: the preset registry can register AFTER this plugin mounts.
|
|
176
191
|
agentPresetsProvider: () => ctx.get('agentPresets'),
|
|
177
192
|
selfCheck,
|
|
193
|
+
interactive,
|
|
194
|
+
rootCtx: ctx,
|
|
195
|
+
approvalProvider: () => ctx.get('approval'),
|
|
196
|
+
approvalPolicy: resolved.approvalPolicy,
|
|
178
197
|
sessionPresetLookup: (sessionId) => ctx.get('sessions')?.get(sessionId)?.header
|
|
179
198
|
?.agentPreset,
|
|
180
199
|
doctorInputs: (session) => {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type InteractiveContext, type InteractiveHost } from './types.js';
|
|
2
|
+
export type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
|
|
3
|
+
export interface ApprovalRequestLike {
|
|
4
|
+
readonly toolName: string;
|
|
5
|
+
readonly reason?: string;
|
|
6
|
+
readonly signal?: AbortSignal;
|
|
7
|
+
}
|
|
8
|
+
export type ApprovalAnswerer = (request: ApprovalRequestLike, next: () => Promise<ApprovalOutcome>) => Promise<ApprovalOutcome>;
|
|
9
|
+
export declare function createApprovalAnswerer(host: InteractiveHost, contextFor: () => InteractiveContext | undefined): ApprovalAnswerer;
|