dsh-router-qoder 0.0.0-stage → 0.1.1
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 +21 -0
- package/README.md +275 -2
- package/cordis.patch.yml +10 -0
- package/lib/index.js +1766 -0
- package/package.json +63 -4
- package/src/cn.ts +41 -0
- package/src/contract.ts +169 -0
- package/src/core.test.ts +552 -0
- package/src/core.ts +425 -0
- package/src/cosy.ts +270 -0
- package/src/global.ts +38 -0
- package/src/icon.ts +8 -0
- package/src/index.ts +64 -0
- package/src/messages.ts +337 -0
- package/src/sse.ts +176 -0
- package/src/types.ts +42 -0
- package/src/upstream.ts +992 -0
package/package.json
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-router-qoder",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "DSH plugin: Qoder supplier family for dsh-router. Provides two same-protocol suppliers through the router.suppliers cordis service: qoder (Qoder CN, gateway.qoder.com.cn) and qoder-global (api1.qoder.sh). Both share one implementation (cosy signer, jobToken exchange, SSE conversion); only the gateway profile differs.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/CARVIN94/dsh-router-qoder.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/CARVIN94/dsh-router-qoder#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/CARVIN94/dsh-router-qoder/issues"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./lib/index.js",
|
|
17
|
+
"./src/*": "./src/*",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"registry": "https://registry.npmjs.org"
|
|
25
|
+
},
|
|
26
|
+
"dsh": {
|
|
27
|
+
"bundle": {
|
|
28
|
+
"patch": "./cordis.patch.yml"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"lib/index.js",
|
|
33
|
+
"src",
|
|
34
|
+
"cordis.patch.yml",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"keywords": [
|
|
39
|
+
"dsh",
|
|
40
|
+
"dsh-plugin",
|
|
41
|
+
"dsh-router",
|
|
42
|
+
"qoder",
|
|
43
|
+
"cosy"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "rm -rf lib && tsdown",
|
|
47
|
+
"test": "node --test \"src/**/*.test.ts\"",
|
|
48
|
+
"typecheck": "tsc --noEmit"
|
|
49
|
+
},
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
53
|
+
"cordis": "^4.0.0-rc.7"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"cordis": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/node": "^26.6.5",
|
|
62
|
+
"tsdown": "^0.23.1",
|
|
63
|
+
"typescript": "^7.0.2"
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/cn.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* qoder(Qoder 国内版)供应商 profile —— 上游 https://gateway.qoder.com.cn。
|
|
3
|
+
*
|
|
4
|
+
* 全部共享逻辑在 core.ts;这里只声明这个部署的差异(区域、图标、名字、uid 前缀)。
|
|
5
|
+
* 区域决定的主机与端点表在 upstream.ts 的 `QODER_REGIONS` 里 —— 那是**唯一**一处
|
|
6
|
+
* 事实,profile 只说「我是哪个区域」,不重复写域名。
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ `id = 'qoder'` **同时是凭证与配置的存储键**(`credentials.sqlite` 的
|
|
9
|
+
* `supplier` 列、`supplier-config.json` 的键,含积分缓存)。改它 = 已添加的
|
|
10
|
+
* 账号全部消失。
|
|
11
|
+
*/
|
|
12
|
+
import type { ModelInfo } from './types.ts'
|
|
13
|
+
import type { QoderProfile } from './core.ts'
|
|
14
|
+
import { QODER_ICON } from './icon.ts'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 兜底模型列表:**故意留空**。
|
|
18
|
+
*
|
|
19
|
+
* Qoder 的模型 key 是上游下发的(形如 `qmodel_38max` 这类服务端标识),不是可以
|
|
20
|
+
* 照着模型名猜出来的。猜一个塞进去,用户点了只会拿到 404,而「没有 PAT 时列表
|
|
21
|
+
* 为空」本来就是正确状态 —— 加 PAT 后点一次「获取模型」就有了。宁可空着,也不
|
|
22
|
+
* 让面板列出一堆点不动的假模型。
|
|
23
|
+
*/
|
|
24
|
+
const FALLBACK_MODELS: ModelInfo[] = []
|
|
25
|
+
|
|
26
|
+
export const profile: QoderProfile = {
|
|
27
|
+
id: 'qoder',
|
|
28
|
+
name: 'Qoder',
|
|
29
|
+
region: 'cn',
|
|
30
|
+
icon: QODER_ICON,
|
|
31
|
+
priority: -2, // 账户自带额度,排在免费通道之前
|
|
32
|
+
uidPrefix: 'qd',
|
|
33
|
+
defaultNickname: 'Qoder',
|
|
34
|
+
errPrefix: 'qoder',
|
|
35
|
+
// 弹窗里唯一能看到「去哪儿拿」的地方:只写令牌形状不写入口,用户只能自己去搜。
|
|
36
|
+
apiKeyHint: '粘贴 Qoder 个人访问令牌(PAT,形如 pt-…)。在哪拿:登录 qoder.cn / qoder.com.cn 控制台 → '
|
|
37
|
+
+ '设置 → 个人访问令牌(旧版页面叫「API 令牌」)→ 创建令牌,复制只显示一次的 pt-…。'
|
|
38
|
+
+ '令牌按区域签发,国内版的 pt-… 在国际版卡片上无效(反之亦然)。'
|
|
39
|
+
+ '插件只在本机用它换 jobToken,之后的请求都带签名会话;PAT 不会发给 Qoder 网关以外的任何地方。',
|
|
40
|
+
fallbackModels: FALLBACK_MODELS,
|
|
41
|
+
}
|
package/src/contract.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 供应商契约 —— dsh-router-codebuddy 自含(不依赖 dsh-router 包)。
|
|
3
|
+
* 与 dsh-router 的 suppliers/contract.ts 契约保持同步:
|
|
4
|
+
* 通用能力(连接池/模型管理/别名/凭证)由 dsh-router 核心统一管,
|
|
5
|
+
* 本插件只提供**差异化能力**。
|
|
6
|
+
*/
|
|
7
|
+
import type { ChatRequest, ModelInfo, SupplierAccount } from './types.ts'
|
|
8
|
+
|
|
9
|
+
/** 通用配置存储(dsh-router 核心注入;连接池/模型管理/别名/积分缓存)。 */
|
|
10
|
+
export interface SupplierConfigStoreLike {
|
|
11
|
+
get(id: string): {
|
|
12
|
+
alias: string
|
|
13
|
+
disabled: string[]
|
|
14
|
+
custom: string[]
|
|
15
|
+
poolOrder: string[]
|
|
16
|
+
poolStrategy: 'fallback' | 'round-robin'
|
|
17
|
+
/** 积分缓存:uid → 剩余额度(-1 = 还没拿到过)。 */
|
|
18
|
+
credits: Record<string, number>
|
|
19
|
+
}
|
|
20
|
+
setAlias(id: string, alias: string): void
|
|
21
|
+
setPoolOrder(id: string, uids: string[]): void
|
|
22
|
+
setPoolStrategy(id: string, strategy: string): void
|
|
23
|
+
setModelEnabled(id: string, modelId: string, enabled: boolean): void
|
|
24
|
+
addCustomModel(id: string, modelId: string): void
|
|
25
|
+
removeCustomModel(id: string, modelId: string): void
|
|
26
|
+
setAllModelsEnabled(id: string, enabled: boolean, modelIds: string[]): void
|
|
27
|
+
/** 读积分缓存;没缓存过返回 -1。 */
|
|
28
|
+
getCredits(id: string, uid: string): number
|
|
29
|
+
/** 合并插件报的积分:-1(未知)回落到缓存值,非 -1 写入并返回它。 */
|
|
30
|
+
putCredits(id: string, uid: string, reported: number): number
|
|
31
|
+
/** 删链接时清掉它的积分缓存。 */
|
|
32
|
+
clearCredits(id: string, uid: string): void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 通用凭证存储(dsh-router 核心注入;不透明凭证 blob)。 */
|
|
36
|
+
export interface CredentialStoreLike {
|
|
37
|
+
list(supplierId: string): string[]
|
|
38
|
+
get<T = unknown>(supplierId: string, uid: string): T | undefined
|
|
39
|
+
save(supplierId: string, uid: string, blob: unknown): void
|
|
40
|
+
remove(supplierId: string, uid: string): void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 账户此刻的状态 —— 插件**解读**上游信号后的语义状态。
|
|
45
|
+
* 插件只报「现在怎么了」,不说「该怎么办」:冷却多久、是否禁用、要不要换号
|
|
46
|
+
* 都是核心的策略。
|
|
47
|
+
*/
|
|
48
|
+
export type AccountState =
|
|
49
|
+
| 'ok' // 正常
|
|
50
|
+
| 'rate_limit' // 限流(429 类)
|
|
51
|
+
| 'quota' // 额度/权益不足
|
|
52
|
+
| 'session_dead' // 凭证彻底失效,必须重新登录才能恢复
|
|
53
|
+
| 'unavailable' // 上游不可用(404 / 服务下线)
|
|
54
|
+
| 'transport' // 网络/连接层失败(没拿到 HTTP 状态)
|
|
55
|
+
| 'unknown' // 说不清是什么错
|
|
56
|
+
/** 这个模型不属于本供应商(不是账号的失败)。核心据此跳过整个供应商换下一个,
|
|
57
|
+
* 而不是记在账号头上——否则组合里每有一个别人家的模型,就会给无关账号攒
|
|
58
|
+
* 一次错误,攒够阈值把它冷却掉。 */
|
|
59
|
+
| 'no_such_model'
|
|
60
|
+
/** 请求本身不合法被上游拒(参数错/图片认不出/tool_call 配对断裂)——同一个
|
|
61
|
+
* 请求对每个号都一样失败,不是账号的错。核心不惩罚账号,直接换下一个。 */
|
|
62
|
+
| 'bad_request'
|
|
63
|
+
|
|
64
|
+
/** 账号「现在状态」(插件只报它观察到的部分)。 */
|
|
65
|
+
export interface SupplierAccountNow {
|
|
66
|
+
uid: string
|
|
67
|
+
nickname?: string
|
|
68
|
+
/**
|
|
69
|
+
* 剩余额度。**拿不到就报 `-1`**,不是 0:
|
|
70
|
+
* 插件刚重启(内存缓存空)、积分拉取失败、或压根不支持积分时都是 `-1`。
|
|
71
|
+
* 核心据此保留上一次持久化下来的值 —— 报 0 会把缓存冲成 0(0 是真值
|
|
72
|
+
* 「用完了」,核心分不清「拿到 0」和「没拿到」)。
|
|
73
|
+
*/
|
|
74
|
+
credits: number
|
|
75
|
+
state: AccountState
|
|
76
|
+
message?: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 插件报的供应商状态。 */
|
|
80
|
+
export interface SupplierStatusNow {
|
|
81
|
+
id: string
|
|
82
|
+
name: string
|
|
83
|
+
accounts: SupplierAccountNow[]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 一次上游调用的结果。 */
|
|
87
|
+
export type ChatOnceResult =
|
|
88
|
+
/** 流式:插件已把上游协议转成 OpenAI SSE,核心只负责 pipe 到 res。 */
|
|
89
|
+
| { ok: true; stream: ReadableStream<Uint8Array> }
|
|
90
|
+
/** 非流式:核心写 JSON。 */
|
|
91
|
+
| { ok: true; status: number; body: string }
|
|
92
|
+
/**
|
|
93
|
+
* 失败:核心据此做冷却/禁用/换号。
|
|
94
|
+
* `resetAt` = 上游自己声明的重置时刻(epoch ms,可选):限流类错误带它时
|
|
95
|
+
* 核心冷却到该点而不是指数退避(6004「将在 … 重置」就属于这类)。
|
|
96
|
+
*/
|
|
97
|
+
| { ok: false; state: AccountState; message: string; resetAt?: number }
|
|
98
|
+
|
|
99
|
+
/** 供应商配置:dsh-router 核心注入的运行时环境。 */
|
|
100
|
+
export interface SupplierEnv {
|
|
101
|
+
dataDir: string
|
|
102
|
+
log: (msg: string) => void
|
|
103
|
+
store: SupplierConfigStoreLike
|
|
104
|
+
credentials: CredentialStoreLike
|
|
105
|
+
/**
|
|
106
|
+
* 迟到的失败上报(可选)——**只在响应已提交之后**才用。
|
|
107
|
+
*
|
|
108
|
+
* 流式请求一旦写出第一个字节就绑死(HTTP 语义),此时上游再报错已经换不了
|
|
109
|
+
* 号、也改不了状态码。但「这个号坏了」对**后续**请求仍有用:不报上来它就会
|
|
110
|
+
* 继续留在池里被轮转选中。核心实现 = `pool.noteFailure`,按状态冷却/禁用。
|
|
111
|
+
* 插件拿不到也不用兜底:不调用就退化成今天的行为。
|
|
112
|
+
*/
|
|
113
|
+
onLateFailure?: (uid: string, model: string, state: AccountState, message: string) => void
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 供应商模块 —— 契约(核心必须,差异化可选)。 */
|
|
117
|
+
export interface SupplierModule {
|
|
118
|
+
readonly id: string
|
|
119
|
+
readonly name: string
|
|
120
|
+
readonly priority?: number
|
|
121
|
+
/** 图标(URL 或 SVG data URI),面板供应商卡片显示。可选。 */
|
|
122
|
+
readonly icon?: string
|
|
123
|
+
/**
|
|
124
|
+
* 「添加 API key」弹窗的提示文案。可选:缺省时前端用通用文案。
|
|
125
|
+
*
|
|
126
|
+
* 走 `addApiKey` 的供应商应该提供它 —— 否则所有供应商的弹窗都显示同一句
|
|
127
|
+
* 通用提示,用户不知道该去哪儿拿这个 key。
|
|
128
|
+
*/
|
|
129
|
+
readonly apiKeyHint?: string
|
|
130
|
+
|
|
131
|
+
// ---- 核心(必须) ----
|
|
132
|
+
/** 报账号「现在状态」(插件只报它观察到的)。冷却/禁用/错误累计由核心叠加。 */
|
|
133
|
+
status(): SupplierStatusNow
|
|
134
|
+
listModels(force?: boolean): Promise<ModelInfo[]> | ModelInfo[]
|
|
135
|
+
/**
|
|
136
|
+
* 对**单个账号**调一次上游。插件不遍历账号、不管冷却、不写 res——
|
|
137
|
+
* 选号/回退/健康判定全是核心的活。
|
|
138
|
+
*
|
|
139
|
+
* 失败原因通过返回值的 `message` 报(核心测试模型时直接拿它做诊断提示),
|
|
140
|
+
* 插件不必维护「上次失败」的状态。
|
|
141
|
+
*/
|
|
142
|
+
chatOnce(uid: string, lv: string, req: ChatRequest): Promise<ChatOnceResult>
|
|
143
|
+
dispose(): void
|
|
144
|
+
|
|
145
|
+
// ---- 差异化能力(可选) ----
|
|
146
|
+
|
|
147
|
+
generateLoginUrl?(): string | { ok: boolean; error?: string; loginUrl?: string } | Promise<string | { ok: boolean; error?: string; loginUrl?: string }>
|
|
148
|
+
completeLogin?(callbackUrl: string): Promise<{ uid: string; nickname: string }>
|
|
149
|
+
/**
|
|
150
|
+
* 添加 API key 账号(弹窗填名字 + key,而不是走 URL 登录)。有它同样显示连接池。
|
|
151
|
+
*
|
|
152
|
+
* 本插件用它承载 Qoder 的 PAT:上游没有 OAuth,用户粘贴一个 `pt-…` 就是
|
|
153
|
+
* 全部认证材料。
|
|
154
|
+
*/
|
|
155
|
+
addApiKey?(input: { name: string; apiKey: string }): Promise<{
|
|
156
|
+
ok: boolean
|
|
157
|
+
error?: string
|
|
158
|
+
account?: { uid: string; nickname: string }
|
|
159
|
+
}>
|
|
160
|
+
removeLink?(uid: string): Promise<boolean>
|
|
161
|
+
/** 轮询式登录标记:有它 → 面板隐藏「粘贴回调链接」步骤,登录后直接点完成(后台轮询)。 */
|
|
162
|
+
pollLogin?(): boolean
|
|
163
|
+
|
|
164
|
+
/** 单个链接签到。遍历所有链接 + 结果汇总是 dsh-router 核心的活。
|
|
165
|
+
* status: 'ok'(签到成功) / 'already'(今日已签到) / 'error'(失败)。 */
|
|
166
|
+
checkinNow?(uid: string): Promise<{ ok: boolean; status: string; message?: string }>
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type { SupplierAccount }
|