dsh-opendesign-corouter 0.1.3 → 0.1.4
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 +6 -6
- package/SECURITY.md +3 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/lib/adapter.d.ts +1 -1
- package/lib/auth.d.ts +18 -3
- package/lib/client.d.ts +8 -1
- package/lib/client.js +65 -14
- package/lib/device-auth.d.ts +34 -0
- package/lib/generated/typert.host.js +32 -1
- package/lib/generated/typert.remote-client.js +32 -1
- package/lib/index.d.ts +5 -1
- package/lib/index.js +506 -277
- package/lib/index.js.map +4 -4
- package/lib/login.d.ts +21 -133
- package/lib/quota.d.ts +6 -0
- package/lib/remote.d.ts +4 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ dsh plugin --profile web add dsh-opendesign-corouter
|
|
|
32
32
|
也可打开 [Actions → CI](https://github.com/KouzakiUmi/dsh-opendesign-corouter/actions/workflows/ci.yml),从成功构建下载 **`dsh-opendesign-corouter-build`** artifact。登录 GitHub 后解压 ZIP,取得 `.tgz` 与 `.sha256`,再安装本地包:
|
|
33
33
|
|
|
34
34
|
```console
|
|
35
|
-
dsh plugin --profile web add /absolute/path/to/dsh-opendesign-corouter-0.1.
|
|
35
|
+
dsh plugin --profile web add /absolute/path/to/dsh-opendesign-corouter-0.1.4.tgz
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
`web` 只是示例,请替换为目标 Profile。Desktop 使用桌面安装器提供的 `dsh` 入口;独立公共 CLI 不能代管保留的 `desktop` Profile。安装后的启用与重载按目标 DSH 的管理流程进行。
|
|
@@ -40,11 +40,11 @@ dsh plugin --profile web add /absolute/path/to/dsh-opendesign-corouter-0.1.2.tgz
|
|
|
40
40
|
### 2. 配置连接
|
|
41
41
|
|
|
42
42
|
1. 打开 **设置 → OpenDesign CoRouter → 连接与登录**。
|
|
43
|
-
2.
|
|
44
|
-
3.
|
|
43
|
+
2. 点击 **开始设备授权**,再用常用浏览器 **打开授权页**,核对授权码并确认;也可单独配置工作空间 API Key 或沿用 `COROUTER_API_KEY`。
|
|
44
|
+
3. 返回设置页等待保存成功。额度默认优先读取桌面登录态;要明确使用设备授权的账号,可关闭桌面优先。
|
|
45
45
|
4. 回到模型选择器,选择 **OpenDesign CoRouter · 套餐赠送** 或 **OpenDesign CoRouter · 付费模型**。
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
**设备授权同时取得查额度和官方网关推理的两类令牌;显式配置的 API Key 仍优先。** [完整连接说明](docs/usage.md)
|
|
48
48
|
|
|
49
49
|
## 功能预览
|
|
50
50
|
|
|
@@ -72,7 +72,7 @@ dsh plugin --profile web add /absolute/path/to/dsh-opendesign-corouter-0.1.2.tgz
|
|
|
72
72
|
|
|
73
73
|
### 桌面优先,也能独立登录
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
独立登录采用设备授权,在常用浏览器打开普通官方授权页,沿用现有配置、密码管理器和自动填充。确认后设备令牌保存到 DSH 凭据服务;取消、超时和卸载停止轮询,不关闭用户浏览器,也不修改桌面配置。
|
|
76
76
|
|
|
77
77
|
<picture>
|
|
78
78
|
<source media="(prefers-color-scheme: dark)" srcset="docs/previews/theme-dark.png">
|
|
@@ -98,7 +98,7 @@ dsh plugin --profile web add /absolute/path/to/dsh-opendesign-corouter-0.1.2.tgz
|
|
|
98
98
|
| 目标 DSH Core | `0.2.1-rc.3`,以官方同版本发布包验证 |
|
|
99
99
|
| Core 声明范围 | `>=0.2.1-rc.3 <0.3.0-0`,范围内其他版本不代表已实测 |
|
|
100
100
|
| Node.js | `>=24.9.0` |
|
|
101
|
-
| 独立登录 |
|
|
101
|
+
| 独立登录 | 能访问官方授权页的常用浏览器;无需浏览器自动化或扩展 |
|
|
102
102
|
| 默认推理接口 | `https://amr-link.open-design.ai/v1/chat/completions` |
|
|
103
103
|
|
|
104
104
|
CI 在 Windows 与 Ubuntu 执行构建、类型检查及隔离测试;Ubuntu 另执行四页签浏览器和亮暗主题检查。通过 CI 不等于已安装或已通过真实账号验收。[开发与验证范围](docs/development.md)
|
package/SECURITY.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# 安全边界
|
|
2
2
|
|
|
3
3
|
- 推理、桌面控制面和控制台 Cookie 分属独立通道,不互相替代鉴权材料。
|
|
4
|
-
- API Key
|
|
4
|
+
- API Key 与设备授权的 controlKey / runtimeKey 仅经 DSH 凭据服务写入插件专属记录;设置页只读取存在性和状态,不回填秘密。
|
|
5
5
|
- 桌面配置只读。仅支持已核实的生产环境,控制令牌只能发送到 `https://amr-api.open-design.ai`。控制台和推理请求禁止自动跟随重定向。
|
|
6
|
+
- 设备授权双令牌原子保存;runtimeKey 只自动提供给请求快照中的官方推理 origin,controlKey 只发往官方控制面。旧 Cookie grant 保留只读兼容。
|
|
6
7
|
- 推理自定义地址由用户显式配置;除本机回环外要求 HTTPS,拒绝 URL 内嵌凭据、查询串与片段。
|
|
7
8
|
- 额度分池不跨身份混合。桌面认证失效时,可回退到单独保存的控制台登录;权限不足和未知额度不伪装成 0。
|
|
8
|
-
-
|
|
9
|
+
- 独立登录使用设备授权,普通浏览器仅打开官方激活页;插件不启动浏览器自动化、不读取浏览器配置或 Cookie。deviceSecret 只在 Host 内存用于轮询;授权码、激活链接和到期时间仅在等待期提供给 UI。取消、超时和卸载中止请求,不关闭用户浏览器。持久化成功是不可撤回的提交边界,之后取消不会假称已撤回。
|
|
9
10
|
- `OPENDESIGN_VELA_PATH` 是用户信任的本地程序路径。插件仅以固定 argv 执行只读查询,不通过 shell;不校验该程序的签名或下载它。不要将此变量指向来源不明的程序。
|
|
10
11
|
- RPC 使用严格白名单解析;原始错误正文、Cookie、请求头、Key 和桌面本地路径不进入状态返回。并发保存有版本冲突检测。
|
|
11
12
|
- 请求、流空闲、额度接口、CLI、登录和 RPC 均有有界超时。停用插件会撤销 Provider / RPC 并中止自身请求。
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 第三方代码与资料
|
|
2
2
|
|
|
3
|
-
本项目的消息/图片兼容处理、套餐模型元数据参考 [dsh-opendesign-go 0.1.9](https://github.com/EliteOtaku/dsh-opendesign-go)
|
|
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
4
|
|
|
5
5
|
离线主题夹具使用 [DeepSeek Harness 官方主题令牌](https://github.com/deepseek-ai/deepseek-harness/blob/22ddee3792402c3e70ec5f86a63c3e3e895a2f60/packages/client/ui-theme/src/styles/design-platform.css),仅供测试,不随插件安装包分发。
|
|
6
6
|
|
package/lib/adapter.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type RuntimeConfig } from './config.js';
|
|
|
5
5
|
export interface AdapterOptions {
|
|
6
6
|
config(): RuntimeConfig;
|
|
7
7
|
models?(): readonly CatalogModel[];
|
|
8
|
-
apiKey(ref: string): Promise<string | undefined>;
|
|
8
|
+
apiKey(ref: string, baseURL?: string): Promise<string | undefined>;
|
|
9
9
|
imageDataUrl?(image: ImageAttachmentRef, signal: AbortSignal): Promise<string | undefined>;
|
|
10
10
|
onEmptyTurn?(): void;
|
|
11
11
|
fetch?: typeof fetch;
|
package/lib/auth.d.ts
CHANGED
|
@@ -77,11 +77,22 @@ export interface DesktopDeps {
|
|
|
77
77
|
*/
|
|
78
78
|
export declare function readDesktopCredential(deps?: DesktopDeps): Promise<DesktopRead>;
|
|
79
79
|
/** Cookie payload persisted through the credential seam as a `grant` record. */
|
|
80
|
-
export interface
|
|
80
|
+
export interface CookieLoginGrant {
|
|
81
81
|
cookie: string;
|
|
82
|
-
/**
|
|
82
|
+
/** Legacy cookie expiry only; never applied to device tokens. */
|
|
83
83
|
expiresAt?: number;
|
|
84
84
|
}
|
|
85
|
+
export interface DeviceLoginGrant {
|
|
86
|
+
kind: 'device';
|
|
87
|
+
controlKey: string;
|
|
88
|
+
runtimeKey: string;
|
|
89
|
+
savedAt?: string;
|
|
90
|
+
}
|
|
91
|
+
export type LoginGrant = CookieLoginGrant | DeviceLoginGrant;
|
|
92
|
+
/** Validate both keys as one snapshot; never combine an incomplete grant with another source. */
|
|
93
|
+
export declare function deviceGrant(value: unknown): DeviceLoginGrant | undefined;
|
|
94
|
+
/** Device runtime credentials may only reach this exact HTTPS origin, never a custom gateway. */
|
|
95
|
+
export declare function isOfficialRuntimeUrl(raw: string): boolean;
|
|
85
96
|
export type AuthSource = 'desktop' | 'login' | 'environment';
|
|
86
97
|
export interface AuthSession extends Session {
|
|
87
98
|
source: SessionSource;
|
|
@@ -112,11 +123,15 @@ export interface AuthDeps extends DesktopDeps {
|
|
|
112
123
|
}
|
|
113
124
|
/** Desktop `controlKey` as a Bearer header (daemon evidence: `/api/v1/wallet/balance`). */
|
|
114
125
|
export declare function desktopSession(credential: DesktopCredential, apiUrl?: string): AuthSession;
|
|
126
|
+
/** Device control token: Bearer on the official control plane only. No inferred token expiry. */
|
|
127
|
+
export declare function deviceSession(tokens: {
|
|
128
|
+
controlKey: string;
|
|
129
|
+
} | string): AuthSession;
|
|
115
130
|
/** A persisted or environment-supplied console cookie as a `cookie` header. */
|
|
116
131
|
export declare function cookieSession(cookie: string, source: SessionSource, apiUrl?: string): AuthSession | undefined;
|
|
117
132
|
/**
|
|
118
133
|
* Resolve the session in the order the user asked for: **desktop first**, then the
|
|
119
|
-
*
|
|
134
|
+
* device grant saved by this plugin, then legacy cookies and environment references.
|
|
120
135
|
*
|
|
121
136
|
* A desktop config whose `apiUrl` is not an official origin is *skipped*, not used: the
|
|
122
137
|
* token must not travel to a host the plugin cannot vouch for. The skip is reported as a
|
package/lib/client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import openDesignCoRouterRemote, { type CoRouterRemote } from './generated/typert.remote-client.js';
|
|
3
3
|
import type { TypertClientRemote } from '@deepseek-ai/dsh-typert-protocol';
|
|
4
|
-
import { type Status, type StatusModel } from './remote.js';
|
|
4
|
+
import { type LoginState, type Status, type StatusModel } from './remote.js';
|
|
5
5
|
import type { QuotaSnapshot, QuotaWindow } from './quota.js';
|
|
6
6
|
import { type ApiProtocol } from './protocol-options.js';
|
|
7
7
|
import type { ModelSettings, ModelSettingsMap } from './model-settings.js';
|
|
@@ -183,6 +183,13 @@ export interface AdvancedProps {
|
|
|
183
183
|
export declare function Advanced({ status, formSnapshot, setFormField, pendingField }: AdvancedProps): React.FunctionComponentElement<{
|
|
184
184
|
children?: React.ReactNode | undefined;
|
|
185
185
|
}>;
|
|
186
|
+
/** Approval opens through an ordinary link; no browser profile is created or controlled. */
|
|
187
|
+
export declare function LoginApproval({ login }: {
|
|
188
|
+
login: LoginState | undefined;
|
|
189
|
+
}): React.DetailedReactHTMLElement<{
|
|
190
|
+
className: string;
|
|
191
|
+
'aria-label': string;
|
|
192
|
+
}, HTMLElement> | null;
|
|
186
193
|
export declare function tabAfter(current: TabId, key: string): TabId | undefined;
|
|
187
194
|
export declare function SettingsPage({ caller, form }: SettingsProps): React.DetailedReactHTMLElement<{
|
|
188
195
|
className: string;
|
package/lib/client.js
CHANGED
|
@@ -32,6 +32,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
32
32
|
var client_exports = {};
|
|
33
33
|
__export(client_exports, {
|
|
34
34
|
Advanced: () => Advanced,
|
|
35
|
+
LoginApproval: () => LoginApproval,
|
|
35
36
|
ModelRow: () => ModelRow,
|
|
36
37
|
Models: () => Models,
|
|
37
38
|
Overview: () => Overview,
|
|
@@ -19925,6 +19926,27 @@ function record2(value) {
|
|
|
19925
19926
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
19926
19927
|
}
|
|
19927
19928
|
|
|
19929
|
+
// src/device-auth.ts
|
|
19930
|
+
function validateActivationUrl(value, userCode) {
|
|
19931
|
+
if (typeof value !== "string" || value.length > 512 || !/^https:\/\/open-design\.ai\/cloud\/cli\/activate(?:\?[^#]*)?$/u.test(value) || /[\s\\]/u.test(value)) return null;
|
|
19932
|
+
try {
|
|
19933
|
+
const url2 = new URL(value);
|
|
19934
|
+
if (url2.origin !== "https://open-design.ai" || url2.pathname !== "/cloud/cli/activate" || url2.username || url2.password || url2.hash) return null;
|
|
19935
|
+
const entries = [...url2.searchParams];
|
|
19936
|
+
if (entries.length > 1) return null;
|
|
19937
|
+
if (entries.length) {
|
|
19938
|
+
const entry = entries[0];
|
|
19939
|
+
if (!["code", "user_code"].includes(entry[0]) || !validUserCode(entry[1]) || userCode !== void 0 && entry[1] !== userCode) return null;
|
|
19940
|
+
}
|
|
19941
|
+
return url2.href;
|
|
19942
|
+
} catch {
|
|
19943
|
+
return null;
|
|
19944
|
+
}
|
|
19945
|
+
}
|
|
19946
|
+
function validUserCode(value) {
|
|
19947
|
+
return typeof value === "string" && /^[a-z0-9][a-z0-9-]{3,31}$/iu.test(value);
|
|
19948
|
+
}
|
|
19949
|
+
|
|
19928
19950
|
// src/protocol-options.ts
|
|
19929
19951
|
var API_PROTOCOL_IDS = ["chat-completions", "responses", "messages"];
|
|
19930
19952
|
function openAiBaseURL(baseURL) {
|
|
@@ -20086,9 +20108,19 @@ function parseLoginState(value) {
|
|
|
20086
20108
|
if (!record2(value)) throw new Error("\u767B\u5F55\u72B6\u6001\u5FC5\u987B\u662F\u5BF9\u8C61");
|
|
20087
20109
|
const jobId = value.jobId;
|
|
20088
20110
|
if (jobId !== null && jobId !== void 0 && typeof jobId !== "string") throw new Error("\u767B\u5F55\u4EFB\u52A1 id \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216 null");
|
|
20111
|
+
const state = member(value.state, LOGIN_PHASES, "\u767B\u5F55\u9636\u6BB5");
|
|
20112
|
+
let approval = {};
|
|
20113
|
+
if (state === "waiting" && (value.activationUrl !== void 0 || value.userCode !== void 0 || value.expiresAt !== void 0)) {
|
|
20114
|
+
const userCode = text(value.userCode, "\u8BBE\u5907\u6388\u6743\u7801", 64);
|
|
20115
|
+
if (!/^[A-Za-z0-9-]+$/u.test(userCode)) throw new Error("\u8BBE\u5907\u6388\u6743\u7801\u683C\u5F0F\u65E0\u6548");
|
|
20116
|
+
const activationUrl = validateActivationUrl(value.activationUrl, userCode);
|
|
20117
|
+
if (activationUrl === null) throw new Error("\u8BBE\u5907\u6388\u6743\u94FE\u63A5\u65E0\u6548");
|
|
20118
|
+
approval = { activationUrl, userCode, expiresAt: isoTime(value.expiresAt, "\u8BBE\u5907\u6388\u6743\u5230\u671F\u65F6\u95F4") };
|
|
20119
|
+
}
|
|
20089
20120
|
return {
|
|
20090
20121
|
jobId: typeof jobId === "string" && jobId.length > 0 ? text(jobId, "\u767B\u5F55\u4EFB\u52A1 id", MAX_JOB_ID) : null,
|
|
20091
|
-
state
|
|
20122
|
+
state,
|
|
20123
|
+
...approval,
|
|
20092
20124
|
...value.message === void 0 ? {} : { message: text(value.message, "\u767B\u5F55\u8BF4\u660E", MAX_MESSAGE) },
|
|
20093
20125
|
...value.startedAt === void 0 ? {} : { startedAt: isoTime(value.startedAt, "\u767B\u5F55\u5F00\u59CB\u65F6\u95F4") },
|
|
20094
20126
|
...value.finishedAt === void 0 ? {} : { finishedAt: isoTime(value.finishedAt, "\u767B\u5F55\u7ED3\u675F\u65F6\u95F4") }
|
|
@@ -20208,10 +20240,10 @@ var MAX_CONTEXT_WINDOW = 1e8;
|
|
|
20208
20240
|
var GENERIC_ERROR = "\u64CD\u4F5C\u672A\u5B8C\u6210\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
20209
20241
|
var LOGIN_LABELS = {
|
|
20210
20242
|
idle: "\u5C1A\u672A\u5F00\u59CB\u72EC\u7ACB\u767B\u5F55",
|
|
20211
|
-
launching: "\u6B63\u5728\
|
|
20212
|
-
waiting: "\
|
|
20213
|
-
verifying: "\u6B63\u5728\
|
|
20214
|
-
success: "\u767B\u5F55\u6210\u529F\uFF0C\
|
|
20243
|
+
launching: "\u6B63\u5728\u7533\u8BF7\u8BBE\u5907\u6388\u6743\u7801\u2026",
|
|
20244
|
+
waiting: "\u8BF7\u6253\u5F00\u6388\u6743\u9875\uFF0C\u5728\u5E38\u7528\u6D4F\u89C8\u5668\u4E2D\u786E\u8BA4\u672C\u6B21\u767B\u5F55\u3002",
|
|
20245
|
+
verifying: "\u6B63\u5728\u4FDD\u5B58\u8BBE\u5907\u6388\u6743\u2026",
|
|
20246
|
+
success: "\u767B\u5F55\u6210\u529F\uFF0C\u8BBE\u5907\u6388\u6743\u5DF2\u4FDD\u5B58\u3002",
|
|
20215
20247
|
cancelled: "\u5DF2\u53D6\u6D88\u672C\u6B21\u767B\u5F55\u3002",
|
|
20216
20248
|
error: "\u767B\u5F55\u672A\u5B8C\u6210\uFF0C\u8BF7\u91CD\u8BD5\u3002"
|
|
20217
20249
|
};
|
|
@@ -20531,7 +20563,7 @@ function Overview({ status, busy, onRefresh, nextAutoRefreshAt, refreshIntervalS
|
|
|
20531
20563
|
"div",
|
|
20532
20564
|
null,
|
|
20533
20565
|
h("h3", null, "\u5148\u5B8C\u6210\u8FDE\u63A5"),
|
|
20534
|
-
h("p", { className: "odc-hint" }, status.apiKeyConfigured === false ? "\u63A8\u7406\
|
|
20566
|
+
h("p", { className: "odc-hint" }, status.apiKeyConfigured === false ? "\u5C1A\u65E0\u5F53\u524D\u7F51\u5173\u7684\u63A8\u7406\u51ED\u636E\uFF1B\u53EF\u5B8C\u6210\u8BBE\u5907\u6388\u6743\u6216\u5355\u72EC\u914D\u7F6E API Key\u3002" : "API Key \u5DF2\u914D\u7F6E\uFF1B\u767B\u5F55\u540E\u5373\u53EF\u67E5\u770B\u8D26\u53F7\u989D\u5EA6\u3002")
|
|
20535
20567
|
),
|
|
20536
20568
|
onConnection === void 0 ? null : h("button", { type: "button", className: "odc-button odc-button-primary", onClick: onConnection }, "\u524D\u5F80\u8FDE\u63A5\u4E0E\u767B\u5F55")
|
|
20537
20569
|
) : null,
|
|
@@ -21069,6 +21101,24 @@ function Switch({ checked, disabled, label, hint, onChange }) {
|
|
|
21069
21101
|
function loginInFlight(state) {
|
|
21070
21102
|
return state === "launching" || state === "waiting" || state === "verifying";
|
|
21071
21103
|
}
|
|
21104
|
+
function LoginApproval({ login }) {
|
|
21105
|
+
if (login?.state !== "waiting" || login.userCode === void 0 || login.expiresAt === void 0) return null;
|
|
21106
|
+
const url2 = validateActivationUrl(login.activationUrl, login.userCode);
|
|
21107
|
+
if (url2 === null) return null;
|
|
21108
|
+
return h(
|
|
21109
|
+
"div",
|
|
21110
|
+
{ className: "odc-readonly", "aria-label": "\u8BBE\u5907\u6388\u6743" },
|
|
21111
|
+
h("span", { className: "odc-label" }, "\u6838\u5BF9\u6388\u6743\u7801"),
|
|
21112
|
+
h("code", { className: "odc-metric-value", "aria-label": "\u8BBE\u5907\u6388\u6743\u7801" }, login.userCode),
|
|
21113
|
+
h(
|
|
21114
|
+
"div",
|
|
21115
|
+
{ className: "odc-actions" },
|
|
21116
|
+
h("a", { className: "odc-button odc-button-primary", href: url2, target: "_blank", rel: "noreferrer noopener" }, "\u6253\u5F00\u6388\u6743\u9875")
|
|
21117
|
+
),
|
|
21118
|
+
h("p", { className: "odc-hint" }, `\u6388\u6743\u7801\u6709\u6548\u81F3 ${formatTime(login.expiresAt)}\u3002\u5728\u5E38\u7528\u6D4F\u89C8\u5668\u786E\u8BA4\u540E\uFF0C\u8FD4\u56DE\u6B64\u9875\u67E5\u770B\u7ED3\u679C\u3002`),
|
|
21119
|
+
h("p", { className: "odc-hint" }, "\u53EF\u53F3\u952E\u590D\u5236\u6388\u6743\u94FE\u63A5\u5230\u5E38\u7528\u6D4F\u89C8\u5668\uFF1B\u5BC6\u7801\u7BA1\u7406\u5668\u548C\u81EA\u52A8\u586B\u5145\u7531\u6D4F\u89C8\u5668\u6B63\u5E38\u63D0\u4F9B\u3002\u4EC5\u786E\u8BA4\u4F60\u672C\u4EBA\u53D1\u8D77\u7684\u6388\u6743\u3002")
|
|
21120
|
+
);
|
|
21121
|
+
}
|
|
21072
21122
|
function Connection({ status, formSnapshot, setFormField, pendingField, caller, loadStatus, refreshAfterLogin }) {
|
|
21073
21123
|
const mounted = useMounted();
|
|
21074
21124
|
const actionVersion = React.useRef(0);
|
|
@@ -21086,11 +21136,11 @@ function Connection({ status, formSnapshot, setFormField, pendingField, caller,
|
|
|
21086
21136
|
if (status === void 0 || busy || keyBusy) return;
|
|
21087
21137
|
const version2 = ++actionVersion.current;
|
|
21088
21138
|
setBusy(true);
|
|
21089
|
-
setNotice({ kind: "status", text: "\u6B63\u5728\
|
|
21139
|
+
setNotice({ kind: "status", text: "\u6B63\u5728\u7533\u8BF7\u8BBE\u5907\u6388\u6743\u2026" });
|
|
21090
21140
|
try {
|
|
21091
21141
|
const next = await caller("startLogin", { expectedRevision: status.revision });
|
|
21092
21142
|
if (!mounted.current || version2 !== actionVersion.current) return;
|
|
21093
|
-
setNotice(
|
|
21143
|
+
setNotice(null);
|
|
21094
21144
|
await (next.state === "success" ? refreshAfterLogin() : loadStatus());
|
|
21095
21145
|
} catch {
|
|
21096
21146
|
if (mounted.current && version2 === actionVersion.current) setNotice({ kind: "error", text: "\u65E0\u6CD5\u542F\u52A8\u767B\u5F55\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" });
|
|
@@ -21163,27 +21213,28 @@ function Connection({ status, formSnapshot, setFormField, pendingField, caller,
|
|
|
21163
21213
|
h(
|
|
21164
21214
|
"section",
|
|
21165
21215
|
{ className: "odc-card" },
|
|
21166
|
-
h("h3", null, "\
|
|
21167
|
-
h("p", { className: "odc-hint" }, "\
|
|
21216
|
+
h("h3", null, "\u8BBE\u5907\u6388\u6743\u767B\u5F55"),
|
|
21217
|
+
h("p", { className: "odc-hint" }, "\u7533\u8BF7\u6388\u6743\u7801\u540E\uFF0C\u5728\u5E38\u7528\u6D4F\u89C8\u5668\u6253\u5F00\u5B98\u65B9\u6388\u6743\u9875\u3002\u6CBF\u7528\u5DF2\u6709\u767B\u5F55\u72B6\u6001\u3001\u5BC6\u7801\u7BA1\u7406\u5668\u548C\u81EA\u52A8\u586B\u5145\uFF1B\u786E\u8BA4\u540E\u81EA\u52A8\u4FDD\u5B58\u8BBE\u5907\u4EE4\u724C\uFF0C\u7528\u4E8E\u67E5\u8BE2\u989D\u5EA6\u53CA\u5B98\u65B9\u7F51\u5173\u63A8\u7406\u3002"),
|
|
21168
21218
|
h(
|
|
21169
21219
|
"div",
|
|
21170
21220
|
{ className: "odc-actions", style: { marginTop: 14 } },
|
|
21171
21221
|
h("button", { className: "odc-button odc-button-primary", type: "button", disabled: status === void 0 || busy || keyBusy || inFlight, onClick: () => {
|
|
21172
21222
|
void startLogin();
|
|
21173
|
-
} }, "\
|
|
21223
|
+
} }, "\u5F00\u59CB\u8BBE\u5907\u6388\u6743"),
|
|
21174
21224
|
inFlight ? h("button", { className: "odc-button", type: "button", disabled: busy || keyBusy || login?.jobId == null, onClick: () => {
|
|
21175
21225
|
void cancelLogin();
|
|
21176
21226
|
} }, "\u53D6\u6D88\u767B\u5F55") : null,
|
|
21177
21227
|
h("a", { className: "odc-link", href: OFFICIAL_SITE, target: "_blank", rel: "noreferrer noopener" }, "\u6253\u5F00 CoRouter \u5B98\u7F51")
|
|
21178
21228
|
),
|
|
21179
|
-
h(
|
|
21229
|
+
h(LoginApproval, { login }),
|
|
21230
|
+
h("p", { className: "odc-meta", role: state === "error" ? "alert" : "status" }, login?.message ?? LOGIN_LABELS[state]),
|
|
21180
21231
|
h(NoticeView, { notice })
|
|
21181
21232
|
),
|
|
21182
21233
|
h(
|
|
21183
21234
|
"section",
|
|
21184
21235
|
{ className: "odc-card" },
|
|
21185
|
-
h("h3", null, "API Key"),
|
|
21186
|
-
h("p", { className: "odc-hint" }, status?.apiKeyConfigured === true ? "\
|
|
21236
|
+
h("h3", null, "API Key\uFF08\u53EF\u9009\u8986\u76D6\uFF09"),
|
|
21237
|
+
h("p", { className: "odc-hint" }, status?.apiKeyConfigured === true ? "\u5F53\u524D\u7F51\u5173\u5DF2\u6709\u53EF\u7528\u63A8\u7406\u51ED\u636E\u3002\u4F18\u5148\u7EA7\uFF1A\u73AF\u5883\u6216\u51ED\u636E\u5F15\u7528 \u2192 \u624B\u52A8\u4FDD\u5B58\u7684 API Key \u2192 \u8BBE\u5907\u6388\u6743\u4EE4\u724C\u3002" : "\u5F53\u524D\u7F51\u5173\u5C1A\u65E0\u63A8\u7406\u51ED\u636E\u3002\u53EF\u5B8C\u6210\u8BBE\u5907\u6388\u6743\u4EE5\u4F7F\u7528\u5B98\u65B9\u7F51\u5173\uFF0C\u6216\u4E3A\u5F53\u524D\u7F51\u5173\u5355\u72EC\u4FDD\u5B58 API Key\u3002"),
|
|
21187
21238
|
h(
|
|
21188
21239
|
"form",
|
|
21189
21240
|
{ onSubmit: (event) => {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface DeviceTokens {
|
|
2
|
+
controlKey: string;
|
|
3
|
+
runtimeKey: string;
|
|
4
|
+
}
|
|
5
|
+
export declare const DEVICE_AUTH_ENDPOINT = "https://amr-api.open-design.ai/api/v1/cli/device-authorizations";
|
|
6
|
+
export declare const DEVICE_AUTH_PLUGIN_VERSION = "0.1.4";
|
|
7
|
+
export declare const DEVICE_REQUEST_TIMEOUT_MS = 20000;
|
|
8
|
+
export declare const DEVICE_RESPONSE_MAX_BYTES = 65536;
|
|
9
|
+
export declare const DEVICE_POLL_MIN_MS = 1000;
|
|
10
|
+
export declare const DEVICE_POLL_MAX_MS = 300000;
|
|
11
|
+
export declare class DeviceAuthError extends Error {
|
|
12
|
+
readonly code: string;
|
|
13
|
+
constructor(code: string);
|
|
14
|
+
}
|
|
15
|
+
/** Strict public-link allowlist: only an optional, single, non-secret code is allowed. */
|
|
16
|
+
export declare function validateActivationUrl(value: unknown, userCode?: string): string | null;
|
|
17
|
+
export interface DeviceAuthorization {
|
|
18
|
+
deviceId: string;
|
|
19
|
+
deviceSecret: string;
|
|
20
|
+
userCode: string;
|
|
21
|
+
activationUrl: string;
|
|
22
|
+
pollMs: number;
|
|
23
|
+
expiresAt: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function parseDeviceAuthorization(value: unknown): DeviceAuthorization;
|
|
26
|
+
export type DevicePollResult = {
|
|
27
|
+
state: 'pending';
|
|
28
|
+
} | {
|
|
29
|
+
state: 'success';
|
|
30
|
+
tokens: DeviceTokens;
|
|
31
|
+
};
|
|
32
|
+
export declare function parseDevicePoll(value: unknown): DevicePollResult;
|
|
33
|
+
/** Bound fetch *and body consumption*, even if an injected transport ignores abort. */
|
|
34
|
+
export declare function postDeviceJson(fetcher: typeof fetch, url: string, body: unknown, signal: AbortSignal, timeoutMs?: number): Promise<unknown>;
|
|
@@ -7,6 +7,27 @@ function record(value) {
|
|
|
7
7
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
// src/device-auth.ts
|
|
11
|
+
function validateActivationUrl(value, userCode) {
|
|
12
|
+
if (typeof value !== "string" || value.length > 512 || !/^https:\/\/open-design\.ai\/cloud\/cli\/activate(?:\?[^#]*)?$/u.test(value) || /[\s\\]/u.test(value)) return null;
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(value);
|
|
15
|
+
if (url.origin !== "https://open-design.ai" || url.pathname !== "/cloud/cli/activate" || url.username || url.password || url.hash) return null;
|
|
16
|
+
const entries = [...url.searchParams];
|
|
17
|
+
if (entries.length > 1) return null;
|
|
18
|
+
if (entries.length) {
|
|
19
|
+
const entry = entries[0];
|
|
20
|
+
if (!["code", "user_code"].includes(entry[0]) || !validUserCode(entry[1]) || userCode !== void 0 && entry[1] !== userCode) return null;
|
|
21
|
+
}
|
|
22
|
+
return url.href;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function validUserCode(value) {
|
|
28
|
+
return typeof value === "string" && /^[a-z0-9][a-z0-9-]{3,31}$/iu.test(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
10
31
|
// src/protocol-options.ts
|
|
11
32
|
var API_PROTOCOL_IDS = ["chat-completions", "responses", "messages"];
|
|
12
33
|
|
|
@@ -155,9 +176,19 @@ function parseLoginState(value) {
|
|
|
155
176
|
if (!record(value)) throw new Error("\u767B\u5F55\u72B6\u6001\u5FC5\u987B\u662F\u5BF9\u8C61");
|
|
156
177
|
const jobId = value.jobId;
|
|
157
178
|
if (jobId !== null && jobId !== void 0 && typeof jobId !== "string") throw new Error("\u767B\u5F55\u4EFB\u52A1 id \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216 null");
|
|
179
|
+
const state = member(value.state, LOGIN_PHASES, "\u767B\u5F55\u9636\u6BB5");
|
|
180
|
+
let approval = {};
|
|
181
|
+
if (state === "waiting" && (value.activationUrl !== void 0 || value.userCode !== void 0 || value.expiresAt !== void 0)) {
|
|
182
|
+
const userCode = text(value.userCode, "\u8BBE\u5907\u6388\u6743\u7801", 64);
|
|
183
|
+
if (!/^[A-Za-z0-9-]+$/u.test(userCode)) throw new Error("\u8BBE\u5907\u6388\u6743\u7801\u683C\u5F0F\u65E0\u6548");
|
|
184
|
+
const activationUrl = validateActivationUrl(value.activationUrl, userCode);
|
|
185
|
+
if (activationUrl === null) throw new Error("\u8BBE\u5907\u6388\u6743\u94FE\u63A5\u65E0\u6548");
|
|
186
|
+
approval = { activationUrl, userCode, expiresAt: isoTime(value.expiresAt, "\u8BBE\u5907\u6388\u6743\u5230\u671F\u65F6\u95F4") };
|
|
187
|
+
}
|
|
158
188
|
return {
|
|
159
189
|
jobId: typeof jobId === "string" && jobId.length > 0 ? text(jobId, "\u767B\u5F55\u4EFB\u52A1 id", MAX_JOB_ID) : null,
|
|
160
|
-
state
|
|
190
|
+
state,
|
|
191
|
+
...approval,
|
|
161
192
|
...value.message === void 0 ? {} : { message: text(value.message, "\u767B\u5F55\u8BF4\u660E", MAX_MESSAGE) },
|
|
162
193
|
...value.startedAt === void 0 ? {} : { startedAt: isoTime(value.startedAt, "\u767B\u5F55\u5F00\u59CB\u65F6\u95F4") },
|
|
163
194
|
...value.finishedAt === void 0 ? {} : { finishedAt: isoTime(value.finishedAt, "\u767B\u5F55\u7ED3\u675F\u65F6\u95F4") }
|
|
@@ -7,6 +7,27 @@ function record(value) {
|
|
|
7
7
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
// src/device-auth.ts
|
|
11
|
+
function validateActivationUrl(value, userCode) {
|
|
12
|
+
if (typeof value !== "string" || value.length > 512 || !/^https:\/\/open-design\.ai\/cloud\/cli\/activate(?:\?[^#]*)?$/u.test(value) || /[\s\\]/u.test(value)) return null;
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(value);
|
|
15
|
+
if (url.origin !== "https://open-design.ai" || url.pathname !== "/cloud/cli/activate" || url.username || url.password || url.hash) return null;
|
|
16
|
+
const entries = [...url.searchParams];
|
|
17
|
+
if (entries.length > 1) return null;
|
|
18
|
+
if (entries.length) {
|
|
19
|
+
const entry = entries[0];
|
|
20
|
+
if (!["code", "user_code"].includes(entry[0]) || !validUserCode(entry[1]) || userCode !== void 0 && entry[1] !== userCode) return null;
|
|
21
|
+
}
|
|
22
|
+
return url.href;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function validUserCode(value) {
|
|
28
|
+
return typeof value === "string" && /^[a-z0-9][a-z0-9-]{3,31}$/iu.test(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
10
31
|
// src/protocol-options.ts
|
|
11
32
|
var API_PROTOCOL_IDS = ["chat-completions", "responses", "messages"];
|
|
12
33
|
|
|
@@ -155,9 +176,19 @@ function parseLoginState(value) {
|
|
|
155
176
|
if (!record(value)) throw new Error("\u767B\u5F55\u72B6\u6001\u5FC5\u987B\u662F\u5BF9\u8C61");
|
|
156
177
|
const jobId = value.jobId;
|
|
157
178
|
if (jobId !== null && jobId !== void 0 && typeof jobId !== "string") throw new Error("\u767B\u5F55\u4EFB\u52A1 id \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216 null");
|
|
179
|
+
const state = member(value.state, LOGIN_PHASES, "\u767B\u5F55\u9636\u6BB5");
|
|
180
|
+
let approval = {};
|
|
181
|
+
if (state === "waiting" && (value.activationUrl !== void 0 || value.userCode !== void 0 || value.expiresAt !== void 0)) {
|
|
182
|
+
const userCode = text(value.userCode, "\u8BBE\u5907\u6388\u6743\u7801", 64);
|
|
183
|
+
if (!/^[A-Za-z0-9-]+$/u.test(userCode)) throw new Error("\u8BBE\u5907\u6388\u6743\u7801\u683C\u5F0F\u65E0\u6548");
|
|
184
|
+
const activationUrl = validateActivationUrl(value.activationUrl, userCode);
|
|
185
|
+
if (activationUrl === null) throw new Error("\u8BBE\u5907\u6388\u6743\u94FE\u63A5\u65E0\u6548");
|
|
186
|
+
approval = { activationUrl, userCode, expiresAt: isoTime(value.expiresAt, "\u8BBE\u5907\u6388\u6743\u5230\u671F\u65F6\u95F4") };
|
|
187
|
+
}
|
|
158
188
|
return {
|
|
159
189
|
jobId: typeof jobId === "string" && jobId.length > 0 ? text(jobId, "\u767B\u5F55\u4EFB\u52A1 id", MAX_JOB_ID) : null,
|
|
160
|
-
state
|
|
190
|
+
state,
|
|
191
|
+
...approval,
|
|
161
192
|
...value.message === void 0 ? {} : { message: text(value.message, "\u767B\u5F55\u8BF4\u660E", MAX_MESSAGE) },
|
|
162
193
|
...value.startedAt === void 0 ? {} : { startedAt: isoTime(value.startedAt, "\u767B\u5F55\u5F00\u59CB\u65F6\u95F4") },
|
|
163
194
|
...value.finishedAt === void 0 ? {} : { finishedAt: isoTime(value.finishedAt, "\u767B\u5F55\u7ED3\u675F\u65F6\u95F4") }
|
package/lib/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { fetchModelCatalog } from './model-discovery.js';
|
|
|
4
4
|
import type { CatalogModel } from './catalog.js';
|
|
5
5
|
import { readConfig, type LiveConfig } from './config.js';
|
|
6
6
|
import { resolveSession } from './auth.js';
|
|
7
|
+
import { type LoginOptions } from './login.js';
|
|
7
8
|
import { readQuota, type QuotaSnapshot } from './quota.js';
|
|
8
9
|
import { readDesktopQuota } from './desktop-quota.js';
|
|
9
10
|
import { type Status, type LoginState, type StartLoginRequest, type CancelLoginRequest, type SaveApiKeyRequest } from './remote.js';
|
|
@@ -18,6 +19,7 @@ export interface HostDependencies {
|
|
|
18
19
|
readDesktopQuota: typeof readDesktopQuota;
|
|
19
20
|
fetchModels?: typeof fetchModelCatalog;
|
|
20
21
|
modelsChanged?: () => void;
|
|
22
|
+
loginOptions?: LoginOptions;
|
|
21
23
|
}
|
|
22
24
|
export declare class OpenDesignCoRouterService extends TypertRemoteService {
|
|
23
25
|
readonly dependencies: HostDependencies;
|
|
@@ -44,7 +46,9 @@ export declare class OpenDesignCoRouterService extends TypertRemoteService {
|
|
|
44
46
|
modelCatalog(): readonly CatalogModel[];
|
|
45
47
|
constructor(ctx: Context, config: () => ReturnType<typeof readConfig>, dependencies?: HostDependencies);
|
|
46
48
|
private _credentials;
|
|
47
|
-
|
|
49
|
+
private _key;
|
|
50
|
+
resolveApiKey(ref: string, baseURL?: string): Promise<string | undefined>;
|
|
51
|
+
private _invalidateModels;
|
|
48
52
|
private _state;
|
|
49
53
|
private _conflict;
|
|
50
54
|
private _expect;
|