dsh-caduceus 0.7.0 → 0.10.5
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 +8 -2
- package/README.zh.md +8 -2
- package/cordis.patch.yml +2 -0
- package/dist/bridge/bridge.d.ts +7 -2
- package/dist/bridge/bridge.js +431 -20
- package/dist/bridge/types.d.ts +21 -1
- package/dist/commands/dispatch.d.ts +2 -2
- package/dist/commands/dispatch.js +3 -3
- package/dist/commands/parse.d.ts +15 -2
- package/dist/commands/parse.js +34 -3
- package/dist/config.d.ts +18 -4
- package/dist/config.js +9 -3
- package/dist/export.d.ts +44 -0
- package/dist/export.js +222 -0
- package/dist/home.d.ts +2 -0
- package/dist/home.js +4 -0
- package/dist/i18n.d.ts +42 -0
- package/dist/i18n.js +94 -0
- package/dist/index.js +48 -1
- package/dist/interactive/approval.js +1 -1
- package/dist/interactive/cards.d.ts +2 -1
- package/dist/interactive/cards.js +3 -1
- package/dist/interactive/runtime.d.ts +4 -0
- package/dist/interactive/runtime.js +15 -0
- package/dist/interactive/types.d.ts +2 -0
- package/dist/metrics.d.ts +28 -0
- package/dist/metrics.js +135 -0
- package/dist/render/cards.d.ts +4 -0
- package/dist/render/cards.js +17 -0
- package/dist/render/permission-card.d.ts +23 -0
- package/dist/render/permission-card.js +22 -0
- package/dist/status.d.ts +22 -0
- package/dist/status.js +95 -0
- package/dist/transport/feishu-response.d.ts +1 -0
- package/dist/transport/feishu-response.js +25 -0
- package/dist/transport/lark.js +5 -4
- package/dist/transport/normalize.js +3 -1
- package/dist/usage.d.ts +23 -0
- package/dist/usage.js +77 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16,9 +16,11 @@ import { dirname, join } from 'node:path';
|
|
|
16
16
|
import { backfillSessionOwnership, scanSessionDirs } from './backfill.js';
|
|
17
17
|
import { Bridge } from './bridge/bridge.js';
|
|
18
18
|
import { runHarnessCommand } from './commands/harness.js';
|
|
19
|
+
import { cleanupExportCache, parseSessionEvents } from './export.js';
|
|
20
|
+
import { resolveLang } from './i18n.js';
|
|
19
21
|
import { Config, accessControlWarning, resolveGatewayConfig, } from './config.js';
|
|
20
22
|
import { FileLogger } from './file-logger.js';
|
|
21
|
-
import { defaultLogPath } from './home.js';
|
|
23
|
+
import { defaultExportCacheDir, defaultLogPath } from './home.js';
|
|
22
24
|
import { adaptLogger, teeLoggers } from './logger.js';
|
|
23
25
|
import { formatStartupLine } from './observability.js';
|
|
24
26
|
import { autoSyncPersona } from './persona/auto-sync.js';
|
|
@@ -140,6 +142,9 @@ export function apply(ctx, config = {}) {
|
|
|
140
142
|
busyMode: resolved.bridge.busyMode,
|
|
141
143
|
}));
|
|
142
144
|
logger.info(`agent preset=${resolved.agentPreset}`);
|
|
145
|
+
if (resolved.approvalPolicy !== undefined) {
|
|
146
|
+
logger.info(`approval policy override=${resolved.approvalPolicy}`);
|
|
147
|
+
}
|
|
143
148
|
const accessWarning = accessControlWarning(resolved.bridge.admins ?? []);
|
|
144
149
|
if (accessWarning !== undefined)
|
|
145
150
|
logger.warn(accessWarning);
|
|
@@ -177,6 +182,10 @@ export function apply(ctx, config = {}) {
|
|
|
177
182
|
},
|
|
178
183
|
logger,
|
|
179
184
|
sendCard: (sessionId, card) => bridge.sendInteractiveCard(sessionId, card),
|
|
185
|
+
permissionSwitch: (action, name) => {
|
|
186
|
+
void bridge.handlePermissionCardAction(action, name);
|
|
187
|
+
},
|
|
188
|
+
turnStop: (action) => bridge.handleStopCardAction(action),
|
|
180
189
|
updateCard: (messageId, card) => transport.updateCard(messageId, card),
|
|
181
190
|
});
|
|
182
191
|
transport.onCardAction?.((action) => {
|
|
@@ -197,6 +206,31 @@ export function apply(ctx, config = {}) {
|
|
|
197
206
|
rootCtx: ctx,
|
|
198
207
|
approvalProvider: () => ctx.get('approval'),
|
|
199
208
|
attachmentProvider: () => ctx.get('attachments'),
|
|
209
|
+
permissionPresetsProvider: () => ctx.get('permissionPresets'),
|
|
210
|
+
usageProvider: async (sessionId) => {
|
|
211
|
+
const query = ctx.get('sessionQuery');
|
|
212
|
+
if (query?.readSession === undefined)
|
|
213
|
+
return undefined;
|
|
214
|
+
const snapshot = await query.readSession(sessionId);
|
|
215
|
+
return snapshot.events ?? [];
|
|
216
|
+
},
|
|
217
|
+
languageProvider: () => resolveLang(resolved.lang, () => ctx.get('settings')),
|
|
218
|
+
exportProvider: async (sessionId) => {
|
|
219
|
+
const query = ctx.get('sessionQuery');
|
|
220
|
+
if (query?.readSession === undefined)
|
|
221
|
+
return undefined;
|
|
222
|
+
try {
|
|
223
|
+
const snapshot = await query.readSession(sessionId);
|
|
224
|
+
const events = snapshot.events ?? [];
|
|
225
|
+
return {
|
|
226
|
+
messages: parseSessionEvents(events),
|
|
227
|
+
rawJsonl: events.map((event) => JSON.stringify(event)).join('\n') + '\n',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
},
|
|
200
234
|
approvalPolicy: resolved.approvalPolicy,
|
|
201
235
|
sessionPresetLookup: (sessionId) => ctx.get('sessions')?.get(sessionId)?.header
|
|
202
236
|
?.agentPreset,
|
|
@@ -262,6 +296,19 @@ export function apply(ctx, config = {}) {
|
|
|
262
296
|
logger.error(`inbound message failed: ${errorText(error)}`);
|
|
263
297
|
});
|
|
264
298
|
});
|
|
299
|
+
const exportCacheDir = defaultExportCacheDir();
|
|
300
|
+
try {
|
|
301
|
+
mkdirSync(exportCacheDir, { recursive: true });
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
logger.warn(`export cache dir unavailable (${exportCacheDir}): ${errorText(error)}`);
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
cleanupExportCache(exportCacheDir, resolved.exportCacheMaxAgeHours, Date.now(), logger);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
logger.warn(`export cache cleanup failed: ${errorText(error)}`);
|
|
311
|
+
}
|
|
265
312
|
ctx.on('session/event', (session, event) => {
|
|
266
313
|
const sessionId = String(session.id);
|
|
267
314
|
if (!bridge.ownsSession(sessionId))
|
|
@@ -39,7 +39,7 @@ export function createApprovalAnswerer(host, contextFor) {
|
|
|
39
39
|
meta: { toolName: request.toolName, message: request.reason },
|
|
40
40
|
});
|
|
41
41
|
host.logger.info(`interactive: approval request id=${entry.id} questions=0 scope=${context.scopeKey}`);
|
|
42
|
-
const card = approvalCard(entry.id, context.sessionId, request.toolName, request.reason);
|
|
42
|
+
const card = approvalCard(entry.id, context.sessionId, request.toolName, request.reason, context.permissionLine);
|
|
43
43
|
let timer;
|
|
44
44
|
const onAbort = () => {
|
|
45
45
|
if (host.pending.delete(entry.id) === undefined)
|
|
@@ -2,13 +2,14 @@ import type { FeishuCard, FeishuCardElement } from '../transport/types.js';
|
|
|
2
2
|
export interface CardButton {
|
|
3
3
|
readonly label: string;
|
|
4
4
|
readonly type?: 'primary' | 'danger' | 'default';
|
|
5
|
+
readonly disabled?: boolean;
|
|
5
6
|
readonly value: Record<string, unknown>;
|
|
6
7
|
}
|
|
7
8
|
/** A card with optional buttons. */
|
|
8
9
|
export declare function interactiveCard(title: string, lines: readonly string[], buttons?: readonly CardButton[], template?: string): FeishuCard;
|
|
9
10
|
/** Card from pre-built elements (forms, inputs, action rows). */
|
|
10
11
|
export declare function interactiveElementsCard(title: string, elements: readonly FeishuCardElement[], template?: string): FeishuCard;
|
|
11
|
-
export declare function approvalCard(requestId: string, sessionId: string, toolName: string, reason: string | undefined): FeishuCard;
|
|
12
|
+
export declare function approvalCard(requestId: string, sessionId: string, toolName: string, reason: string | undefined, permissionLine?: string): FeishuCard;
|
|
12
13
|
export declare function approvalTerminalCard(toolName: string, outcome: string): FeishuCard;
|
|
13
14
|
export interface QuestionCardInput {
|
|
14
15
|
readonly requestId: string;
|
|
@@ -14,6 +14,7 @@ function actionRow(buttons) {
|
|
|
14
14
|
tag: 'button',
|
|
15
15
|
text: { tag: 'plain_text', content: button.label },
|
|
16
16
|
type: button.type ?? 'default',
|
|
17
|
+
...(button.disabled === true ? { disabled: true } : {}),
|
|
17
18
|
value: button.value,
|
|
18
19
|
})),
|
|
19
20
|
};
|
|
@@ -29,8 +30,9 @@ export function interactiveCard(title, lines, buttons = [], template = 'orange')
|
|
|
29
30
|
export function interactiveElementsCard(title, elements, template = 'orange') {
|
|
30
31
|
return buildCard({ title, template, elements });
|
|
31
32
|
}
|
|
32
|
-
export function approvalCard(requestId, sessionId, toolName, reason) {
|
|
33
|
+
export function approvalCard(requestId, sessionId, toolName, reason, permissionLine) {
|
|
33
34
|
return interactiveCard(`审批请求 · ${toolName}`, [
|
|
35
|
+
...(permissionLine === undefined ? [] : [permissionLine]),
|
|
34
36
|
reason === undefined || reason.trim().length === 0 ? '(无说明)' : reason,
|
|
35
37
|
], [
|
|
36
38
|
{
|
|
@@ -18,6 +18,10 @@ export interface InteractiveRuntimeOptions {
|
|
|
18
18
|
readonly threadId?: string;
|
|
19
19
|
}>;
|
|
20
20
|
updateCard(messageId: string, card: FeishuCard): Promise<void>;
|
|
21
|
+
/** Button callback for the permission card (`caduceus.permission.switch`). */
|
|
22
|
+
permissionSwitch?(action: CardAction, name: string): void | Promise<void>;
|
|
23
|
+
/** Button callback for the process-card stop button. */
|
|
24
|
+
turnStop?(action: CardAction): void | Promise<void>;
|
|
21
25
|
}
|
|
22
26
|
export declare class InteractiveRuntime {
|
|
23
27
|
#private;
|
|
@@ -12,12 +12,16 @@ export class InteractiveRuntime {
|
|
|
12
12
|
pending = new PendingRegistry();
|
|
13
13
|
#sendCard;
|
|
14
14
|
#updateCard;
|
|
15
|
+
#permissionSwitch;
|
|
16
|
+
#turnStop;
|
|
15
17
|
constructor(options) {
|
|
16
18
|
this.config = options.config;
|
|
17
19
|
this.logger = options.logger ?? silentLogger();
|
|
18
20
|
this.clock = options.clock ?? systemClock;
|
|
19
21
|
this.#sendCard = options.sendCard;
|
|
20
22
|
this.#updateCard = options.updateCard;
|
|
23
|
+
this.#permissionSwitch = options.permissionSwitch;
|
|
24
|
+
this.#turnStop = options.turnStop;
|
|
21
25
|
}
|
|
22
26
|
/**
|
|
23
27
|
* Register both waterfall answerers ONCE on the plugin root context, as dsh's
|
|
@@ -58,6 +62,17 @@ export class InteractiveRuntime {
|
|
|
58
62
|
* a warning and do not resolve or patch anything.
|
|
59
63
|
*/
|
|
60
64
|
async handleCardAction(action) {
|
|
65
|
+
if (action.value['action'] === 'caduceus.turn.stop') {
|
|
66
|
+
this.logger.info(`card action stop operator=${logPrefix(action.operatorOpenId)}`);
|
|
67
|
+
await this.#turnStop?.(action);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (action.value['action'] === 'caduceus.permission.switch' &&
|
|
71
|
+
typeof action.value['name'] === 'string') {
|
|
72
|
+
this.logger.info(`card action permission switch name=${action.value['name']} operator=${logPrefix(action.operatorOpenId)}`);
|
|
73
|
+
await this.#permissionSwitch?.(action, action.value['name']);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
61
76
|
const id = typeof action.value['id'] === 'string' ? action.value['id'] : undefined;
|
|
62
77
|
if (id === undefined) {
|
|
63
78
|
this.logger.info('card action ignored (unknown id)');
|
|
@@ -26,6 +26,8 @@ export interface InteractiveContext {
|
|
|
26
26
|
readonly chatId: string;
|
|
27
27
|
readonly ownerOpenId: string;
|
|
28
28
|
readonly sessionId: string;
|
|
29
|
+
/** Optional permission summary line for approval cards. */
|
|
30
|
+
readonly permissionLine?: string;
|
|
29
31
|
}
|
|
30
32
|
/** A Cordis context (the plugin root or an agent scope); only `on` is needed. */
|
|
31
33
|
export interface EventScopeLike {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card/`/usage` metrics aggregated from durable session events:
|
|
3
|
+
* rounds, steps, tok/s, cumulative tokens, cache hit rate, context usage.
|
|
4
|
+
*
|
|
5
|
+
* These are deliberately derived from the event log (not the in-memory status
|
|
6
|
+
* tracker), so a restarted gateway can still show the same numbers.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-caduceus/metrics
|
|
9
|
+
*/
|
|
10
|
+
export type MetricsMode = 'off' | 'compact' | 'detailed';
|
|
11
|
+
export interface Metrics {
|
|
12
|
+
readonly rounds: number;
|
|
13
|
+
readonly steps: number;
|
|
14
|
+
readonly tokensPerSecond?: number;
|
|
15
|
+
readonly totalTokens: number;
|
|
16
|
+
/** Raw sums (used when merging an in-flight turn with the full log). */
|
|
17
|
+
readonly inputTokens: number;
|
|
18
|
+
readonly outputTokens: number;
|
|
19
|
+
readonly cacheReadTokens: number;
|
|
20
|
+
readonly cacheHitPercent?: number;
|
|
21
|
+
readonly contextPercent?: number;
|
|
22
|
+
}
|
|
23
|
+
/** Aggregate metrics from one session event sequence. */
|
|
24
|
+
export declare function aggregateMetrics(events: readonly unknown[]): Metrics;
|
|
25
|
+
/** Compact number format: 999, 1.6k, 7.9M. */
|
|
26
|
+
export declare function formatTokenCount(value: number): string;
|
|
27
|
+
/** One-line metrics summary; empty when mode is off or nothing to show. */
|
|
28
|
+
export declare function formatMetricsLine(metrics: Metrics, mode: MetricsMode): string;
|
package/dist/metrics.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
function asRecord(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null
|
|
3
|
+
? value
|
|
4
|
+
: undefined;
|
|
5
|
+
}
|
|
6
|
+
function num(record, key) {
|
|
7
|
+
const value = record?.[key];
|
|
8
|
+
return typeof value === 'number' ? value : 0;
|
|
9
|
+
}
|
|
10
|
+
function usageDelta(value) {
|
|
11
|
+
const record = asRecord(value);
|
|
12
|
+
if (record === undefined)
|
|
13
|
+
return undefined;
|
|
14
|
+
const input = num(record, 'inputTokens');
|
|
15
|
+
const output = num(record, 'outputTokens');
|
|
16
|
+
const cacheRead = num(record, 'cacheReadTokens');
|
|
17
|
+
const cacheWrite = num(record, 'cacheWriteTokens');
|
|
18
|
+
const providerTotal = typeof record['totalTokens'] === 'number' ? record['totalTokens'] : 0;
|
|
19
|
+
if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0 && providerTotal === 0) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
input,
|
|
24
|
+
output,
|
|
25
|
+
cacheRead,
|
|
26
|
+
cacheWrite,
|
|
27
|
+
total: providerTotal > 0 ? providerTotal : input + output + cacheRead + cacheWrite,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Aggregate metrics from one session event sequence. */
|
|
31
|
+
export function aggregateMetrics(events) {
|
|
32
|
+
let rounds = 0;
|
|
33
|
+
let steps = 0;
|
|
34
|
+
let totalTokens = 0;
|
|
35
|
+
let cacheRead = 0;
|
|
36
|
+
let input = 0;
|
|
37
|
+
let outputTotal = 0;
|
|
38
|
+
const outputByTurn = new Map();
|
|
39
|
+
const promptByTurn = new Map();
|
|
40
|
+
const startByTurn = new Map();
|
|
41
|
+
const endByTurn = new Map();
|
|
42
|
+
let lastEventAt;
|
|
43
|
+
let contextWindow;
|
|
44
|
+
for (const event of events) {
|
|
45
|
+
const row = asRecord(event);
|
|
46
|
+
if (row === undefined)
|
|
47
|
+
continue;
|
|
48
|
+
const data = asRecord(row.data);
|
|
49
|
+
const time = typeof row.time === 'number' ? row.time : undefined;
|
|
50
|
+
if (time !== undefined)
|
|
51
|
+
lastEventAt = time;
|
|
52
|
+
const turn = typeof data?.['turn'] === 'number' ? data['turn'] : undefined;
|
|
53
|
+
if (turn !== undefined)
|
|
54
|
+
rounds = Math.max(rounds, turn);
|
|
55
|
+
if (row.type === 'turn/start' && turn !== undefined && time !== undefined) {
|
|
56
|
+
startByTurn.set(turn, time);
|
|
57
|
+
}
|
|
58
|
+
if (row.type === 'turn/end' && turn !== undefined && time !== undefined) {
|
|
59
|
+
endByTurn.set(turn, time);
|
|
60
|
+
}
|
|
61
|
+
if (row.type === 'step/start')
|
|
62
|
+
steps += 1;
|
|
63
|
+
if (row.type === 'request/context') {
|
|
64
|
+
const window = num(data, 'contextWindow');
|
|
65
|
+
if (window > 0)
|
|
66
|
+
contextWindow = window;
|
|
67
|
+
}
|
|
68
|
+
if (row.type !== 'assistant/message')
|
|
69
|
+
continue;
|
|
70
|
+
const delta = usageDelta(data?.usage);
|
|
71
|
+
if (delta === undefined)
|
|
72
|
+
continue;
|
|
73
|
+
totalTokens += delta.total;
|
|
74
|
+
cacheRead += delta.cacheRead;
|
|
75
|
+
input += delta.input;
|
|
76
|
+
outputTotal += delta.output;
|
|
77
|
+
if (turn !== undefined) {
|
|
78
|
+
outputByTurn.set(turn, (outputByTurn.get(turn) ?? 0) + delta.output);
|
|
79
|
+
promptByTurn.set(turn, (promptByTurn.get(turn) ?? 0) + delta.input + delta.cacheRead + delta.cacheWrite);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
let tokensPerSecond;
|
|
83
|
+
const start = startByTurn.get(rounds);
|
|
84
|
+
const end = endByTurn.get(rounds) ?? lastEventAt;
|
|
85
|
+
const output = outputByTurn.get(rounds) ?? 0;
|
|
86
|
+
if (start !== undefined && end !== undefined && end - start >= 1000 && output > 0) {
|
|
87
|
+
tokensPerSecond = output / ((end - start) / 1000);
|
|
88
|
+
}
|
|
89
|
+
const promptTokens = promptByTurn.get(rounds - 1);
|
|
90
|
+
const contextPercent = contextWindow !== undefined && promptTokens !== undefined && contextWindow > 0
|
|
91
|
+
? Math.round((promptTokens / contextWindow) * 100)
|
|
92
|
+
: undefined;
|
|
93
|
+
const denominator = cacheRead + input;
|
|
94
|
+
const cacheHitPercent = denominator > 0 ? Math.round((cacheRead / denominator) * 100) : undefined;
|
|
95
|
+
return {
|
|
96
|
+
rounds,
|
|
97
|
+
steps,
|
|
98
|
+
totalTokens,
|
|
99
|
+
inputTokens: input,
|
|
100
|
+
outputTokens: outputTotal,
|
|
101
|
+
cacheReadTokens: cacheRead,
|
|
102
|
+
...(tokensPerSecond === undefined ? {} : { tokensPerSecond }),
|
|
103
|
+
...(cacheHitPercent === undefined ? {} : { cacheHitPercent }),
|
|
104
|
+
...(contextPercent === undefined ? {} : { contextPercent }),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Compact number format: 999, 1.6k, 7.9M. */
|
|
108
|
+
export function formatTokenCount(value) {
|
|
109
|
+
if (value >= 1_000_000)
|
|
110
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
111
|
+
if (value >= 1_000)
|
|
112
|
+
return `${(value / 1_000).toFixed(1)}k`;
|
|
113
|
+
return String(Math.round(value));
|
|
114
|
+
}
|
|
115
|
+
/** One-line metrics summary; empty when mode is off or nothing to show. */
|
|
116
|
+
export function formatMetricsLine(metrics, mode) {
|
|
117
|
+
if (mode === 'off')
|
|
118
|
+
return '';
|
|
119
|
+
const parts = [];
|
|
120
|
+
if (metrics.rounds > 0)
|
|
121
|
+
parts.push(`${metrics.rounds} 轮`);
|
|
122
|
+
if (metrics.steps > 0)
|
|
123
|
+
parts.push(`${metrics.steps} 步`);
|
|
124
|
+
if (metrics.tokensPerSecond !== undefined) {
|
|
125
|
+
parts.push(`${Math.round(metrics.tokensPerSecond)} tok/s`);
|
|
126
|
+
}
|
|
127
|
+
if (metrics.totalTokens > 0)
|
|
128
|
+
parts.push(`${formatTokenCount(metrics.totalTokens)} tok`);
|
|
129
|
+
if (metrics.cacheHitPercent !== undefined)
|
|
130
|
+
parts.push(`缓存 ${metrics.cacheHitPercent}%`);
|
|
131
|
+
if (mode === 'detailed' && metrics.contextPercent !== undefined) {
|
|
132
|
+
parts.push(`上下文 ${metrics.contextPercent}%`);
|
|
133
|
+
}
|
|
134
|
+
return parts.join(' · ');
|
|
135
|
+
}
|
package/dist/render/cards.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ export declare function buildCard(input: CardInput): FeishuCard;
|
|
|
26
26
|
export declare function tailText(text: string, maxBytes?: number): string;
|
|
27
27
|
export interface ProgressCardInput {
|
|
28
28
|
readonly title: string;
|
|
29
|
+
/** Extra status lines (compaction, teammate, stop requested). */
|
|
30
|
+
readonly extraLines?: readonly string[];
|
|
31
|
+
/** Show the stop button (running turns only). */
|
|
32
|
+
readonly stopButton?: boolean;
|
|
29
33
|
readonly stateLabel: string;
|
|
30
34
|
readonly steps: number;
|
|
31
35
|
readonly tools: number;
|
package/dist/render/cards.js
CHANGED
|
@@ -64,9 +64,26 @@ export function progressCard(input) {
|
|
|
64
64
|
div(stateLine.join(' · ')),
|
|
65
65
|
div(`最近:${clipLabel(input.recentEvent ?? '(无)')}`),
|
|
66
66
|
];
|
|
67
|
+
for (const line of input.extraLines ?? []) {
|
|
68
|
+
if (line.trim().length > 0)
|
|
69
|
+
elements.push(div(line));
|
|
70
|
+
}
|
|
67
71
|
const tail = input.tail === undefined ? '' : tailText(input.tail);
|
|
68
72
|
if (tail.trim().length > 0)
|
|
69
73
|
elements.push(div(tail));
|
|
74
|
+
if (input.stopButton === true) {
|
|
75
|
+
elements.push({
|
|
76
|
+
tag: 'action',
|
|
77
|
+
actions: [
|
|
78
|
+
{
|
|
79
|
+
tag: 'button',
|
|
80
|
+
text: { tag: 'plain_text', content: '停止' },
|
|
81
|
+
type: 'danger',
|
|
82
|
+
value: { action: 'caduceus.turn.stop' },
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
70
87
|
elements.push(div('_过程消息;收尾会把终稿更新到本卡。_'));
|
|
71
88
|
return buildCard({ title: input.title, template: 'blue', elements });
|
|
72
89
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission UI rendered once as a Feishu card and once as plain text (the
|
|
3
|
+
* send-failure fallback). Both consume the same data and the same i18n dict.
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-caduceus/render/permission-card
|
|
6
|
+
*/
|
|
7
|
+
import type { FeishuCard } from '../transport/types.js';
|
|
8
|
+
import type { Lang } from '../i18n.js';
|
|
9
|
+
export interface PermissionPresetView {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly sandbox?: string;
|
|
12
|
+
readonly approval?: string;
|
|
13
|
+
/** False/absent when resolve failed: render the bare name only. */
|
|
14
|
+
readonly resolved?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface PermissionView {
|
|
17
|
+
readonly lang: Lang;
|
|
18
|
+
readonly current: string;
|
|
19
|
+
readonly presets: readonly PermissionPresetView[];
|
|
20
|
+
}
|
|
21
|
+
export declare const PERMISSION_SWITCH_ACTION = "caduceus.permission.switch";
|
|
22
|
+
export declare function buildPermissionCard(view: PermissionView): FeishuCard;
|
|
23
|
+
export declare function renderPermissionText(view: PermissionView): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { presetLabel, t } from '../i18n.js';
|
|
2
|
+
import { interactiveCard } from '../interactive/cards.js';
|
|
3
|
+
export const PERMISSION_SWITCH_ACTION = 'caduceus.permission.switch';
|
|
4
|
+
function title(view, dict) {
|
|
5
|
+
const label = view.current === 'custom' ? dict.customLabel : presetLabel(view.lang, view.current);
|
|
6
|
+
return dict.permissionTitle(label);
|
|
7
|
+
}
|
|
8
|
+
function buttonLabels(view) {
|
|
9
|
+
return view.presets.map((preset) => presetLabel(view.lang, preset.name));
|
|
10
|
+
}
|
|
11
|
+
export function buildPermissionCard(view) {
|
|
12
|
+
const dict = t(view.lang);
|
|
13
|
+
return interactiveCard(title(view, dict), [dict.usage], view.presets.map((preset) => ({
|
|
14
|
+
label: presetLabel(view.lang, preset.name),
|
|
15
|
+
disabled: preset.name === view.current,
|
|
16
|
+
value: { action: PERMISSION_SWITCH_ACTION, name: preset.name },
|
|
17
|
+
})));
|
|
18
|
+
}
|
|
19
|
+
export function renderPermissionText(view) {
|
|
20
|
+
const dict = t(view.lang);
|
|
21
|
+
return [title(view, dict), dict.usage, buttonLabels(view).join(' / ')].join('\n');
|
|
22
|
+
}
|
package/dist/status.d.ts
CHANGED
|
@@ -21,6 +21,14 @@
|
|
|
21
21
|
*/
|
|
22
22
|
/** Derived session states. */
|
|
23
23
|
export type StatusState = 'idle' | 'waiting-input' | 'running' | 'completed' | 'failed' | 'waiting-approval' | 'interrupted';
|
|
24
|
+
/** Disjoint token totals for one or more model calls. */
|
|
25
|
+
export interface UsageTotals {
|
|
26
|
+
readonly input: number;
|
|
27
|
+
readonly output: number;
|
|
28
|
+
readonly cacheRead: number;
|
|
29
|
+
readonly cacheWrite: number;
|
|
30
|
+
readonly total: number;
|
|
31
|
+
}
|
|
24
32
|
/** Immutable status snapshot. */
|
|
25
33
|
export interface StatusSnapshot {
|
|
26
34
|
readonly state: StatusState;
|
|
@@ -41,6 +49,20 @@ export interface StatusSnapshot {
|
|
|
41
49
|
readonly terminalKind?: string;
|
|
42
50
|
/** Provider failure text for `turn/end error`. */
|
|
43
51
|
readonly errorMessage?: string;
|
|
52
|
+
/** Token usage for the current turn (`assistant/message.usage`). */
|
|
53
|
+
readonly usageTurn?: UsageTotals;
|
|
54
|
+
/** Usage of the previous turn. */
|
|
55
|
+
readonly usagePrevious?: UsageTotals;
|
|
56
|
+
/** Cumulative usage of the session. */
|
|
57
|
+
readonly usageTotal?: UsageTotals;
|
|
58
|
+
/** True once at least one usage-bearing assistant message was seen. */
|
|
59
|
+
readonly usageSeen?: boolean;
|
|
60
|
+
/** Context compaction lifecycle. */
|
|
61
|
+
readonly compaction?: 'running' | 'done' | 'failed';
|
|
62
|
+
/** Messages retained after compaction when the event reports it. */
|
|
63
|
+
readonly compactionKept?: number;
|
|
64
|
+
/** Latest teammate/task line (only one, later overwrites earlier). */
|
|
65
|
+
readonly teamLine?: string;
|
|
44
66
|
}
|
|
45
67
|
/** Structural subset of a dsh `SessionEvent` used by the reducer. */
|
|
46
68
|
export interface StatusEventLike {
|
package/dist/status.js
CHANGED
|
@@ -19,6 +19,42 @@
|
|
|
19
19
|
*
|
|
20
20
|
* @module dsh-caduceus/status
|
|
21
21
|
*/
|
|
22
|
+
const ZERO_USAGE = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
23
|
+
function usageFromEvent(data) {
|
|
24
|
+
const usage = data?.['usage'];
|
|
25
|
+
if (typeof usage !== 'object' || usage === null)
|
|
26
|
+
return undefined;
|
|
27
|
+
const record = usage;
|
|
28
|
+
const number = (key) => typeof record[key] === 'number' ? record[key] : 0;
|
|
29
|
+
const input = number('inputTokens');
|
|
30
|
+
const output = number('outputTokens');
|
|
31
|
+
const cacheRead = number('cacheReadTokens');
|
|
32
|
+
const cacheWrite = number('cacheWriteTokens');
|
|
33
|
+
if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0)
|
|
34
|
+
return undefined;
|
|
35
|
+
const total = typeof record['totalTokens'] === 'number'
|
|
36
|
+
? record['totalTokens']
|
|
37
|
+
: input + output + cacheRead + cacheWrite;
|
|
38
|
+
return { input, output, cacheRead, cacheWrite, total };
|
|
39
|
+
}
|
|
40
|
+
function addUsage(base, delta) {
|
|
41
|
+
const left = base ?? ZERO_USAGE;
|
|
42
|
+
return {
|
|
43
|
+
input: left.input + delta.input,
|
|
44
|
+
output: left.output + delta.output,
|
|
45
|
+
cacheRead: left.cacheRead + delta.cacheRead,
|
|
46
|
+
cacheWrite: left.cacheWrite + delta.cacheWrite,
|
|
47
|
+
total: left.total + delta.total,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function keptFromEvent(data) {
|
|
51
|
+
for (const key of ['kept', 'retained', 'messages', 'messageCount']) {
|
|
52
|
+
const value = data?.[key];
|
|
53
|
+
if (typeof value === 'number')
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
22
58
|
const INITIAL = { state: 'idle', steps: 0, tools: 0, turn: 0 };
|
|
23
59
|
function asRecord(value) {
|
|
24
60
|
return typeof value === 'object' && value !== null ? value : undefined;
|
|
@@ -39,11 +75,14 @@ export class StatusTracker {
|
|
|
39
75
|
case 'turn/start': {
|
|
40
76
|
const turn = typeof data?.['turn'] === 'number' ? data['turn'] : base.turn + 1;
|
|
41
77
|
this.snapshot = {
|
|
78
|
+
...base,
|
|
42
79
|
state: 'running',
|
|
43
80
|
steps: 0,
|
|
44
81
|
tools: 0,
|
|
45
82
|
turn,
|
|
46
83
|
...(time === undefined ? {} : { startedAt: time }),
|
|
84
|
+
...(base.usageTurn === undefined ? {} : { usagePrevious: base.usageTurn }),
|
|
85
|
+
usageTurn: ZERO_USAGE,
|
|
47
86
|
...common,
|
|
48
87
|
};
|
|
49
88
|
return this.snapshot;
|
|
@@ -60,6 +99,46 @@ export class StatusTracker {
|
|
|
60
99
|
this.snapshot = { ...base, tools: base.tools + 1, ...common };
|
|
61
100
|
return this.snapshot;
|
|
62
101
|
}
|
|
102
|
+
case 'assistant/message': {
|
|
103
|
+
const usage = usageFromEvent(data);
|
|
104
|
+
if (usage === undefined) {
|
|
105
|
+
this.snapshot = { ...base, ...common };
|
|
106
|
+
return this.snapshot;
|
|
107
|
+
}
|
|
108
|
+
this.snapshot = {
|
|
109
|
+
...base,
|
|
110
|
+
usageSeen: true,
|
|
111
|
+
usageTurn: addUsage(base.usageTurn, usage),
|
|
112
|
+
usageTotal: addUsage(base.usageTotal, usage),
|
|
113
|
+
...common,
|
|
114
|
+
};
|
|
115
|
+
return this.snapshot;
|
|
116
|
+
}
|
|
117
|
+
case 'compaction/start': {
|
|
118
|
+
this.snapshot = { ...base, compaction: 'running', ...common };
|
|
119
|
+
return this.snapshot;
|
|
120
|
+
}
|
|
121
|
+
case 'compaction/end': {
|
|
122
|
+
const failed = typeof data?.['error'] === 'string';
|
|
123
|
+
const kept = keptFromEvent(data);
|
|
124
|
+
this.snapshot = {
|
|
125
|
+
...base,
|
|
126
|
+
compaction: failed ? 'failed' : 'done',
|
|
127
|
+
...(kept === undefined ? {} : { compactionKept: kept }),
|
|
128
|
+
...common,
|
|
129
|
+
};
|
|
130
|
+
return this.snapshot;
|
|
131
|
+
}
|
|
132
|
+
case 'team/member':
|
|
133
|
+
case 'team/task': {
|
|
134
|
+
const line = teamLineFromEvent(event.type, data);
|
|
135
|
+
this.snapshot = {
|
|
136
|
+
...base,
|
|
137
|
+
...(line === undefined ? {} : { teamLine: line }),
|
|
138
|
+
...common,
|
|
139
|
+
};
|
|
140
|
+
return this.snapshot;
|
|
141
|
+
}
|
|
63
142
|
case 'turn/end': {
|
|
64
143
|
const turnData = (data ?? {});
|
|
65
144
|
const kind = turnData.reason?.kind;
|
|
@@ -90,6 +169,22 @@ export class StatusTracker {
|
|
|
90
169
|
this.snapshot = INITIAL;
|
|
91
170
|
}
|
|
92
171
|
}
|
|
172
|
+
function teamLineFromEvent(type, data) {
|
|
173
|
+
if (type === 'team/member') {
|
|
174
|
+
const member = asRecord(data?.['member']);
|
|
175
|
+
const name = typeof member?.['name'] === 'string' ? member['name'] : undefined;
|
|
176
|
+
const phase = typeof member?.['phase'] === 'string' ? member['phase'] : undefined;
|
|
177
|
+
if (name === undefined)
|
|
178
|
+
return undefined;
|
|
179
|
+
return `队友 ${name}:${phase ?? '更新'}`;
|
|
180
|
+
}
|
|
181
|
+
const task = asRecord(data?.['task']);
|
|
182
|
+
const subject = typeof task?.['subject'] === 'string' ? task['subject'] : undefined;
|
|
183
|
+
const status = typeof task?.['status'] === 'string' ? task['status'] : undefined;
|
|
184
|
+
if (subject === undefined)
|
|
185
|
+
return undefined;
|
|
186
|
+
return `队友 ${subject}:${status ?? '更新'}`;
|
|
187
|
+
}
|
|
93
188
|
/** Map a `turn/end` reason kind to the derived state. */
|
|
94
189
|
export function stateForTurnEnd(kind) {
|
|
95
190
|
switch (kind) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function unwrapFeishuData<T extends object>(response: unknown, operation: string): T;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Some Feishu SDK methods return a `{code,msg,data}` envelope, while the
|
|
3
|
+
* file/image upload endpoints return the bare `data` object. Normalize both.
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-caduceus/transport/feishu-response
|
|
6
|
+
*/
|
|
7
|
+
import { FeishuApiError } from './types.js';
|
|
8
|
+
export function unwrapFeishuData(response, operation) {
|
|
9
|
+
if (typeof response !== 'object' || response === null) {
|
|
10
|
+
throw new FeishuApiError(`feishu ${operation} returned no data`, -1);
|
|
11
|
+
}
|
|
12
|
+
const record = response;
|
|
13
|
+
if (typeof record.code === 'number') {
|
|
14
|
+
if (record.code !== 0) {
|
|
15
|
+
throw new FeishuApiError(`feishu ${operation} failed (code ${record.code}): ${typeof record.msg === 'string' ? record.msg : 'unknown error'}`, record.code);
|
|
16
|
+
}
|
|
17
|
+
const data = record.data;
|
|
18
|
+
if (typeof data !== 'object' || data === null) {
|
|
19
|
+
throw new FeishuApiError(`feishu ${operation} returned no data`, -1);
|
|
20
|
+
}
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
23
|
+
// Bare data shape (file/image upload endpoints in the installed SDK).
|
|
24
|
+
return record;
|
|
25
|
+
}
|
package/dist/transport/lark.js
CHANGED
|
@@ -13,6 +13,7 @@ import { silentLogger } from '../logger.js';
|
|
|
13
13
|
import { normalizeCardAction, normalizeMessageEvent, } from './normalize.js';
|
|
14
14
|
import { formatGateLine } from '../gate.js';
|
|
15
15
|
import { FeishuApiError } from './types.js';
|
|
16
|
+
import { unwrapFeishuData } from './feishu-response.js';
|
|
16
17
|
/** Long-connection Feishu implementation of {@link FeishuApi}. */
|
|
17
18
|
export class LarkFeishuApi {
|
|
18
19
|
client;
|
|
@@ -174,8 +175,8 @@ export class LarkFeishuApi {
|
|
|
174
175
|
file: Buffer.from(bytes),
|
|
175
176
|
},
|
|
176
177
|
}));
|
|
177
|
-
|
|
178
|
-
const fileKey =
|
|
178
|
+
const data = unwrapFeishuData(response, 'im.v1.file.create');
|
|
179
|
+
const fileKey = data.file_key;
|
|
179
180
|
if (fileKey === undefined || fileKey.length === 0) {
|
|
180
181
|
throw new FeishuApiError('feishu im.v1.file.create returned no file_key', -1);
|
|
181
182
|
}
|
|
@@ -188,8 +189,8 @@ export class LarkFeishuApi {
|
|
|
188
189
|
image: Buffer.from(bytes),
|
|
189
190
|
},
|
|
190
191
|
}));
|
|
191
|
-
|
|
192
|
-
const imageKey =
|
|
192
|
+
const data = unwrapFeishuData(response, 'im.v1.image.create');
|
|
193
|
+
const imageKey = data.image_key;
|
|
193
194
|
if (imageKey === undefined || imageKey.length === 0) {
|
|
194
195
|
throw new FeishuApiError('feishu im.v1.image.create returned no image_key', -1);
|
|
195
196
|
}
|