dsh-opendesign-corouter 0.0.0-stage → 0.1.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KouzakiUmi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,124 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # OpenDesign CoRouter for DSH
4
+
5
+ **模型接进来,额度看清楚。**
6
+
7
+ 在 DeepSeek Harness 中使用 CoRouter 模型,分开查看套餐赠送窗口与钱包余额;桌面登录态优先,也能独立登录。
8
+
9
+ [![CI](https://github.com/KouzakiUmi/dsh-opendesign-corouter/actions/workflows/ci.yml/badge.svg)](https://github.com/KouzakiUmi/dsh-opendesign-corouter/actions/workflows/ci.yml)
10
+ [![DSH Core](https://img.shields.io/badge/DSH_Core-0.2.1--rc.3-64748b)](#兼容性)
11
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D24.9-64748b)](#开发与构建)
12
+
13
+ [快速开始](#快速开始) · [功能预览](#功能预览) · [连接与凭据](docs/usage.md) · [常见问题](docs/troubleshooting.md) · [更新记录](docs/changelog.md)
14
+
15
+ <picture>
16
+ <source media="(prefers-color-scheme: dark)" srcset="docs/previews/overview-dark.png">
17
+ <img src="docs/previews/overview-light.png" width="1040" alt="CoRouter 四层额度概览:套餐赠送窗口、套餐余额、充值赠送金额与总额度;模拟数据">
18
+ </picture>
19
+
20
+ <sub>真实组件 · 模拟数据 · 离线预览,不是运行中的 DSH 或真实账号截图</sub>
21
+
22
+ </div>
23
+
24
+ ## 快速开始
25
+
26
+ ### 1. 通过 DSH 安装
27
+
28
+ ```console
29
+ dsh plugin --profile web add dsh-opendesign-corouter
30
+ ```
31
+
32
+ 也可打开 [Actions → CI](https://github.com/KouzakiUmi/dsh-opendesign-corouter/actions/workflows/ci.yml),从成功构建下载 **`dsh-opendesign-corouter-build`** artifact。登录 GitHub 后解压 ZIP,取得 `.tgz` 与 `.sha256`,再安装本地包:
33
+
34
+ ```console
35
+ dsh plugin --profile web add /absolute/path/to/dsh-opendesign-corouter-0.1.2.tgz
36
+ ```
37
+
38
+ `web` 只是示例,请替换为目标 Profile。Desktop 使用桌面安装器提供的 `dsh` 入口;独立公共 CLI 不能代管保留的 `desktop` Profile。安装后的启用与重载按目标 DSH 的管理流程进行。
39
+
40
+ ### 2. 配置连接
41
+
42
+ 1. 打开 **设置 → OpenDesign CoRouter → 连接与登录**。
43
+ 2. 为推理配置 CoRouter 工作空间 **API Key**,或沿用启动环境中的 `COROUTER_API_KEY`。
44
+ 3. 额度默认读取桌面登录态;没有桌面端时,点击 **登录并保存会话**,在官方页面自行完成登录。
45
+ 4. 回到模型选择器,选择 **OpenDesign CoRouter · 套餐赠送** 或 **OpenDesign CoRouter · 付费模型**。
46
+
47
+ **API Key 用于推理,桌面 / 控制台会话用于查额度。两者不能互相代替。** [完整连接说明](docs/usage.md)
48
+
49
+ ## 功能预览
50
+
51
+ ### 两个 Provider,逐模型配置
52
+
53
+ | Provider | 模型数 | 用途 |
54
+ | --- | ---: | --- |
55
+ | `opendesign-corouter-plan` | 12 | 官网 Design Plan 套餐赠送模型 |
56
+ | `opendesign-corouter-paid` | 21 | 其余公开对话模型 |
57
+
58
+ 上表为内置目录。模型页可从 OpenAI `/models` 端口刷新,逐个启用或停用模型,并配置温度、top-p、默认输出上限、上下文预算及思考深度。停用同时影响模型选择器和直接调用;参数会保存并应用到实际请求。媒体生成接口不在对话目录中,未知能力不按名称猜测。
59
+
60
+ 推理协议可选 **OpenAI Chat Completions、OpenAI Responses、Anthropic Messages**。模型发现始终使用 OpenAI 端口,不随推理协议切换。
61
+
62
+ > **分组不等于计费隔离。** 赠送额度耗尽后是否转扣套餐余额或钱包,由服务端和账号权益决定。选择“套餐赠送”不能保证绝不消耗钱包余额。
63
+
64
+ ### 四层额度,不重复相加
65
+
66
+ - **套餐赠送窗口**:各自显示已用、剩余及重置时间。
67
+ - **套餐余额**:订阅池的可用金额。
68
+ - **充值 / 赠送金额**:钱包中对应池的可用金额。
69
+ - **总额度**:接口报告的总余额,不再与分池数字重复相加。
70
+
71
+ 接口缺失或权限不足时显示未知 / 部分可用,不补成 0。窗口重置、订阅账期结束和下次自动刷新分别显示。桌面通道只有部分额度且已有完整独立会话时,会整体切换到独立会话并标明来源,不拼接账号数据。额度身份与推理 Key 的账号 / 工作区仍需保持一致。
72
+
73
+ ### 桌面优先,也能独立登录
74
+
75
+ 不复制或修改桌面配置。独立登录使用临时 Chrome 窗口,用户亲自完成官方登录与人机验证;验证通过才保存到 DSH 凭据服务。支持取消、超时和卸载清理,不触碰日常浏览器。
76
+
77
+ <picture>
78
+ <source media="(prefers-color-scheme: dark)" srcset="docs/previews/theme-dark.png">
79
+ <img src="docs/previews/theme-light.png" width="1040" alt="连接与登录页,展示桌面优先开关、独立登录按钮和不回填的 API Key 输入框;模拟数据">
80
+ </picture>
81
+
82
+ ### 空轮监控,只作用于赠送模型
83
+
84
+ 独立开关,默认开启。参考 [dsh-empty-turn-guard](https://github.com/FYTXY/dsh-empty-turn-guard),将“正常 stop、没有工具调用、答案为空或只剩协议残渣”的工具对话标成 `EMPTY_RESPONSE`。
85
+
86
+ 监控不注入提示、不修改正文、不重放工具、不自动重启;实际重试交给宿主策略,**可能额外消耗额度**。付费 Provider 和无工具后台请求不启用这项附加监控。
87
+
88
+ ## 使用前先知道
89
+
90
+ - **数字不是账单**:部分读取接口属于桌面 / 控制台私有协议,可能随上游变化。
91
+ - **凭据不是加密承诺**:插件使用 DSH 凭据服务,不把 Key / Cookie 放进普通设置或回填网页;落盘保护仍取决于宿主凭据实现和本机权限。
92
+ - **验证有范围**:已做类型检查、隔离 Loader 装载 / 卸载 / 重载、离线界面与主题检查;尚未完成本插件的真实账号登录、推理及生产 Profile 联调。
93
+
94
+ ## 兼容性
95
+
96
+ | 项目 | 要求 / 证据 |
97
+ | --- | --- |
98
+ | 目标 DSH Core | `0.2.1-rc.3`,以官方同版本发布包验证 |
99
+ | Core 声明范围 | `>=0.2.1-rc.3 <0.3.0-0`,范围内其他版本不代表已实测 |
100
+ | Node.js | `>=24.9.0` |
101
+ | 独立登录 | 本机 Google Chrome;插件不会自动下载 |
102
+ | 默认推理接口 | `https://amr-link.open-design.ai/v1/chat/completions` |
103
+
104
+ CI 在 Windows 与 Ubuntu 执行构建、类型检查及隔离测试;Ubuntu 另执行四页签浏览器和亮暗主题检查。通过 CI 不等于已安装或已通过真实账号验收。[开发与验证范围](docs/development.md)
105
+
106
+ ## 开发与构建
107
+
108
+ ```console
109
+ git clone https://github.com/KouzakiUmi/dsh-opendesign-corouter.git
110
+ cd dsh-opendesign-corouter
111
+ npm ci --ignore-scripts
112
+ npm run check
113
+ npm run test:ui
114
+ ```
115
+
116
+ 本地 UI 测试需要 Chrome,页面使用模拟数据并阻断联网。源码 checkout 不包含预构建 `lib`;需要安装本地目录时,先运行构建。[开发与 CI](docs/development.md)
117
+
118
+ ## 文档与反馈
119
+
120
+ [使用指南](docs/usage.md) · [排障](docs/troubleshooting.md) · [更新记录](docs/changelog.md) · [安全边界](SECURITY.md) · [第三方声明](THIRD_PARTY_NOTICES.md)
121
+
122
+ 通过 [Issues](https://github.com/KouzakiUmi/dsh-opendesign-corouter/issues) 反馈时,请附插件版本、DSH **Core** 版本和已脱敏的错误说明。不要贴 API Key、Cookie、凭据文件或含秘密的 URL。
123
+
124
+ 本项目采用 [MIT 许可](LICENSE),上游版权声明见 [第三方声明](THIRD_PARTY_NOTICES.md)。版本标签通过 GitHub OIDC trusted publishing 发布到 npm,流程见 [发布说明](docs/publishing.md)。本项目与 OpenDesign、DeepSeek 无官方隶属关系。
package/SECURITY.md ADDED
@@ -0,0 +1,14 @@
1
+ # 安全边界
2
+
3
+ - 推理、桌面控制面和控制台 Cookie 分属独立通道,不互相替代鉴权材料。
4
+ - API Key 与独立登录 Cookie 仅经 DSH 凭据服务写入插件专属记录;设置页只读取存在性和状态,不回填秘密。
5
+ - 桌面配置只读。仅支持已核实的生产环境,控制令牌只能发送到 `https://amr-api.open-design.ai`。控制台和推理请求禁止自动跟随重定向。
6
+ - 推理自定义地址由用户显式配置;除本机回环外要求 HTTPS,拒绝 URL 内嵌凭据、查询串与片段。
7
+ - 额度分池不跨身份混合。桌面认证失效时,可回退到单独保存的控制台登录;权限不足和未知额度不伪装成 0。
8
+ - 独立登录使用临时浏览器上下文。用户自行完成登录和验证;取消、超时和卸载关闭插件拥有的窗口。持久化成功是不可撤回的提交边界,之后取消不会假称已撤回。
9
+ - `OPENDESIGN_VELA_PATH` 是用户信任的本地程序路径。插件仅以固定 argv 执行只读查询,不通过 shell;不校验该程序的签名或下载它。不要将此变量指向来源不明的程序。
10
+ - RPC 使用严格白名单解析;原始错误正文、Cookie、请求头、Key 和桌面本地路径不进入状态返回。并发保存有版本冲突检测。
11
+ - 请求、流空闲、额度接口、CLI、登录和 RPC 均有有界超时。停用插件会撤销 Provider / RPC 并中止自身请求。
12
+ - 空轮守卫不发起自己的重试、不注入消息、不执行工具。宿主重试可能额外计费;该开关不能阻止上游自身的余额回退规则。
13
+
14
+ 依赖的控制台及桌面私有协议可能变化。遇到未识别响应时显示失败或部分未知,不推测登录成功、套餐不存在或额度已清零。
@@ -0,0 +1,39 @@
1
+ # 第三方代码与资料
2
+
3
+ 本项目的消息/图片兼容处理、套餐模型元数据参考 [dsh-opendesign-go 0.1.9](https://github.com/EliteOtaku/dsh-opendesign-go)。额度归一化和临时浏览器登录流程改编自 [dsh-subusage 0.16.2](https://github.com/KouzakiUmi/dsh-subusage)。空轮判定参考 [dsh-empty-turn-guard 0.4.0](https://github.com/FYTXY/dsh-empty-turn-guard)。设置布局参考 [dsh-code-pill](https://github.com/KouzakiUmi/dsh-code-pill)。
4
+
5
+ 离线主题夹具使用 [DeepSeek Harness 官方主题令牌](https://github.com/deepseek-ai/deepseek-harness/blob/22ddee3792402c3e70ec5f86a63c3e3e895a2f60/packages/client/ui-theme/src/styles/design-platform.css),仅供测试,不随插件安装包分发。
6
+
7
+ Client bundle 内含 [Zod 4.6.6](https://github.com/colinhacks/zod) 与 DSH 协议辅助代码,分别保留 Colin McDonnell 与 DeepSeek 的 MIT 声明。
8
+
9
+ 以上项目为 MIT 许可。保留如下版权和许可声明,适用于相应移植与改编部分:
10
+
11
+ ```text
12
+ MIT License
13
+
14
+ Copyright (c) 2026 EliteOtaku
15
+ Copyright (c) 2026 KouzakiUmi
16
+ Copyright (c) 2026 FYTXY
17
+ Copyright (c) 2026 DeepSeek
18
+ Copyright (c) 2025 Colin McDonnell
19
+
20
+ Permission is hereby granted, free of charge, to any person obtaining a copy
21
+ of this software and associated documentation files (the "Software"), to deal
22
+ in the Software without restriction, including without limitation the rights
23
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
24
+ copies of the Software, and to permit persons to whom the Software is
25
+ furnished to do so, subject to the following conditions:
26
+
27
+ The above copyright notice and this permission notice shall be included in all
28
+ copies or substantial portions of the Software.
29
+
30
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
31
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
32
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
33
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
34
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
35
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
36
+ SOFTWARE.
37
+ ```
38
+
39
+ 模型 ID / 分组来自 [CoRouter 官方目录](https://corouter.open-design.ai/zh/models/) 与 [官方套餐页](https://corouter.open-design.ai/zh/),观察日期 2026-10-26。仅使用接入所需事实,不分发 OpenDesign 客户端代码、资源或账号数据。
@@ -0,0 +1,4 @@
1
+ - insert:
2
+ - id: llm-opendesign-corouter
3
+ name: dsh-opendesign-corouter
4
+ config: {}
@@ -0,0 +1,27 @@
1
+ import { LlmAdapter, type GenerateOptions, type LlmModelInfo, type LlmResolvedModelInfo, type PreparedAdapterCall, type StreamChunk } from '@deepseek-ai/dsh-llm';
2
+ import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
3
+ import { type CatalogModel } from './catalog.js';
4
+ import { type RuntimeConfig } from './config.js';
5
+ export interface AdapterOptions {
6
+ config(): RuntimeConfig;
7
+ models?(): readonly CatalogModel[];
8
+ apiKey(ref: string): Promise<string | undefined>;
9
+ imageDataUrl?(image: ImageAttachmentRef, signal: AbortSignal): Promise<string | undefined>;
10
+ onEmptyTurn?(): void;
11
+ fetch?: typeof fetch;
12
+ }
13
+ export declare class CoRouterAdapter extends LlmAdapter {
14
+ #private;
15
+ readonly options: AdapterOptions;
16
+ constructor(options: AdapterOptions);
17
+ dispose(): void;
18
+ providerInfo(provider: string): {
19
+ id: string;
20
+ name: string;
21
+ };
22
+ private modelOf;
23
+ listModels(provider: string): Promise<readonly LlmModelInfo[]>;
24
+ resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
25
+ prepareCall(provider: string, model: string, signal?: AbortSignal): Promise<PreparedAdapterCall>;
26
+ stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
27
+ }
package/lib/auth.d.ts ADDED
@@ -0,0 +1,132 @@
1
+ import type { Session, SessionSource } from './quota.js';
2
+ /** Control-plane origin the desktop build ships with (open-design-config.json / daemon default). */
3
+ export declare const DEFAULT_API_URL = "https://amr-api.open-design.ai";
4
+ /** The one host this plugin is willing to send an account token to. */
5
+ export declare const API_HOST = "amr-api.open-design.ai";
6
+ /** Profiles the official client recognizes; anything else falls back to `prod`. */
7
+ export declare const DESKTOP_PROFILES: readonly ["prod", "test", "feature-test", "local"];
8
+ export type DesktopProfile = (typeof DESKTOP_PROFILES)[number];
9
+ export declare const DEFAULT_PROFILE: DesktopProfile;
10
+ /** Reference names holding a console session cookie, read through the credential seam. */
11
+ export declare const COOKIE_ENV_REFS: readonly ["OPEN_DESIGN_COOKIE", "OPEN_DESIGN_SESSION"];
12
+ /**
13
+ * Exactly one accepted origin. `apiUrl` comes from a file this plugin does not own, so an
14
+ * edited or attacker-supplied value must never receive the account token: without this gate,
15
+ * pointing `apiUrl` anywhere would exfiltrate the desktop `controlKey`. A sibling subdomain is
16
+ * not accepted either — no widening "just in case" (`*.open-design.ai` would already be wrong).
17
+ */
18
+ export declare function isOfficialApiUrl(raw: string): boolean;
19
+ /** Canonical `https://host` form, or `undefined` when the value is not an official origin. */
20
+ export declare function normalizeApiUrl(raw: string | undefined): string | undefined;
21
+ /** Profile selection matches the client: `OPEN_DESIGN_AMR_PROFILE` wins over `VELA_PROFILE`. */
22
+ export declare function resolveProfile(env?: NodeJS.ProcessEnv): DesktopProfile;
23
+ /** `AMR_HOME` (with `~` / `~/x` expansion), else `<home>/.amr` — the client's `configDir()`. */
24
+ export declare function desktopConfigDir(env?: NodeJS.ProcessEnv, home?: string): string;
25
+ /** Full path of the desktop credential file. */
26
+ export declare function desktopConfigPath(env?: NodeJS.ProcessEnv, home?: string): string;
27
+ /** What the desktop file says about one profile. Values are held in memory only. */
28
+ export interface DesktopCredential {
29
+ profile: DesktopProfile;
30
+ controlKey: string;
31
+ /** Present only when the profile carries an official origin. */
32
+ apiUrl?: string;
33
+ plan?: string;
34
+ /** `runtimeKey` present means the desktop considers itself signed in. */
35
+ signedIn: boolean;
36
+ }
37
+ export type DesktopRead = {
38
+ state: 'missing';
39
+ } | {
40
+ state: 'unreadable';
41
+ } | {
42
+ state: 'invalid';
43
+ } | {
44
+ state: 'incomplete';
45
+ profile: DesktopProfile;
46
+ }
47
+ /** The profile's `apiUrl` is not the one accepted origin: never used, and never retried elsewhere. */
48
+ | {
49
+ state: 'unsafe-api-url';
50
+ profile: DesktopProfile;
51
+ }
52
+ /** A non-`prod` profile: its token belongs to another environment, so it is not sent to prod. */
53
+ | {
54
+ state: 'unsupported-profile';
55
+ profile: DesktopProfile;
56
+ } | {
57
+ state: 'credential';
58
+ credential: DesktopCredential;
59
+ };
60
+ /**
61
+ * Parse `config.json` text. Never throws and never returns the raw text, so callers can
62
+ * treat a hostile file as "absent" without risking a leak. `state: 'invalid'` means the
63
+ * JSON or its shape was not what the client writes.
64
+ */
65
+ export declare function parseDesktopConfigText(text: string, profile?: DesktopProfile): DesktopRead;
66
+ export interface DesktopDeps {
67
+ env?: NodeJS.ProcessEnv;
68
+ home?: string;
69
+ profile?: DesktopProfile;
70
+ readFile?: (path: string) => Promise<string>;
71
+ }
72
+ /**
73
+ * Read the desktop credential, distinguishing "no desktop installed or signed out"
74
+ * (`missing` / `incomplete`) from "there is a file but it cannot be used"
75
+ * (`unreadable` / `invalid`). The two must not be conflated: the first is normal for
76
+ * users without the desktop app, the second deserves a different hint.
77
+ */
78
+ export declare function readDesktopCredential(deps?: DesktopDeps): Promise<DesktopRead>;
79
+ /** Cookie payload persisted through the credential seam as a `grant` record. */
80
+ export interface LoginGrant {
81
+ cookie: string;
82
+ /** Epoch ms; `0`/absent means the console declared no expiry. */
83
+ expiresAt?: number;
84
+ }
85
+ export type AuthSource = 'desktop' | 'login' | 'environment';
86
+ export interface AuthSession extends Session {
87
+ source: SessionSource;
88
+ via: AuthSource;
89
+ apiUrl: string;
90
+ /** Desktop profile name, when the session came from the desktop file. */
91
+ profile?: string;
92
+ plan?: string;
93
+ }
94
+ export type AuthReason = 'no-credential' | 'desktop-incomplete' | 'desktop-unreadable' | 'unsafe-api-url' | 'unsupported-profile';
95
+ export type AuthOutcome = {
96
+ state: 'ready';
97
+ session: AuthSession;
98
+ warnings: AuthReason[];
99
+ } | {
100
+ state: 'need-login';
101
+ reason: AuthReason;
102
+ warnings: AuthReason[];
103
+ };
104
+ export interface AuthDeps extends DesktopDeps {
105
+ /** Allow reading the desktop login state; `false` for users who only use this plugin's login. */
106
+ useDesktop?: boolean;
107
+ /** Read the persisted login grant (Host-owned credential store). */
108
+ readGrant?: () => Promise<LoginGrant | undefined>;
109
+ /** Resolve an environment-variable reference (Host-owned credential seam). */
110
+ resolveRef?: (ref: string) => Promise<string | undefined>;
111
+ now?: () => number;
112
+ }
113
+ /** Desktop `controlKey` as a Bearer header (daemon evidence: `/api/v1/wallet/balance`). */
114
+ export declare function desktopSession(credential: DesktopCredential, apiUrl?: string): AuthSession;
115
+ /** A persisted or environment-supplied console cookie as a `cookie` header. */
116
+ export declare function cookieSession(cookie: string, source: SessionSource, apiUrl?: string): AuthSession | undefined;
117
+ /**
118
+ * Resolve the session in the order the user asked for: **desktop first**, then the
119
+ * cookie saved by this plugin's own login, then environment references.
120
+ *
121
+ * A desktop config whose `apiUrl` is not an official origin is *skipped*, not used: the
122
+ * token must not travel to a host the plugin cannot vouch for. The skip is reported as a
123
+ * warning so a UI can explain why the desktop session was ignored.
124
+ */
125
+ export declare function resolveSession(deps?: AuthDeps): Promise<AuthOutcome>;
126
+ /** Endpoint builders for a resolved origin, mirroring `quota.ts` for non-default hosts. */
127
+ export declare function endpoints(apiUrl: string): {
128
+ base: string;
129
+ current: string;
130
+ summary: string;
131
+ usage: (workspaceId: string) => string;
132
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * CoRouter 模型目录(仅对话模型,供 llm 适配器的模型选择器使用)。
3
+ *
4
+ * 取证来源:
5
+ * - 模型 ID / 显示名 / 供应商 / 单价:`https://corouter.open-design.ai/zh/models/<slug>/`
6
+ * 详情页内联 `data-model="{...}"` JSON(公开页面,2026-10-26)。
7
+ * - group:官网首页套餐卡 `https://corouter.open-design.ai/zh/` 的
8
+ * `data-model-module="design-plan"` 12 款归 `plan`;其余在售对话模型归 `paid`。
9
+ * - `plan` 12 款的 contextWindow / maxOutputTokens / inputModalities / reasoningEfforts
10
+ * 来自 Go 侧 `dsh-opendesign-go` 0.1.9 的 MODEL_DEFAULTS(同一 `amr-link` endpoint 部署资料),
11
+ * 由父会话移植,**未经官网独立复核**;只用于 plan 通道。
12
+ * - 客户端桌面安装(Open Design 0.26.1)不内联 CoRouter 目录;其
13
+ * `vela model preset --format json` 只有 11 个旧预设 id(见 docs/client-evidence.md),
14
+ * 与本表不同代,故不用于本目录。
15
+ *
16
+ * 分组语义(重要):`plan`/`paid` 是**本插件本地目录分类**,用于把“套餐赠送通道”与
17
+ * “其余付费通道”拆成两个 Provider,不是上游强制计费契约。最终可用性、扣哪个额度池、
18
+ * 是否先扣 Design Plan 额度再用套餐模型/钱包额度,全部由服务端按账号套餐决定。
19
+ * `paid` 21 款里只有 8 款出现在官网套餐可抵扣名单,其余 13 款同样可经钱包/按量付费调用,
20
+ * 因此 `paid` 收录 21 款而不是 13 款。
21
+ *
22
+ * 未收录(证据不足,不猜测):
23
+ * - 套餐模型组中尚未公开精确 ID 的 `Muse Spark 1.3`、`Gemini 4 Argon`:
24
+ * 前者详情页没有 `data-model`/模型 ID,后者在模型目录里没有详情页。
25
+ * - 媒体模型 `GPT Image 2`、`Nano Banana 2`、`Seedream 5 Lite / Pro`:
26
+ * 走 `/api/v1/media/*`,不应出现在对话模型列表里,避免选中后 chat 请求必败。
27
+ *
28
+ * `paid` 21 款的能力字段一律缺省:既没有官网数值,也没有可信默认表,
29
+ * 不按名称推断图像能力,也不给未知项填 `['text']` 谎称不支持图像。
30
+ */
31
+ export interface CatalogModel {
32
+ id: string;
33
+ name: string;
34
+ group: 'plan' | 'paid';
35
+ contextWindow?: number;
36
+ maxOutputTokens?: number;
37
+ inputModalities?: readonly ('text' | 'image')[];
38
+ reasoningEfforts?: readonly string[];
39
+ }
40
+ /**
41
+ * 顺序按供应商分组(与官网模型目录导航一致);`plan` 为 Design Plan 额度覆盖的 12 款,
42
+ * 其余 21 款公开对话模型归 `paid`。
43
+ */
44
+ export declare const MODELS: readonly CatalogModel[];
45
+ export declare function modelsFor(group: 'plan' | 'paid'): readonly CatalogModel[];
46
+ export declare function findModel(id: string): CatalogModel | undefined;
@@ -0,0 +1,192 @@
1
+ import * as React from 'react';
2
+ import openDesignCoRouterRemote, { type CoRouterRemote } from './generated/typert.remote-client.js';
3
+ import type { TypertClientRemote } from '@deepseek-ai/dsh-typert-protocol';
4
+ import { type Status, type StatusModel } from './remote.js';
5
+ import type { QuotaSnapshot, QuotaWindow } from './quota.js';
6
+ import { type ApiProtocol } from './protocol-options.js';
7
+ import type { ModelSettings, ModelSettingsMap } from './model-settings.js';
8
+ declare const TABS: readonly [{
9
+ readonly id: "overview";
10
+ readonly label: "概览";
11
+ }, {
12
+ readonly id: "models";
13
+ readonly label: "模型";
14
+ }, {
15
+ readonly id: "connection";
16
+ readonly label: "连接与登录";
17
+ }, {
18
+ readonly id: "advanced";
19
+ readonly label: "高级";
20
+ }];
21
+ type TabId = typeof TABS[number]['id'];
22
+ export type RpcCaller = <T>(method: keyof CoRouterRemote, request?: unknown) => Promise<T>;
23
+ export interface FormValues {
24
+ emptyTurnGuard?: boolean;
25
+ useDesktopCredential?: boolean;
26
+ baseURL?: string;
27
+ apiKeyEnv?: string;
28
+ apiProtocol?: ApiProtocol;
29
+ modelSettings?: ModelSettingsMap;
30
+ quotaRefreshSeconds?: number;
31
+ requestTimeoutMs?: number;
32
+ streamIdleTimeoutMs?: number;
33
+ }
34
+ export interface FormSnapshot {
35
+ status: 'loading' | 'ready' | 'unavailable';
36
+ value: FormValues | undefined;
37
+ writable: boolean;
38
+ }
39
+ export interface ConfigForm {
40
+ getSnapshot(): FormSnapshot;
41
+ subscribe(listener: () => void): () => void;
42
+ set(field: string, value: unknown): Promise<boolean>;
43
+ }
44
+ export interface ConfigForms {
45
+ get(namespace: string): ConfigForm;
46
+ }
47
+ export interface SlotDescriptor<P> {
48
+ name: 'settings.section';
49
+ id: string;
50
+ order: number;
51
+ label: () => string;
52
+ inject: () => P;
53
+ }
54
+ export interface Slots {
55
+ inject(name: 'settings.section', register: () => void | (() => void)): void | (() => void);
56
+ register<P>(descriptor: SlotDescriptor<P>, component: React.ComponentType<P>): void | (() => void);
57
+ }
58
+ export interface ClientContext {
59
+ remote: TypertClientRemote & {
60
+ $mount(contribution: typeof openDesignCoRouterRemote): Promise<() => void>;
61
+ };
62
+ effect(effect: () => void | (() => void) | Promise<void | (() => void)>, label?: string): void;
63
+ inject(services: ['configForms'], apply: (ctx: ConfigScope) => void): void;
64
+ inject(services: ['remote.openDesignCoRouter'], apply: (ctx: RemoteScope) => void): void;
65
+ }
66
+ export interface RemoteScope {
67
+ remote: TypertClientRemote;
68
+ effect(effect: () => void | (() => void), label?: string): void;
69
+ }
70
+ export interface ConfigScope {
71
+ configForms: ConfigForms;
72
+ slots: Slots;
73
+ effect(effect: () => void | (() => void), label?: string): void;
74
+ }
75
+ export interface SettingsProps {
76
+ caller: RpcCaller;
77
+ form: ConfigForm;
78
+ }
79
+ interface Notice {
80
+ kind: 'status' | 'error';
81
+ text: string;
82
+ }
83
+ export declare function formatUsd(value: number | undefined): string;
84
+ export declare function formatTime(value: string | undefined): string;
85
+ /** Token capacities use the same zh-CN grouping as amounts so large windows stay readable. */
86
+ export declare function formatTokens(value: number | undefined): string;
87
+ export interface QuotaViewProps {
88
+ quota: QuotaSnapshot | undefined;
89
+ nextAutoRefreshAt?: string | undefined;
90
+ refreshIntervalSeconds?: number | undefined;
91
+ }
92
+ /** Earliest still-future window reset; quota recovery comes only from `windows[].resetsAt`. */
93
+ export declare function nextWindowResetAt(windows: readonly QuotaWindow[], now: number): string | undefined;
94
+ export declare function QuotaView({ quota, nextAutoRefreshAt, refreshIntervalSeconds }: QuotaViewProps): React.DetailedReactHTMLElement<{
95
+ className: string;
96
+ role: "status";
97
+ }, HTMLElement> | React.FunctionComponentElement<{
98
+ children?: React.ReactNode | undefined;
99
+ }>;
100
+ /** Whole-map model settings writer: absent entry means "inherit every default". */
101
+ export declare function writeModelSettings(map: ModelSettingsMap | undefined, id: string, next: ModelSettings | undefined): ModelSettingsMap;
102
+ /** One model's explicit settings; unknown ids read as "no overrides". */
103
+ export declare function modelSettingsEntry(map: ModelSettingsMap | undefined, id: string): ModelSettings;
104
+ /**
105
+ * Explicit settings always win. When the settings map is authoritative (a ready form snapshot or
106
+ * the Host's mirror), an absent entry means "enabled"; only a non-authoritative view falls back to
107
+ * `Status.models[].enabled`, which can lag one refresh behind a reset.
108
+ */
109
+ export declare function modelEnabled(model: StatusModel, settings: ModelSettings | undefined, authoritative?: boolean): boolean;
110
+ export interface NumberInputSpec {
111
+ label: string;
112
+ min: number;
113
+ max: number;
114
+ integer?: boolean;
115
+ allowEmpty?: boolean;
116
+ }
117
+ export type NumberInputResult = {
118
+ ok: true;
119
+ value: number | undefined;
120
+ } | {
121
+ ok: false;
122
+ message: string;
123
+ };
124
+ /** Parse a label-bound numeric setting; an empty string means "inherit" when allowed. */
125
+ export declare function parseNumberInput(raw: string, spec: NumberInputSpec): NumberInputResult;
126
+ /** Absolute endpoint of the selected protocol; the gateway itself stays read-only. */
127
+ export declare function protocolEndpoint(baseURL: string, protocol: ApiProtocol): string;
128
+ export declare function normaliseQuotaRefreshSeconds(value: number | undefined): number | undefined;
129
+ export interface QuotaAutoRefresh {
130
+ delayMs: number;
131
+ at: string;
132
+ intervalSeconds: number;
133
+ }
134
+ /**
135
+ * Schedule the next quota refresh: the automatic interval normally, or earlier when the Host's
136
+ * cache policy reports its next planned read (`nextRefreshAt`, which is never a quota reset).
137
+ * During failure backoff the host plan wins, even when the normal interval is shorter.
138
+ * A timestamp that is already past must not create a 0 ms retry loop, so it falls back to the
139
+ * regular interval instead of hammering the console.
140
+ */
141
+ export declare function quotaAutoRefresh(secondsRaw: number | undefined, nextRefreshAt: string | undefined, now: number, backingOff?: boolean): QuotaAutoRefresh | undefined;
142
+ export interface OverviewProps {
143
+ status: Status | undefined;
144
+ busy: boolean;
145
+ onRefresh: () => void;
146
+ nextAutoRefreshAt?: string | undefined;
147
+ refreshIntervalSeconds?: number | undefined;
148
+ }
149
+ export declare function Overview({ status, busy, onRefresh, nextAutoRefreshAt, refreshIntervalSeconds }: OverviewProps): React.FunctionComponentElement<{
150
+ children?: React.ReactNode | undefined;
151
+ }>;
152
+ export interface ModelRowProps {
153
+ model: StatusModel;
154
+ settings: ModelSettings;
155
+ /** Whether an absent settings entry means enabled rather than the host catalog flag. */
156
+ authoritative: boolean;
157
+ writable: boolean;
158
+ pending: boolean;
159
+ onCommit: (next: ModelSettings | undefined) => Promise<boolean>;
160
+ }
161
+ export declare function ModelRow({ model, settings, authoritative, writable, pending, onCommit }: ModelRowProps): React.DetailedReactHTMLElement<{
162
+ className: string;
163
+ }, HTMLElement>;
164
+ export interface ModelsProps {
165
+ status: Status | undefined;
166
+ formSnapshot: FormSnapshot;
167
+ setFormField: (field: string, value: unknown) => Promise<boolean>;
168
+ pendingField: string | null;
169
+ onRefreshModels: () => void;
170
+ refreshingModels: boolean;
171
+ notice: Notice | null;
172
+ }
173
+ export declare function Models({ status, formSnapshot, setFormField, pendingField, onRefreshModels, refreshingModels, notice }: ModelsProps): React.FunctionComponentElement<{
174
+ children?: React.ReactNode | undefined;
175
+ }>;
176
+ export interface AdvancedProps {
177
+ status: Status | undefined;
178
+ formSnapshot: FormSnapshot;
179
+ setFormField: (field: string, value: unknown) => Promise<boolean>;
180
+ pendingField: string | null;
181
+ }
182
+ export declare function Advanced({ status, formSnapshot, setFormField, pendingField }: AdvancedProps): React.FunctionComponentElement<{
183
+ children?: React.ReactNode | undefined;
184
+ }>;
185
+ export declare function tabAfter(current: TabId, key: string): TabId | undefined;
186
+ export declare function SettingsPage({ caller, form }: SettingsProps): React.DetailedReactHTMLElement<{
187
+ className: string;
188
+ }, HTMLElement>;
189
+ export declare const name = "dsh-opendesign-corouter";
190
+ export declare const inject: string[];
191
+ export declare function apply(ctx: ClientContext): void;
192
+ export {};