fix: resolve apiProxy via ctx.get for /model (v0.3.2)

Cordis throws on ctx.apiProxy without inject; use ctx.get and declare apiProxy inject so catalog and model APIs work under dsh web.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
owo
2026-08-14 20:20:55 +08:00
co-authored by Cursor
parent f433cce24d
commit 5dbe833205
18 changed files with 209 additions and 35 deletions
+30
View File
@@ -0,0 +1,30 @@
import type { Context } from '@deepseek-ai/cordis';
/** Loose ApiProxy surface used by catalog + model helpers. */
export type ApiProxySessions = {
list?: (req: {
rpcId: string;
payload: unknown;
}) => Promise<unknown>;
models?: (req: {
rpcId: string;
payload: unknown;
}) => Promise<unknown>;
selectModel?: (req: {
rpcId: string;
payload: unknown;
}) => Promise<unknown>;
};
export type ApiProxyLike = {
workspace?: {
list?: (req: {
rpcId: string;
payload: unknown;
}) => Promise<unknown>;
};
sessions?: ApiProxySessions;
};
/**
* Resolve host apiProxy without requiring Cordis inject.
* Direct `ctx.apiProxy` throws "cannot get property without inject" on real fibers.
*/
export declare function resolveApiProxy(ctx: Context): ApiProxyLike | undefined;
+27
View File
@@ -0,0 +1,27 @@
/**
* Resolve host apiProxy without requiring Cordis inject.
* Direct `ctx.apiProxy` throws "cannot get property without inject" on real fibers.
*/
export function resolveApiProxy(ctx) {
const c = ctx;
if (typeof c.get === 'function') {
try {
const viaGet = c.get('apiProxy');
if (viaGet)
return viaGet;
}
catch {
// ignore and try own-property fallback (tests / plain mocks)
}
}
try {
// Own property on plain mocks only — Cordis proxy throws without inject.
if (Object.prototype.hasOwnProperty.call(c, 'apiProxy') && c.apiProxy) {
return c.apiProxy;
}
}
catch {
return undefined;
}
return undefined;
}
+2 -1
View File
@@ -363,7 +363,8 @@ export class TelegramBridge {
}
catch (err) {
this.ctx.logger.warn(`dsh-telegram-channel: /model failed: ${this.redact(err)}`);
await this.client.sendMessage(chatId, MSG.MODEL_UNAVAILABLE);
const detail = err instanceof Error ? err.message : String(err);
await this.client.sendMessage(chatId, MSG.MODEL_UNAVAILABLE(detail));
}
}
async sendEffortPicker(chatId, option) {
+2 -1
View File
@@ -1,4 +1,5 @@
import { randomUUID } from 'node:crypto';
import { resolveApiProxy } from './apiproxy.js';
import { describeAgent, workspaceName } from './label.js';
function rpcCall(fn, payload) {
if (typeof fn !== 'function')
@@ -27,7 +28,7 @@ function titleOf(summary) {
}
/** Load workspaces + sessions aligned with Web UI (via apiProxy when available). */
export async function loadCatalog(ctx) {
const api = ctx.apiProxy;
const api = resolveApiProxy(ctx);
const wsPromise = rpcCall(api?.workspace?.list, {});
const sessPromise = rpcCall(api?.sessions?.list, {});
if (!wsPromise || !sessPromise)
+1 -1
View File
@@ -15,7 +15,7 @@ export declare const MSG: {
readonly STATUS_BOUND: (label: string) => string;
readonly STATUS_BOUND_COLD: (label: string) => string;
readonly GONE: "绑定的会话已不可用。请重新 /sessions。";
readonly MODEL_UNAVAILABLE: "无法读取模型列表(需要 dsh web / apiProxy)。请确认本机 Harness 已加载 host-apiproxy。";
readonly MODEL_UNAVAILABLE: (detail?: string) => string;
readonly MODEL_UNROUTABLE: (current: string) => string;
readonly MODEL_EMPTY: (current: string) => string;
readonly MODEL_SET: (selected: string) => string;
+6 -1
View File
@@ -36,7 +36,12 @@ export const MSG = {
return `当前绑定:${label}\n(会话当前未在内存中运行;发消息时会自动 resume。)`;
},
GONE: '绑定的会话已不可用。请重新 /sessions。',
MODEL_UNAVAILABLE: '无法读取模型列表(需要 dsh web / apiProxy)。请确认本机 Harness 已加载 host-apiproxy。',
MODEL_UNAVAILABLE(detail) {
const tip = '无法读取模型列表。请确认已绑定会话,且本机 dsh web 已加载 host-apiproxy。';
if (!detail)
return tip;
return `${tip}\n详情:${detail}`;
},
MODEL_UNROUTABLE(current) {
return `当前模型不可路由:${current}\n请在 Web 或本机配置可用 provider 后再试 /model。`;
},
+1
View File
@@ -1,6 +1,7 @@
import type { Context } from '@deepseek-ai/cordis';
import Schema from '@deepseek-ai/schemastery';
export declare const name = "dsh-telegram-channel";
/** agents: followup; apiProxy: workspace/session catalog + /model (Cordis requires inject to read ctx.apiProxy). */
export declare const inject: string[];
export interface TelegramChannelConfig {
token?: string;
+2 -1
View File
@@ -1,7 +1,8 @@
import Schema from '@deepseek-ai/schemastery';
import { TelegramBridge } from './bridge.js';
export const name = 'dsh-telegram-channel';
export const inject = ['agents'];
/** agents: followup; apiProxy: workspace/session catalog + /model (Cordis requires inject to read ctx.apiProxy). */
export const inject = ['agents', 'apiProxy'];
export const Config = Schema.object({
token: Schema.string().default(''),
allowedUserIds: Schema.array(Schema.number()).default([]),
+27 -8
View File
@@ -1,21 +1,36 @@
import { randomUUID } from 'node:crypto';
import { resolveApiProxy } from './apiproxy.js';
function unwrap(res) {
const r = res;
if (r?.result?.ok === true)
return r.result.value;
return undefined;
}
function unwrapError(res) {
const r = res;
if (r?.result && r.result.ok === false) {
const err = r.result.error;
if (!err)
return 'rpc failed';
return err.code ? `${err.code}: ${err.message ?? ''}`.trim() : (err.message ?? 'rpc failed');
}
return undefined;
}
async function call(fn, payload) {
if (typeof fn !== 'function')
throw new Error('apiProxy sessions API unavailable');
return fn({ rpcId: randomUUID(), payload });
}
export async function loadSessionModels(ctx, sessionId) {
const api = ctx.apiProxy;
const raw = await call(api?.sessions?.models, { sessionId });
const api = resolveApiProxy(ctx);
if (!api?.sessions?.models) {
throw new Error('apiProxy unavailable (use ctx.get / inject apiProxy)');
}
const raw = await call(api.sessions.models, { sessionId });
const value = unwrap(raw);
if (!value)
throw new Error('failed to load session models');
if (!value) {
throw new Error(unwrapError(raw) ?? 'failed to load session models');
}
const options = [];
for (const group of value.groups ?? []) {
for (const m of group.models ?? []) {
@@ -34,16 +49,20 @@ export async function loadSessionModels(ctx, sessionId) {
};
}
export async function selectSessionModel(ctx, sessionId, selection) {
const api = ctx.apiProxy;
const raw = await call(api?.sessions?.selectModel, {
const api = resolveApiProxy(ctx);
if (!api?.sessions?.selectModel) {
throw new Error('apiProxy unavailable (use ctx.get / inject apiProxy)');
}
const raw = await call(api.sessions.selectModel, {
sessionId,
provider: selection.provider,
model: selection.model,
reasoningEffort: selection.reasoningEffort,
});
const value = unwrap(raw);
if (!value?.selected)
throw new Error('failed to select model');
if (!value?.selected) {
throw new Error(unwrapError(raw) ?? 'failed to select model');
}
return value.selected;
}
export function formatModel(sel) {