dsh-caduceus 0.12.1 → 0.12.3
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/bridge/bridge.d.ts +1 -1
- package/dist/bridge/bridge.js +95 -22
- package/dist/bridge/types.d.ts +6 -0
- package/dist/card-policy.d.ts +80 -0
- package/dist/card-policy.js +169 -0
- package/dist/config.d.ts +2 -2
- package/dist/index.js +17 -0
- package/dist/metrics.d.ts +7 -0
- package/dist/metrics.js +33 -0
- package/dist/render/cards.d.ts +26 -6
- package/dist/render/cards.js +61 -13
- package/lib/client.js +1 -1
- package/package.json +1 -1
package/dist/bridge/bridge.d.ts
CHANGED
|
@@ -35,4 +35,4 @@ export declare class Bridge {
|
|
|
35
35
|
}>;
|
|
36
36
|
}
|
|
37
37
|
/** Build the process card for one snapshot (exported for render tests). */
|
|
38
|
-
export declare function progressCardForSnapshot(snapshot: StatusSnapshot, sessionTitle: string | undefined, recentEvent: string, tail: string, now: number, presetMissing?: boolean, extraLines?: readonly string[], stopButton?: boolean): FeishuCard;
|
|
38
|
+
export declare function progressCardForSnapshot(snapshot: StatusSnapshot, sessionTitle: string | undefined, recentEvent: string, tail: string, now: number, presetMissing?: boolean, extraLines?: readonly string[], stopButton?: boolean, stepLines?: readonly string[], stepNote?: string): FeishuCard;
|
package/dist/bridge/bridge.js
CHANGED
|
@@ -29,11 +29,12 @@ import { writeOwnership } from '../ownership.js';
|
|
|
29
29
|
import { defaultExportCacheDir } from '../home.js';
|
|
30
30
|
import { extractMediaMarkers, resolveMedia } from '../media.js';
|
|
31
31
|
import { aggregateUsage, renderUsageReport } from '../usage.js';
|
|
32
|
-
import { aggregateMetrics, formatMetricsLine } from '../metrics.js';
|
|
32
|
+
import { aggregateMetrics, formatMetricsLine, formatPerformanceLines } from '../metrics.js';
|
|
33
|
+
import { DEFAULT_CARD_POLICY, foldsAtTurnEnd, formatStepLines, showsStepElapsed, stepLimit, stepLinesFromEvents, } from '../card-policy.js';
|
|
33
34
|
import { presetLabel, t } from '../i18n.js';
|
|
34
35
|
import { buildPermissionCard, renderPermissionText, } from '../render/permission-card.js';
|
|
35
36
|
import { readSessionLogFile, renderExportJsonl, renderExportMarkdown, truncateExport, writeExportFile, } from '../export.js';
|
|
36
|
-
import { finalCard, finalCardPlainText, progressCard, } from '../render/cards.js';
|
|
37
|
+
import { clipLabel, collapsedProcessCard, finalCard, finalCardPlainText, progressCard, } from '../render/cards.js';
|
|
37
38
|
import { CardStream, systemClock } from '../render/stream.js';
|
|
38
39
|
import { mintSessionId, scopeKey, SCOPE_KEY_SEPARATOR } from '../session-key.js';
|
|
39
40
|
import { StatusTracker, shouldNotifyTerminal, stateForTurnEnd, recentEventText, statusLabel, statusTemplate, statusText, terminalNotice, } from '../status.js';
|
|
@@ -230,6 +231,7 @@ export class Bridge {
|
|
|
230
231
|
state.running = true;
|
|
231
232
|
state.stopRequested = false;
|
|
232
233
|
state.lastOutput = '';
|
|
234
|
+
state.replies = [];
|
|
233
235
|
state.progress?.dispose();
|
|
234
236
|
state.progress = this.#makeCardStream(state);
|
|
235
237
|
await this.#renderProgress(state);
|
|
@@ -262,8 +264,15 @@ export class Bridge {
|
|
|
262
264
|
}
|
|
263
265
|
case 'assistant/message': {
|
|
264
266
|
const text = assistantTextFromEvent(event.data);
|
|
265
|
-
if (text.length > 0)
|
|
267
|
+
if (text.length > 0) {
|
|
266
268
|
state.lastOutput = text;
|
|
269
|
+
const trimmed = text.trim();
|
|
270
|
+
if (trimmed.length > 0 && state.replies[state.replies.length - 1] !== trimmed) {
|
|
271
|
+
state.replies.push(trimmed);
|
|
272
|
+
if (state.replies.length > 3)
|
|
273
|
+
state.replies.shift();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
267
276
|
await this.#renderProgress(state);
|
|
268
277
|
break;
|
|
269
278
|
}
|
|
@@ -295,6 +304,16 @@ export class Bridge {
|
|
|
295
304
|
}
|
|
296
305
|
async #processInbound(message) {
|
|
297
306
|
const state = this.#stateFor(message);
|
|
307
|
+
const pending = state.pendingCollapse;
|
|
308
|
+
if (pending !== undefined) {
|
|
309
|
+
state.pendingCollapse = undefined;
|
|
310
|
+
try {
|
|
311
|
+
await this.#transport.updateCard(pending.messageId, pending.card);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
this.#logger.warn(`deferred collapse failed: ${errorMessage(error)}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
298
317
|
state.lastInboundMessageId = message.messageId;
|
|
299
318
|
state.ownerOpenId = message.senderOpenId;
|
|
300
319
|
// Startup-stage failures have no session to notify yet; the first message
|
|
@@ -860,21 +879,30 @@ export class Bridge {
|
|
|
860
879
|
state.progress = undefined;
|
|
861
880
|
}
|
|
862
881
|
const card = finalCard(finalInput);
|
|
882
|
+
// 过程卡按 dsh 的「工作步骤收起时机」处理:completion 立刻置灰收起;
|
|
883
|
+
// next-input 留到下一次输入再收,正文始终另发一张摘要卡。
|
|
884
|
+
const policy = this.#policy();
|
|
863
885
|
if (messageId !== undefined) {
|
|
864
|
-
|
|
865
|
-
|
|
886
|
+
const collapsed = this.#collapsedCard(state, snapshot, kind);
|
|
887
|
+
if (foldsAtTurnEnd(policy)) {
|
|
888
|
+
try {
|
|
889
|
+
if (process.env.CADUCEUS_DEBUG === '1')
|
|
890
|
+
console.error('[dbg] collapse', messageId);
|
|
891
|
+
await this.#transport.updateCard(messageId, collapsed);
|
|
892
|
+
}
|
|
893
|
+
catch (error) {
|
|
894
|
+
this.#logger.warn(`collapse patch failed: ${errorMessage(error)}`);
|
|
895
|
+
}
|
|
866
896
|
}
|
|
867
|
-
|
|
868
|
-
|
|
897
|
+
else {
|
|
898
|
+
state.pendingCollapse = { messageId, card: collapsed };
|
|
869
899
|
}
|
|
870
900
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
await this.#finalFallback(state, finalInput, error);
|
|
877
|
-
}
|
|
901
|
+
try {
|
|
902
|
+
await this.#sendNewCard(state, card);
|
|
903
|
+
}
|
|
904
|
+
catch (error) {
|
|
905
|
+
await this.#finalFallback(state, finalInput, error);
|
|
878
906
|
}
|
|
879
907
|
await this.#sendOutboundMedia(state, outbound.entries);
|
|
880
908
|
if (shouldNotifyTerminal(kind)) {
|
|
@@ -944,7 +972,7 @@ export class Bridge {
|
|
|
944
972
|
#finalCardInput(state, snapshot, body, finalState, finalStateLabel) {
|
|
945
973
|
const sessionTitle = this.#titleOf(state)?.trim() ?? state.title;
|
|
946
974
|
return {
|
|
947
|
-
title: '飞书会话 ·
|
|
975
|
+
title: '飞书会话 · 回答',
|
|
948
976
|
// Event-derived, never the (possibly stale) tracker snapshot.
|
|
949
977
|
stateLabel: finalStateLabel,
|
|
950
978
|
steps: snapshot.steps,
|
|
@@ -1714,6 +1742,7 @@ export class Bridge {
|
|
|
1714
1742
|
running: false,
|
|
1715
1743
|
status: new StatusTracker(),
|
|
1716
1744
|
lastOutput: '',
|
|
1745
|
+
replies: [],
|
|
1717
1746
|
reactionQueue: [],
|
|
1718
1747
|
reactionStarts: new Map(),
|
|
1719
1748
|
logEvents: [],
|
|
@@ -1729,6 +1758,8 @@ export class Bridge {
|
|
|
1729
1758
|
return sent;
|
|
1730
1759
|
},
|
|
1731
1760
|
update: async (messageId, card) => {
|
|
1761
|
+
if (process.env.CADUCEUS_DEBUG === '1')
|
|
1762
|
+
console.error('[dbg] stream.update', messageId, card.header?.title.content);
|
|
1732
1763
|
await this.#transport.updateCard(messageId, card);
|
|
1733
1764
|
},
|
|
1734
1765
|
};
|
|
@@ -1854,16 +1885,56 @@ export class Bridge {
|
|
|
1854
1885
|
extraLines.push('上下文压缩失败');
|
|
1855
1886
|
if (snapshot.teamLine !== undefined)
|
|
1856
1887
|
extraLines.push(snapshot.teamLine);
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1888
|
+
// 性能与用量:dsh 的 ui-chat.performanceUsage 决定密度;caduceus 的
|
|
1889
|
+
// `metrics: off` 仅作排障用的整体关闭开关。
|
|
1890
|
+
const policy = this.#policy();
|
|
1891
|
+
if (this.#config.metrics !== 'off') {
|
|
1892
|
+
const metrics = await this.#metricsFor(state, snapshot);
|
|
1893
|
+
for (const line of formatPerformanceLines(metrics, policy.performance)) {
|
|
1894
|
+
extraLines.push(line);
|
|
1895
|
+
}
|
|
1862
1896
|
}
|
|
1897
|
+
const steps = this.#stepLines(state, snapshot, policy);
|
|
1863
1898
|
if (state.stopRequested === true) {
|
|
1864
1899
|
extraLines.push('已停止当前回合。后台任务仍在运行,完成后结果会回来。');
|
|
1865
1900
|
}
|
|
1866
|
-
return progressCardForSnapshot(snapshot, state.title, recentEventText(snapshot), state.lastOutput, this.#now(), state.sessionId !== undefined && this.#presetFailures.has(state.sessionId), extraLines, state.running && state.stopRequested !== true);
|
|
1901
|
+
return progressCardForSnapshot(snapshot, state.title, recentEventText(snapshot), state.lastOutput, this.#now(), state.sessionId !== undefined && this.#presetFailures.has(state.sessionId), extraLines, state.running && state.stopRequested !== true, steps.lines, steps.note);
|
|
1902
|
+
}
|
|
1903
|
+
/** Card policy from dsh's live settings; defaults only when unavailable. */
|
|
1904
|
+
#policy() {
|
|
1905
|
+
try {
|
|
1906
|
+
return this.#options.cardPolicyProvider?.() ?? DEFAULT_CARD_POLICY;
|
|
1907
|
+
}
|
|
1908
|
+
catch {
|
|
1909
|
+
return DEFAULT_CARD_POLICY;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
/** Step rows for the current turn, limited by the transcript level. */
|
|
1913
|
+
#stepLines(state, snapshot, policy) {
|
|
1914
|
+
const all = stepLinesFromEvents(state.logEvents, Number.POSITIVE_INFINITY, {
|
|
1915
|
+
turn: snapshot.turn,
|
|
1916
|
+
elapsed: showsStepElapsed(policy.transcript),
|
|
1917
|
+
});
|
|
1918
|
+
const limit = stepLimit(policy.transcript);
|
|
1919
|
+
const shown = Number.isFinite(limit) ? all.slice(Math.max(0, all.length - limit)) : all;
|
|
1920
|
+
const lines = formatStepLines(shown);
|
|
1921
|
+
const note = all.length > shown.length ? `共 ${all.length} 次调用,显示最近 ${shown.length} 条` : undefined;
|
|
1922
|
+
return { lines, ...(note === undefined ? {} : { note }) };
|
|
1923
|
+
}
|
|
1924
|
+
/** Greyed, collapsed record of the finished turn's process card. */
|
|
1925
|
+
#collapsedCard(state, snapshot, kind) {
|
|
1926
|
+
const policy = this.#policy();
|
|
1927
|
+
const steps = this.#stepLines(state, snapshot, policy);
|
|
1928
|
+
const card = progressCardForSnapshot(snapshot, state.title, recentEventText(snapshot), '', this.#now(), state.sessionId !== undefined && this.#presetFailures.has(state.sessionId), [], false, steps.lines, steps.note);
|
|
1929
|
+
const noteLines = [`已收起 · ${snapshot.steps} 步`];
|
|
1930
|
+
if (kind !== undefined && kind !== 'completed')
|
|
1931
|
+
noteLines[0] += ` · ${statusLabel(stateForTurnEnd(kind))}`;
|
|
1932
|
+
if (policy.foldedReplies === 'all' && state.replies.length > 0) {
|
|
1933
|
+
for (const [index, reply] of state.replies.slice(-3).entries()) {
|
|
1934
|
+
noteLines.push(`过程回复 ${index + 1}:${clipLabel(reply.replace(/\s+/g, ' '), 60)}`);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
return collapsedProcessCard(card, noteLines.join('\n'));
|
|
1867
1938
|
}
|
|
1868
1939
|
async #replyText(state, text) {
|
|
1869
1940
|
if (text.length === 0)
|
|
@@ -1883,10 +1954,12 @@ export class Bridge {
|
|
|
1883
1954
|
}
|
|
1884
1955
|
}
|
|
1885
1956
|
/** Build the process card for one snapshot (exported for render tests). */
|
|
1886
|
-
export function progressCardForSnapshot(snapshot, sessionTitle, recentEvent, tail, now, presetMissing = false, extraLines = [], stopButton = false) {
|
|
1957
|
+
export function progressCardForSnapshot(snapshot, sessionTitle, recentEvent, tail, now, presetMissing = false, extraLines = [], stopButton = false, stepLines = [], stepNote) {
|
|
1887
1958
|
return progressCard({
|
|
1888
1959
|
title: '飞书会话 · 过程',
|
|
1889
1960
|
...(extraLines.length === 0 ? {} : { extraLines }),
|
|
1961
|
+
...(stepLines.length === 0 ? {} : { stepLines }),
|
|
1962
|
+
...(stepNote === undefined || stepNote.length === 0 ? {} : { stepNote }),
|
|
1890
1963
|
...(stopButton ? { stopButton: true } : {}),
|
|
1891
1964
|
stateLabel: statusLabel(snapshot.state),
|
|
1892
1965
|
steps: snapshot.steps,
|
package/dist/bridge/types.d.ts
CHANGED
|
@@ -171,6 +171,12 @@ export interface BridgeOptions {
|
|
|
171
171
|
readonly permissionPresetsProvider?: () => PermissionPresetsLike | undefined;
|
|
172
172
|
/** Session log reader for `/usage` (`sessionQuery.readSession().events`). */
|
|
173
173
|
readonly usageProvider?: (sessionId: string) => Promise<readonly unknown[] | undefined> | readonly unknown[] | undefined;
|
|
174
|
+
/**
|
|
175
|
+
* Card presentation level, read live from dsh's own settings
|
|
176
|
+
* (`ui-chat` / `ui-settings-general`). Resolved lazily per render so a
|
|
177
|
+
* settings change applies without a restart.
|
|
178
|
+
*/
|
|
179
|
+
readonly cardPolicyProvider?: () => import('../card-policy.js').CardPolicy;
|
|
174
180
|
/** Locale source (dsh settings/env); default zh. */
|
|
175
181
|
readonly languageProvider?: () => import('../i18n.js').Lang;
|
|
176
182
|
/** Lazily resolve dsh's approval service (agent-scoped setPolicy). */
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card presentation policy — mirrors dsh's own Chat/General settings so the
|
|
3
|
+
* Feishu card shows the same amount of process detail as the Web UI instead of
|
|
4
|
+
* inventing a second set of knobs.
|
|
5
|
+
*
|
|
6
|
+
* Read live from the `settings` service (`describe()`), which returns the
|
|
7
|
+
* active profile's entry values:
|
|
8
|
+
*
|
|
9
|
+
* - `ui-chat.transcriptView` 工作步骤展示
|
|
10
|
+
* - `ui-chat.collapseTiming` 工作步骤收起时机
|
|
11
|
+
* - `ui-chat.foldedReplies` 收起后显示的回复
|
|
12
|
+
* - `ui-chat.performanceUsage` 性能与用量
|
|
13
|
+
* - `ui-settings-general.developerTools` 显示代码工作视图
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-caduceus/card-policy
|
|
16
|
+
*/
|
|
17
|
+
/** Transcript detail levels accepted by dsh's Chat settings. */
|
|
18
|
+
export declare const TRANSCRIPT_MODES: readonly ["compact", "standard", "detailed", "verbose"];
|
|
19
|
+
export type TranscriptMode = (typeof TRANSCRIPT_MODES)[number];
|
|
20
|
+
/** When the completed turn's process folds away. */
|
|
21
|
+
export declare const COLLAPSE_TIMINGS: readonly ["completion", "next-input"];
|
|
22
|
+
export type CollapseTiming = (typeof COLLAPSE_TIMINGS)[number];
|
|
23
|
+
/** Which assistant replies stay visible once the process folds. */
|
|
24
|
+
export declare const FOLDED_REPLIES: readonly ["all", "final"];
|
|
25
|
+
export type FoldedReplies = (typeof FOLDED_REPLIES)[number];
|
|
26
|
+
/** Performance/usage line density. */
|
|
27
|
+
export declare const PERFORMANCE_MODES: readonly ["compact", "detailed"];
|
|
28
|
+
export type PerformanceMode = (typeof PERFORMANCE_MODES)[number];
|
|
29
|
+
/** Everything the card renderer needs to know about the user's detail level. */
|
|
30
|
+
export interface CardPolicy {
|
|
31
|
+
readonly transcript: TranscriptMode;
|
|
32
|
+
readonly collapseTiming: CollapseTiming;
|
|
33
|
+
readonly foldedReplies: FoldedReplies;
|
|
34
|
+
readonly developerTools: boolean;
|
|
35
|
+
readonly performance: PerformanceMode;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* dsh's shipped defaults. Used only when the settings service is unavailable
|
|
39
|
+
* (headless boot, older runtime) — never as a user-facing default override.
|
|
40
|
+
*/
|
|
41
|
+
export declare const DEFAULT_CARD_POLICY: CardPolicy;
|
|
42
|
+
/** One `settings.describe()` entry, narrowed to what the policy reads. */
|
|
43
|
+
export interface SettingsEntryLike {
|
|
44
|
+
readonly ns?: unknown;
|
|
45
|
+
readonly id?: unknown;
|
|
46
|
+
readonly value?: unknown;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Map live settings entries onto the card policy. Unknown or malformed values
|
|
50
|
+
* fall back to dsh's defaults per field, so one bad field never blanks the card.
|
|
51
|
+
*/
|
|
52
|
+
export declare function cardPolicyFromSettings(entries: readonly SettingsEntryLike[] | undefined): CardPolicy;
|
|
53
|
+
/** How many step lines a transcript mode shows; `Infinity` = the whole turn. */
|
|
54
|
+
export declare function stepLimit(mode: TranscriptMode): number;
|
|
55
|
+
/** Per-step elapsed time is part of the `detailed` and `verbose` levels. */
|
|
56
|
+
export declare function showsStepElapsed(mode: TranscriptMode): boolean;
|
|
57
|
+
/** Tool invocation total belongs to `detailed` and up. */
|
|
58
|
+
export declare function showsToolCount(mode: TranscriptMode): boolean;
|
|
59
|
+
/** Fold the process card as soon as the turn ends (`next-input` defers it). */
|
|
60
|
+
export declare function foldsAtTurnEnd(policy: CardPolicy): boolean;
|
|
61
|
+
/** One rendered step line. */
|
|
62
|
+
export interface StepLine {
|
|
63
|
+
readonly label: string;
|
|
64
|
+
readonly done: boolean;
|
|
65
|
+
readonly durationMs?: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Reduce the in-memory event window to step lines for the current turn.
|
|
69
|
+
*
|
|
70
|
+
* `tool/call` carries the name and `tool/result` carries the matching
|
|
71
|
+
* `message.source.callId`, so a call is `done` once its result arrives and the
|
|
72
|
+
* pair yields a duration. Only the last `limit` lines are returned; callers
|
|
73
|
+
* surface the total separately.
|
|
74
|
+
*/
|
|
75
|
+
export declare function stepLinesFromEvents(events: readonly unknown[], limit: number, options?: {
|
|
76
|
+
readonly turn?: number;
|
|
77
|
+
readonly elapsed?: boolean;
|
|
78
|
+
}): StepLine[];
|
|
79
|
+
/** Render step lines the way the card shows them: one tool per row. */
|
|
80
|
+
export declare function formatStepLines(lines: readonly StepLine[]): string[];
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card presentation policy — mirrors dsh's own Chat/General settings so the
|
|
3
|
+
* Feishu card shows the same amount of process detail as the Web UI instead of
|
|
4
|
+
* inventing a second set of knobs.
|
|
5
|
+
*
|
|
6
|
+
* Read live from the `settings` service (`describe()`), which returns the
|
|
7
|
+
* active profile's entry values:
|
|
8
|
+
*
|
|
9
|
+
* - `ui-chat.transcriptView` 工作步骤展示
|
|
10
|
+
* - `ui-chat.collapseTiming` 工作步骤收起时机
|
|
11
|
+
* - `ui-chat.foldedReplies` 收起后显示的回复
|
|
12
|
+
* - `ui-chat.performanceUsage` 性能与用量
|
|
13
|
+
* - `ui-settings-general.developerTools` 显示代码工作视图
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-caduceus/card-policy
|
|
16
|
+
*/
|
|
17
|
+
/** Transcript detail levels accepted by dsh's Chat settings. */
|
|
18
|
+
export const TRANSCRIPT_MODES = ['compact', 'standard', 'detailed', 'verbose'];
|
|
19
|
+
/** When the completed turn's process folds away. */
|
|
20
|
+
export const COLLAPSE_TIMINGS = ['completion', 'next-input'];
|
|
21
|
+
/** Which assistant replies stay visible once the process folds. */
|
|
22
|
+
export const FOLDED_REPLIES = ['all', 'final'];
|
|
23
|
+
/** Performance/usage line density. */
|
|
24
|
+
export const PERFORMANCE_MODES = ['compact', 'detailed'];
|
|
25
|
+
/**
|
|
26
|
+
* dsh's shipped defaults. Used only when the settings service is unavailable
|
|
27
|
+
* (headless boot, older runtime) — never as a user-facing default override.
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_CARD_POLICY = {
|
|
30
|
+
transcript: 'compact',
|
|
31
|
+
collapseTiming: 'completion',
|
|
32
|
+
foldedReplies: 'final',
|
|
33
|
+
developerTools: false,
|
|
34
|
+
performance: 'compact',
|
|
35
|
+
};
|
|
36
|
+
function asRecord(value) {
|
|
37
|
+
return typeof value === 'object' && value !== null
|
|
38
|
+
? value
|
|
39
|
+
: undefined;
|
|
40
|
+
}
|
|
41
|
+
function entryValue(entries, namespace) {
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const key = typeof entry.ns === 'string' ? entry.ns : entry.id;
|
|
44
|
+
if (key === namespace)
|
|
45
|
+
return asRecord(entry.value);
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
function pick(record, field, allowed, fallback) {
|
|
50
|
+
const raw = record?.[field];
|
|
51
|
+
return typeof raw === 'string' && allowed.includes(raw)
|
|
52
|
+
? raw
|
|
53
|
+
: fallback;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Map live settings entries onto the card policy. Unknown or malformed values
|
|
57
|
+
* fall back to dsh's defaults per field, so one bad field never blanks the card.
|
|
58
|
+
*/
|
|
59
|
+
export function cardPolicyFromSettings(entries) {
|
|
60
|
+
if (entries === undefined)
|
|
61
|
+
return DEFAULT_CARD_POLICY;
|
|
62
|
+
const chat = entryValue(entries, 'ui-chat');
|
|
63
|
+
const general = entryValue(entries, 'ui-settings-general');
|
|
64
|
+
const developerTools = general?.['developerTools'];
|
|
65
|
+
return {
|
|
66
|
+
transcript: pick(chat, 'transcriptView', TRANSCRIPT_MODES, DEFAULT_CARD_POLICY.transcript),
|
|
67
|
+
collapseTiming: pick(chat, 'collapseTiming', COLLAPSE_TIMINGS, DEFAULT_CARD_POLICY.collapseTiming),
|
|
68
|
+
foldedReplies: pick(chat, 'foldedReplies', FOLDED_REPLIES, DEFAULT_CARD_POLICY.foldedReplies),
|
|
69
|
+
developerTools: typeof developerTools === 'boolean' ? developerTools : DEFAULT_CARD_POLICY.developerTools,
|
|
70
|
+
performance: pick(chat, 'performanceUsage', PERFORMANCE_MODES, DEFAULT_CARD_POLICY.performance),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** How many step lines a transcript mode shows; `Infinity` = the whole turn. */
|
|
74
|
+
export function stepLimit(mode) {
|
|
75
|
+
switch (mode) {
|
|
76
|
+
case 'compact':
|
|
77
|
+
return 1;
|
|
78
|
+
case 'standard':
|
|
79
|
+
return 3;
|
|
80
|
+
case 'detailed':
|
|
81
|
+
return 5;
|
|
82
|
+
case 'verbose':
|
|
83
|
+
return Number.POSITIVE_INFINITY;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Per-step elapsed time is part of the `detailed` and `verbose` levels. */
|
|
87
|
+
export function showsStepElapsed(mode) {
|
|
88
|
+
return mode === 'detailed' || mode === 'verbose';
|
|
89
|
+
}
|
|
90
|
+
/** Tool invocation total belongs to `detailed` and up. */
|
|
91
|
+
export function showsToolCount(mode) {
|
|
92
|
+
return mode !== 'compact';
|
|
93
|
+
}
|
|
94
|
+
/** Fold the process card as soon as the turn ends (`next-input` defers it). */
|
|
95
|
+
export function foldsAtTurnEnd(policy) {
|
|
96
|
+
return policy.collapseTiming === 'completion';
|
|
97
|
+
}
|
|
98
|
+
function seconds(ms) {
|
|
99
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Reduce the in-memory event window to step lines for the current turn.
|
|
103
|
+
*
|
|
104
|
+
* `tool/call` carries the name and `tool/result` carries the matching
|
|
105
|
+
* `message.source.callId`, so a call is `done` once its result arrives and the
|
|
106
|
+
* pair yields a duration. Only the last `limit` lines are returned; callers
|
|
107
|
+
* surface the total separately.
|
|
108
|
+
*/
|
|
109
|
+
export function stepLinesFromEvents(events, limit, options = {}) {
|
|
110
|
+
const calls = new Map();
|
|
111
|
+
const order = [];
|
|
112
|
+
for (const raw of events) {
|
|
113
|
+
const event = asRecord(raw);
|
|
114
|
+
if (event === undefined)
|
|
115
|
+
continue;
|
|
116
|
+
const data = asRecord(event.data);
|
|
117
|
+
if (data === undefined)
|
|
118
|
+
continue;
|
|
119
|
+
if (options.turn !== undefined) {
|
|
120
|
+
const turn = data['turn'];
|
|
121
|
+
if (typeof turn === 'number' && turn !== options.turn)
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const time = typeof event.time === 'number' ? event.time : undefined;
|
|
125
|
+
if (event.type === 'tool/call') {
|
|
126
|
+
const callId = typeof data['callId'] === 'string' ? data['callId'] : undefined;
|
|
127
|
+
const name = typeof data['name'] === 'string' ? data['name'] : '调用';
|
|
128
|
+
if (callId === undefined)
|
|
129
|
+
continue;
|
|
130
|
+
calls.set(callId, { label: name, at: time ?? 0, done: false });
|
|
131
|
+
order.push(callId);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (event.type === 'tool/result') {
|
|
135
|
+
const message = asRecord(data['message']);
|
|
136
|
+
const source = asRecord(message?.['source']);
|
|
137
|
+
const callId = typeof source?.['callId'] === 'string' ? source['callId'] : undefined;
|
|
138
|
+
if (callId === undefined)
|
|
139
|
+
continue;
|
|
140
|
+
const entry = calls.get(callId);
|
|
141
|
+
if (entry === undefined)
|
|
142
|
+
continue;
|
|
143
|
+
entry.done = true;
|
|
144
|
+
if (time !== undefined && entry.at > 0)
|
|
145
|
+
entry.durationMs = Math.max(0, time - entry.at);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const lines = order
|
|
149
|
+
.map((callId) => calls.get(callId))
|
|
150
|
+
.filter((entry) => entry !== undefined)
|
|
151
|
+
.map((entry) => ({
|
|
152
|
+
label: entry.label,
|
|
153
|
+
done: entry.done,
|
|
154
|
+
...(options.elapsed === true && entry.durationMs !== undefined
|
|
155
|
+
? { durationMs: entry.durationMs }
|
|
156
|
+
: {}),
|
|
157
|
+
}));
|
|
158
|
+
if (!Number.isFinite(limit))
|
|
159
|
+
return lines;
|
|
160
|
+
return lines.slice(Math.max(0, lines.length - limit));
|
|
161
|
+
}
|
|
162
|
+
/** Render step lines the way the card shows them: one tool per row. */
|
|
163
|
+
export function formatStepLines(lines) {
|
|
164
|
+
return lines.map((line) => {
|
|
165
|
+
const mark = line.done ? '✓' : '▸';
|
|
166
|
+
const duration = line.durationMs === undefined ? '' : ` · ${seconds(line.durationMs)}`;
|
|
167
|
+
return `${mark} ${line.label}${duration}`;
|
|
168
|
+
});
|
|
169
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -92,7 +92,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
92
92
|
admins: z<string[], string[], "plain">;
|
|
93
93
|
lang: z<"zh" | "en" | "auto", "zh" | "en" | "auto", "plain">;
|
|
94
94
|
exportCacheMaxAgeHours: z<number, number, "plain">;
|
|
95
|
-
metrics: z<"
|
|
95
|
+
metrics: z<"compact" | "detailed" | "off", "compact" | "detailed" | "off", "plain">;
|
|
96
96
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
97
97
|
appId: z<string, string, "plain">;
|
|
98
98
|
appSecret: z<string, string, "plain">;
|
|
@@ -124,7 +124,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
124
124
|
admins: z<string[], string[], "plain">;
|
|
125
125
|
lang: z<"zh" | "en" | "auto", "zh" | "en" | "auto", "plain">;
|
|
126
126
|
exportCacheMaxAgeHours: z<number, number, "plain">;
|
|
127
|
-
metrics: z<"
|
|
127
|
+
metrics: z<"compact" | "detailed" | "off", "compact" | "detailed" | "off", "plain">;
|
|
128
128
|
}>>, "plain">;
|
|
129
129
|
/** Default card patch throttle: half of Feishu's 5 QPS budget. */
|
|
130
130
|
export declare const DEFAULT_CARD_THROTTLE_MS = 400;
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { createRequire } from 'node:module';
|
|
|
15
15
|
import { dirname, join } from 'node:path';
|
|
16
16
|
import { backfillSessionOwnership, scanSessionDirs } from './backfill.js';
|
|
17
17
|
import { Bridge } from './bridge/bridge.js';
|
|
18
|
+
import { cardPolicyFromSettings } from './card-policy.js';
|
|
18
19
|
import { runHarnessCommand } from './commands/harness.js';
|
|
19
20
|
import { cleanupExportCache, parseSessionEvents } from './export.js';
|
|
20
21
|
import { resolveLang } from './i18n.js';
|
|
@@ -54,6 +55,20 @@ function agentOptions(ctx) {
|
|
|
54
55
|
return {};
|
|
55
56
|
}
|
|
56
57
|
}
|
|
58
|
+
/** Read dsh's live Chat/General settings as a card policy (lazy each render). */
|
|
59
|
+
function makeCardPolicy(ctx) {
|
|
60
|
+
const read = () => {
|
|
61
|
+
try {
|
|
62
|
+
const settings = ctx.get('settings');
|
|
63
|
+
const entries = settings?.describe?.();
|
|
64
|
+
return cardPolicyFromSettings(entries);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return cardPolicyFromSettings(undefined);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return read;
|
|
71
|
+
}
|
|
57
72
|
/** Adapt `ctx.get('agents')` to the bridge's structural seam. */
|
|
58
73
|
function makeAgents(ctx) {
|
|
59
74
|
const service = () => ctx.get('agents');
|
|
@@ -217,6 +232,8 @@ export function apply(ctx, config = {}) {
|
|
|
217
232
|
sessionIdsProvider: () => scanSessionDirs().map((entry) => entry.sessionId),
|
|
218
233
|
// Lazy per use: the preset registry can register AFTER this plugin mounts.
|
|
219
234
|
agentPresetsProvider: () => ctx.get('agentPresets'),
|
|
235
|
+
// Card detail follows dsh's own Chat/General settings (live, no restart).
|
|
236
|
+
cardPolicyProvider: makeCardPolicy(ctx),
|
|
220
237
|
selfCheck,
|
|
221
238
|
interactive,
|
|
222
239
|
rootCtx: ctx,
|
package/dist/metrics.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module dsh-caduceus/metrics
|
|
9
9
|
*/
|
|
10
|
+
import type { PerformanceMode } from './card-policy.js';
|
|
10
11
|
export type MetricsMode = 'off' | 'compact' | 'detailed';
|
|
11
12
|
export interface Metrics {
|
|
12
13
|
readonly rounds: number;
|
|
@@ -26,3 +27,9 @@ export declare function aggregateMetrics(events: readonly unknown[]): Metrics;
|
|
|
26
27
|
export declare function formatTokenCount(value: number): string;
|
|
27
28
|
/** One-line metrics summary; empty when mode is off or nothing to show. */
|
|
28
29
|
export declare function formatMetricsLine(metrics: Metrics, mode: MetricsMode): string;
|
|
30
|
+
/**
|
|
31
|
+
* Performance/usage block, laid out per the user's dsh `性能与用量` level:
|
|
32
|
+
* `compact` is one dense line, `detailed` splits rate from volume so neither
|
|
33
|
+
* line has to be read across a long `·` chain.
|
|
34
|
+
*/
|
|
35
|
+
export declare function formatPerformanceLines(metrics: Metrics, performance: PerformanceMode): string[];
|
package/dist/metrics.js
CHANGED
|
@@ -133,3 +133,36 @@ export function formatMetricsLine(metrics, mode) {
|
|
|
133
133
|
}
|
|
134
134
|
return parts.join(' · ');
|
|
135
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Performance/usage block, laid out per the user's dsh `性能与用量` level:
|
|
138
|
+
* `compact` is one dense line, `detailed` splits rate from volume so neither
|
|
139
|
+
* line has to be read across a long `·` chain.
|
|
140
|
+
*/
|
|
141
|
+
export function formatPerformanceLines(metrics, performance) {
|
|
142
|
+
const rate = [];
|
|
143
|
+
if (metrics.tokensPerSecond !== undefined) {
|
|
144
|
+
rate.push(`${Math.round(metrics.tokensPerSecond)} tok/s`);
|
|
145
|
+
}
|
|
146
|
+
if (metrics.cacheHitPercent !== undefined)
|
|
147
|
+
rate.push(`缓存 ${metrics.cacheHitPercent}%`);
|
|
148
|
+
if (metrics.contextPercent !== undefined)
|
|
149
|
+
rate.push(`上下文 ${metrics.contextPercent}%`);
|
|
150
|
+
const volume = [];
|
|
151
|
+
if (metrics.rounds > 0)
|
|
152
|
+
volume.push(`${metrics.rounds} 轮`);
|
|
153
|
+
if (metrics.steps > 0)
|
|
154
|
+
volume.push(`${metrics.steps} 步`);
|
|
155
|
+
if (metrics.totalTokens > 0)
|
|
156
|
+
volume.push(`${formatTokenCount(metrics.totalTokens)} tok`);
|
|
157
|
+
if (performance === 'compact') {
|
|
158
|
+
// 简洁档:一行放完,但把速度、总量、缓存分开读得懂的顺序。
|
|
159
|
+
const merged = [...volume, ...rate];
|
|
160
|
+
return merged.length === 0 ? [] : [merged.join(' · ')];
|
|
161
|
+
}
|
|
162
|
+
const lines = [];
|
|
163
|
+
if (rate.length > 0)
|
|
164
|
+
lines.push(`性能:${rate.join(' · ')}`);
|
|
165
|
+
if (volume.length > 0)
|
|
166
|
+
lines.push(`用量:${volume.join(' · ')}`);
|
|
167
|
+
return lines;
|
|
168
|
+
}
|
package/dist/render/cards.d.ts
CHANGED
|
@@ -36,24 +36,44 @@ export interface ProgressCardInput {
|
|
|
36
36
|
/** Unix epoch ms when the turn started; used for the elapsed line. */
|
|
37
37
|
readonly startedAt?: number;
|
|
38
38
|
readonly now?: number;
|
|
39
|
-
/** Session title shown
|
|
39
|
+
/** Session title shown above the state line when known. */
|
|
40
40
|
readonly sessionTitle?: string;
|
|
41
|
-
/** Latest tool/event label (
|
|
41
|
+
/** Latest tool/event label (kept for callers that have no step log). */
|
|
42
42
|
readonly recentEvent?: string;
|
|
43
43
|
/** Assistant output tail shown while the turn runs (≤8 KB). */
|
|
44
44
|
readonly tail?: string;
|
|
45
45
|
/** Append `(preset 未挂载)` to the state line. */
|
|
46
46
|
readonly presetMissing?: boolean;
|
|
47
|
+
/** 性能与用量 block, already density-adjusted for the user's level. */
|
|
48
|
+
readonly metricLines?: readonly string[];
|
|
49
|
+
/** Step log rows (one tool per row), already limited for the level. */
|
|
50
|
+
readonly stepLines?: readonly string[];
|
|
51
|
+
/** `共 N 次调用,显示最近 M 条` when the log was truncated. */
|
|
52
|
+
readonly stepNote?: string;
|
|
47
53
|
}
|
|
48
54
|
/** Clip by Unicode code point so a label never exceeds the budget. */
|
|
49
55
|
export declare function clipLabel(text: string, maxChars?: number): string;
|
|
50
56
|
/**
|
|
51
|
-
* The single per-turn card
|
|
52
|
-
* one
|
|
53
|
-
*
|
|
54
|
-
*
|
|
57
|
+
* The single per-turn card, laid out in stacked blocks so nothing is crammed
|
|
58
|
+
* onto one `·` chain:
|
|
59
|
+
*
|
|
60
|
+
* 会话标题
|
|
61
|
+
* 状态行(状态 + 已运行)
|
|
62
|
+
* 性能与用量(按档位一行或两行)
|
|
63
|
+
* 附加状态(压缩 / 协作 / 已请求停止)
|
|
64
|
+
* ───
|
|
65
|
+
* 步骤流水(一行一个工具,按档位限量)
|
|
66
|
+
* ───
|
|
67
|
+
* 正文尾部
|
|
68
|
+
* [停止]
|
|
69
|
+
* 脚注
|
|
70
|
+
*
|
|
71
|
+
* It deliberately carries NO assistant body beyond the tail — the final card is
|
|
72
|
+
* the only place a complete answer is rendered.
|
|
55
73
|
*/
|
|
56
74
|
export declare function progressCard(input: ProgressCardInput): FeishuCard;
|
|
75
|
+
/** Turn the live process card into the greyed, collapsed record of the turn. */
|
|
76
|
+
export declare function collapsedProcessCard(card: FeishuCard, note: string): FeishuCard;
|
|
57
77
|
export interface FinalCardInput {
|
|
58
78
|
readonly title: string;
|
|
59
79
|
readonly stateLabel: string;
|
package/dist/render/cards.js
CHANGED
|
@@ -41,18 +41,30 @@ export function clipLabel(text, maxChars = 40) {
|
|
|
41
41
|
return Array.from(text).slice(0, maxChars).join('');
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
* The single per-turn card
|
|
45
|
-
* one
|
|
46
|
-
*
|
|
47
|
-
*
|
|
44
|
+
* The single per-turn card, laid out in stacked blocks so nothing is crammed
|
|
45
|
+
* onto one `·` chain:
|
|
46
|
+
*
|
|
47
|
+
* 会话标题
|
|
48
|
+
* 状态行(状态 + 已运行)
|
|
49
|
+
* 性能与用量(按档位一行或两行)
|
|
50
|
+
* 附加状态(压缩 / 协作 / 已请求停止)
|
|
51
|
+
* ───
|
|
52
|
+
* 步骤流水(一行一个工具,按档位限量)
|
|
53
|
+
* ───
|
|
54
|
+
* 正文尾部
|
|
55
|
+
* [停止]
|
|
56
|
+
* 脚注
|
|
57
|
+
*
|
|
58
|
+
* It deliberately carries NO assistant body beyond the tail — the final card is
|
|
59
|
+
* the only place a complete answer is rendered.
|
|
48
60
|
*/
|
|
49
61
|
export function progressCard(input) {
|
|
50
|
-
const
|
|
62
|
+
const elements = [];
|
|
51
63
|
const sessionTitle = input.sessionTitle?.trim();
|
|
52
64
|
if (sessionTitle !== undefined && sessionTitle.length > 0) {
|
|
53
|
-
|
|
65
|
+
elements.push(div(`会话:${clipLabel(sessionTitle)}`));
|
|
54
66
|
}
|
|
55
|
-
stateLine
|
|
67
|
+
const stateLine = [`**${input.stateLabel}**`];
|
|
56
68
|
if (input.startedAt !== undefined) {
|
|
57
69
|
const now = input.now ?? Date.now();
|
|
58
70
|
const seconds = Math.max(0, Math.floor((now - input.startedAt) / 1000));
|
|
@@ -60,17 +72,34 @@ export function progressCard(input) {
|
|
|
60
72
|
}
|
|
61
73
|
if (input.presetMissing === true)
|
|
62
74
|
stateLine.push('(preset 未挂载)');
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
75
|
+
elements.push(div(stateLine.join(' · ')));
|
|
76
|
+
for (const line of input.metricLines ?? []) {
|
|
77
|
+
if (line.trim().length > 0)
|
|
78
|
+
elements.push(div(line));
|
|
79
|
+
}
|
|
67
80
|
for (const line of input.extraLines ?? []) {
|
|
68
81
|
if (line.trim().length > 0)
|
|
69
82
|
elements.push(div(line));
|
|
70
83
|
}
|
|
84
|
+
const stepRows = (input.stepLines ?? []).filter((line) => line.trim().length > 0);
|
|
85
|
+
const rows = stepRows.length > 0
|
|
86
|
+
? stepRows
|
|
87
|
+
: input.recentEvent === undefined
|
|
88
|
+
? []
|
|
89
|
+
: [`最近:${clipLabel(input.recentEvent)}`];
|
|
90
|
+
if (rows.length > 0) {
|
|
91
|
+
elements.push({ tag: 'hr' });
|
|
92
|
+
for (const row of rows)
|
|
93
|
+
elements.push(div(row));
|
|
94
|
+
const note = input.stepNote?.trim();
|
|
95
|
+
if (note !== undefined && note.length > 0)
|
|
96
|
+
elements.push(div(`_${note}_`));
|
|
97
|
+
}
|
|
71
98
|
const tail = input.tail === undefined ? '' : tailText(input.tail);
|
|
72
|
-
if (tail.trim().length > 0)
|
|
99
|
+
if (tail.trim().length > 0) {
|
|
100
|
+
elements.push({ tag: 'hr' });
|
|
73
101
|
elements.push(div(tail));
|
|
102
|
+
}
|
|
74
103
|
if (input.stopButton === true) {
|
|
75
104
|
elements.push({
|
|
76
105
|
tag: 'action',
|
|
@@ -84,9 +113,28 @@ export function progressCard(input) {
|
|
|
84
113
|
],
|
|
85
114
|
});
|
|
86
115
|
}
|
|
87
|
-
elements.push(div('_
|
|
116
|
+
elements.push(div('_过程消息;结束后会置灰收起,正文另发。_'));
|
|
88
117
|
return buildCard({ title: input.title, template: 'blue', elements });
|
|
89
118
|
}
|
|
119
|
+
/** Turn the live process card into the greyed, collapsed record of the turn. */
|
|
120
|
+
export function collapsedProcessCard(card, note) {
|
|
121
|
+
// Drop the buttons and the running-time footnote: both would lie on a card
|
|
122
|
+
// that has already ended.
|
|
123
|
+
const elements = card.elements.filter((element) => element.tag !== 'action' &&
|
|
124
|
+
!element.text?.content?.startsWith('_过程消息'));
|
|
125
|
+
const text = note.trim();
|
|
126
|
+
if (text.length > 0)
|
|
127
|
+
elements.push(div(`_${text}_`));
|
|
128
|
+
const grey = {
|
|
129
|
+
config: card.config,
|
|
130
|
+
...(card.header === undefined
|
|
131
|
+
? {}
|
|
132
|
+
: { header: { ...card.header, template: 'grey' } }),
|
|
133
|
+
elements,
|
|
134
|
+
};
|
|
135
|
+
assertCardFits(grey);
|
|
136
|
+
return grey;
|
|
137
|
+
}
|
|
90
138
|
/** State-line parts shared by the card markdown and the text fallback. */
|
|
91
139
|
function finalStateLine(input, markdown) {
|
|
92
140
|
const parts = [];
|
package/lib/client.js
CHANGED
|
@@ -85,7 +85,7 @@ function openDrawer(props) {
|
|
|
85
85
|
fontSize: "18px"
|
|
86
86
|
});
|
|
87
87
|
const version = document.createElement("p");
|
|
88
|
-
version.textContent = `${t("panel.version")} 0.12.
|
|
88
|
+
version.textContent = `${t("panel.version")} 0.12.3`;
|
|
89
89
|
const session = document.createElement("p");
|
|
90
90
|
session.textContent = `${t("drawer.session")}: ${props.sessionId ?? t("drawer.none")}`;
|
|
91
91
|
const loaded = document.createElement("p");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-caduceus",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.3",
|
|
4
4
|
"description": "Feishu/Lark surface plugin for DeepSeek Harness (dsh): one topic per session, SOUL persona sync, streaming process cards, /status and /doctor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|