dsh-llm-codebuddy-power 1.0.0 → 1.3.0
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 +22 -22
- package/README.md +84 -66
- package/README.zh.md +84 -66
- package/cordis.patch.yml +7 -7
- package/icon.svg +6 -0
- package/lib/cli-login.js +113 -110
- package/lib/client.js +491 -410
- package/lib/index.js +439 -273
- package/lib/types/adapter.d.ts +7 -1
- package/lib/types/auth-service.d.ts +68 -22
- package/lib/types/cli/login.d.ts +1 -2
- package/lib/types/client/AccountSwitcher.d.ts +1 -2
- package/lib/types/client/CodeBuddySection.d.ts +17 -12
- package/lib/types/client/TurnCreditPill.d.ts +5 -4
- package/lib/types/client/index.d.ts +6 -6
- package/lib/types/client/locales.d.ts +6 -4
- package/lib/types/client/store.d.ts +19 -13
- package/lib/types/client/wire.d.ts +20 -3
- package/lib/types/constants.d.ts +15 -0
- package/lib/types/index.d.ts +44 -22
- package/lib/types/prefs.d.ts +17 -13
- package/lib/types/serialize.d.ts +19 -10
- package/lib/types/session-store.d.ts +1 -1
- package/lib/types/session.d.ts +59 -29
- package/lib/types/settings.d.ts +10 -7
- package/lib/types/storage.d.ts +6 -13
- package/lib/types/usage.d.ts +4 -2
- package/package.json +37 -30
- package/lib/types/client/UsageIndicator.d.ts +0 -21
- package/lib/types/client/poll.d.ts +0 -19
- package/lib/types/credit-store.d.ts +0 -25
- package/lib/types/selection.d.ts +0 -35
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, ToolCallId, contentHasImage, isContextWindowExceededError, isQuotaExceededError, resolveImageAttachmentAccess } from "@deepseek-ai/dsh-llm";
|
|
1
3
|
import { gzipSync } from "node:zlib";
|
|
2
|
-
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, ToolCallId, contentHasImage, isContextWindowExceededError, isQuotaExceededError } from "@deepseek-ai/dsh-llm";
|
|
3
4
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
4
5
|
import { promises } from "node:fs";
|
|
5
6
|
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
@@ -7,7 +8,6 @@ import { dirname } from "node:path";
|
|
|
7
8
|
import { EventSourceParserStream } from "eventsource-parser/stream";
|
|
8
9
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
9
10
|
import { spawn } from "node:child_process";
|
|
10
|
-
import z from "@deepseek-ai/schemastery";
|
|
11
11
|
//#region lib/types/constants.js
|
|
12
12
|
/**
|
|
13
13
|
* CodeBuddy 服务的固定事实。
|
|
@@ -77,8 +77,23 @@ const REQUEST_IMAGE_DIMENSION_LADDER = [
|
|
|
77
77
|
const LOGIN_TIMEOUT_MS = 600 * 1e3;
|
|
78
78
|
/** 等待浏览器登录完成时的轮询间隔,单位 ms。 */
|
|
79
79
|
const LOGIN_POLL_INTERVAL_MS = 1e3;
|
|
80
|
+
/**
|
|
81
|
+
* 握手结算后在宿主表里保留结果的时长,单位 ms。
|
|
82
|
+
*
|
|
83
|
+
* 逾期即回收,但**客户端是过一会儿才来问的**(它按自己的间隔轮询),所以结果不能
|
|
84
|
+
* 一结算就丢:一丢,那次本已成功的登录就再也答不出 `done: true`,客户端只能干等到
|
|
85
|
+
* 十分钟截止。保留窗口只需覆盖一个客户端轮询周期,取一个宽裕值。
|
|
86
|
+
*/
|
|
87
|
+
const LOGIN_RESULT_RETENTION_MS = 60 * 1e3;
|
|
80
88
|
/** 含义为“浏览器登录尚未完成”的服务码。 */
|
|
81
89
|
const AUTH_PENDING_CODE = 11217;
|
|
90
|
+
/**
|
|
91
|
+
* 一次性赠送活动所用的子产品编码。
|
|
92
|
+
*
|
|
93
|
+
* "裂变包"与"拉新权益包"两类赠送活动各有自己的 `PackageCode`,却共用这一个子产品
|
|
94
|
+
* 编码,而可续期主套餐用的是另一个,因此它是区分赠送与主体的稳定依据。
|
|
95
|
+
*/
|
|
96
|
+
const CODEBUDDY_GIFT_SUB_PRODUCT_CODE = "sp_tcaca_codebuddyide_bonus_pack";
|
|
82
97
|
//#endregion
|
|
83
98
|
//#region lib/types/codebuddy.js
|
|
84
99
|
/**
|
|
@@ -525,16 +540,19 @@ function normalizeResetTime(raw) {
|
|
|
525
540
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
526
541
|
}
|
|
527
542
|
/**
|
|
528
|
-
*
|
|
543
|
+
* 判断某个计量窗口是否是一次性赠送额度。
|
|
529
544
|
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
545
|
+
* 依据是子产品编码:{@link CODEBUDDY_GIFT_SUB_PRODUCT_CODE} 由全部赠送活动共用,
|
|
546
|
+
* 而可续期主套餐用另一个编码。套餐显示名不能独立作依据 —— 两类赠送活动的
|
|
547
|
+
* `PackageCode` 不同、显示名也不同,只有一类名字里带“裂变”,另一类叫“拉新权益包”,
|
|
548
|
+
* 按名匹配会把后者错算进主池。显示名与子产品名因此只作兜底,供未来换了子产品编码
|
|
549
|
+
* 却仍在名字里沿用“裂变/赠送”字样的赠送活动使用。
|
|
550
|
+
* @param resource - `Accounts` 数组里的一项。
|
|
551
|
+
* @returns 赠送窗口返回 true。
|
|
535
552
|
*/
|
|
536
|
-
function
|
|
537
|
-
|
|
553
|
+
function isGiftResource(resource) {
|
|
554
|
+
if (string(resource, "SubProductCode") === "sp_tcaca_codebuddyide_bonus_pack") return true;
|
|
555
|
+
return /裂变|赠送/.test(string(resource, "PackageName") ?? "") || /裂变|赠送/.test(string(resource, "SubProductName") ?? "");
|
|
538
556
|
}
|
|
539
557
|
/**
|
|
540
558
|
* 把有可用容量的窗口聚合成一个池(used/limit 求和)。
|
|
@@ -563,13 +581,13 @@ function liveGiftWindows(windows) {
|
|
|
563
581
|
/**
|
|
564
582
|
* 构建个人快照的主池/赠送池划分,以及提示框的赠送明细(最早过期的有效赠送
|
|
565
583
|
* 在前,最多 5 个)。
|
|
566
|
-
* @param windows -
|
|
584
|
+
* @param windows - 已解析的窗口,各自带 {@link UsageWindow.gift} 归属。
|
|
567
585
|
* @returns 该划分,仅当对应池有可用 limit 时才包含 `main`/`gift`。
|
|
568
586
|
*/
|
|
569
587
|
function splitPools(windows) {
|
|
570
588
|
const mainWindows = [];
|
|
571
589
|
const giftWindows = [];
|
|
572
|
-
for (const window of windows) if (
|
|
590
|
+
for (const window of windows) if (window.gift) giftWindows.push(window);
|
|
573
591
|
else mainWindows.push(window);
|
|
574
592
|
const main = poolOf(mainWindows);
|
|
575
593
|
const live = liveGiftWindows(giftWindows);
|
|
@@ -608,14 +626,17 @@ function personalUsage(accounts) {
|
|
|
608
626
|
const name = string(resource, "PackageName") ?? string(resource, "PackageCode") ?? string(resource, "ResourceId") ?? `resource_${index}`;
|
|
609
627
|
const rawReset = string(resource, "CycleEndTime");
|
|
610
628
|
const resetsAt = rawReset === void 0 ? void 0 : normalizeResetTime(rawReset);
|
|
629
|
+
const gift = isGiftResource(resource);
|
|
611
630
|
if (limit <= 0) return {
|
|
612
631
|
name,
|
|
632
|
+
gift,
|
|
613
633
|
...resetsAt === void 0 ? {} : { resetsAt }
|
|
614
634
|
};
|
|
615
635
|
return {
|
|
616
636
|
name,
|
|
617
637
|
used,
|
|
618
638
|
limit,
|
|
639
|
+
gift,
|
|
619
640
|
...resetsAt === void 0 ? {} : { resetsAt }
|
|
620
641
|
};
|
|
621
642
|
});
|
|
@@ -645,6 +666,7 @@ function enterpriseUsage(data) {
|
|
|
645
666
|
name: "enterprise",
|
|
646
667
|
used,
|
|
647
668
|
limit,
|
|
669
|
+
gift: false,
|
|
648
670
|
...reset === void 0 ? {} : { resetsAt: reset }
|
|
649
671
|
};
|
|
650
672
|
const main = poolOf([window]);
|
|
@@ -908,10 +930,14 @@ function isAccountRef(value) {
|
|
|
908
930
|
/**
|
|
909
931
|
* 文件读出的记录是否合法。
|
|
910
932
|
*
|
|
911
|
-
* `
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
933
|
+
* `credit` 与 `accounts` 同为必填:一条记录说不出这一轮扣了多少、用的是哪个账号,
|
|
934
|
+
* 就答不了它存在的唯一问题,与"没有这条记录"没有区别,因此不完整即整条丢弃。引用
|
|
935
|
+
* 还必须能在同一份文件的账号表里解析到 —— 一个悬空引用渲染不出账号,与缺字段是同
|
|
936
|
+
* 一种残缺。
|
|
937
|
+
*
|
|
938
|
+
* `requestId` 则允许缺失:它是后加的字段,已发布的旧版本写下的记录没有它。那些轮次
|
|
939
|
+
* 照常显示积分,只是拿不出请求 id,因此不值得为它丢掉整轮积分。存在时必须是字符串,
|
|
940
|
+
* 否则是损坏内容而不是旧格式。
|
|
915
941
|
* @param value - 文件里的一条轮次记录。
|
|
916
942
|
* @param table - 同一份文件的账号表。
|
|
917
943
|
* @returns 该记录是否可用。
|
|
@@ -919,7 +945,7 @@ function isAccountRef(value) {
|
|
|
919
945
|
function isCreditRecord(value, table) {
|
|
920
946
|
if (typeof value !== "object" || value === null) return false;
|
|
921
947
|
const record = value;
|
|
922
|
-
return typeof record.credit === "number" && Number.isFinite(record.credit) && Array.isArray(record.accounts) && record.accounts.length > 0 && record.accounts.every((key) => typeof key === "string" && isAccountRef(table[key]));
|
|
948
|
+
return typeof record.credit === "number" && Number.isFinite(record.credit) && Array.isArray(record.accounts) && record.accounts.length > 0 && record.accounts.every((key) => typeof key === "string" && isAccountRef(table[key])) && (record.requestId === void 0 || typeof record.requestId === "string");
|
|
923
949
|
}
|
|
924
950
|
/**
|
|
925
951
|
* 读取一个会话的信息。
|
|
@@ -1005,7 +1031,7 @@ async function updateSessionInfo(sessionId, change) {
|
|
|
1005
1031
|
* (`$DSH_HOME/data/plugins/llm-codebuddy-power/`,通过 harness 所用的同一个
|
|
1006
1032
|
* `@deepseek-ai/dsh-home-paths` 解析),而不放在插件包内,因此重装不会让
|
|
1007
1033
|
* 用户退出登录。写入经 [atomic-file.ts](./atomic-file.ts) 做原子替换并按路径串行化:
|
|
1008
|
-
*
|
|
1034
|
+
* 多个账号的令牌刷新、配置页的增删与 CLI 的调用都在改同一份文件,不串行化时
|
|
1009
1035
|
* 两个并发写入会各自基于自己读到的旧内容覆盖对方。残缺文件会让用户手里只有
|
|
1010
1036
|
* 一份读不出的凭据,还无法与“从未登录过”区分开。
|
|
1011
1037
|
*
|
|
@@ -1080,9 +1106,8 @@ function trimEmptyAccountFields(account) {
|
|
|
1080
1106
|
* 把解析出的 JSON 归一化为账号集合。
|
|
1081
1107
|
*
|
|
1082
1108
|
* 同时接受当前形态与仅有一个账号的旧形态(顶层 `auth` + `account`):旧文件
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1085
|
-
* 指针,留着会让“有没有默认账号”与实际情况相反。
|
|
1109
|
+
* 迁移成单元素列表,于是升级不会改变用户下一次请求用的是谁。`lastUid` 指向已不
|
|
1110
|
+
* 存在的账号时丢弃 —— 它只是个指针,留着会让兜底落到一个用不了的账号上。
|
|
1086
1111
|
* @param parsed - 解析出的 JSON 值。
|
|
1087
1112
|
* @returns 归一化后的账号集合。
|
|
1088
1113
|
*/
|
|
@@ -1098,22 +1123,17 @@ function normalizeStorage(parsed) {
|
|
|
1098
1123
|
if (typeof value !== "string") return void 0;
|
|
1099
1124
|
return accounts.some((a) => a.account.uid === value) ? value : void 0;
|
|
1100
1125
|
};
|
|
1101
|
-
const defaultUid = pick(record.defaultUid);
|
|
1102
1126
|
const lastUid = pick(record.lastUid);
|
|
1103
1127
|
return {
|
|
1104
1128
|
accounts,
|
|
1105
|
-
...defaultUid === void 0 ? {} : { defaultUid },
|
|
1106
1129
|
...lastUid === void 0 ? {} : { lastUid }
|
|
1107
1130
|
};
|
|
1108
1131
|
}
|
|
1109
1132
|
if (!isAccountEntry(record)) return emptyStorage();
|
|
1110
|
-
return {
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
}],
|
|
1115
|
-
defaultUid: record.account.uid
|
|
1116
|
-
};
|
|
1133
|
+
return { accounts: [{
|
|
1134
|
+
auth: record.auth,
|
|
1135
|
+
account: trimEmptyAccountFields(record.account)
|
|
1136
|
+
}] };
|
|
1117
1137
|
}
|
|
1118
1138
|
/**
|
|
1119
1139
|
* 读取已存储的账号集合。
|
|
@@ -1153,18 +1173,17 @@ async function updateStorage(change) {
|
|
|
1153
1173
|
* 插入一个账号,或替换同 uid 账号的凭据。
|
|
1154
1174
|
*
|
|
1155
1175
|
* 同 uid 视为重新登录同一个账号:令牌被替换而不是追加成第二个条目,否则
|
|
1156
|
-
*
|
|
1176
|
+
* 配置页会出现两个同名徽章,退出其中一个还会留下另一个。
|
|
1157
1177
|
* @param entry - 刚登录得到的账号。
|
|
1158
1178
|
*/
|
|
1159
1179
|
async function upsertAccount(entry) {
|
|
1160
1180
|
await updateStorage((current) => {
|
|
1161
1181
|
const uid = entry.account.uid;
|
|
1162
1182
|
const accounts = current.accounts.some((a) => a.account.uid === uid) ? current.accounts.map((a) => a.account.uid === uid ? entry : a) : [...current.accounts, entry];
|
|
1163
|
-
const
|
|
1183
|
+
const lastUid = current.lastUid ?? (current.accounts.length === 0 ? uid : void 0);
|
|
1164
1184
|
return {
|
|
1165
1185
|
accounts,
|
|
1166
|
-
...
|
|
1167
|
-
...current.lastUid === void 0 ? {} : { lastUid: current.lastUid }
|
|
1186
|
+
...lastUid === void 0 ? {} : { lastUid }
|
|
1168
1187
|
};
|
|
1169
1188
|
});
|
|
1170
1189
|
}
|
|
@@ -1198,39 +1217,22 @@ async function reorderAccounts(uids) {
|
|
|
1198
1217
|
});
|
|
1199
1218
|
}
|
|
1200
1219
|
/**
|
|
1201
|
-
*
|
|
1220
|
+
* 移除若干账号,并清掉指向它们的“最后一次手动切换”的账号。
|
|
1202
1221
|
* @param uids - 要移除的账号 uid。
|
|
1203
1222
|
*/
|
|
1204
1223
|
async function removeAccounts(uids) {
|
|
1205
1224
|
const removed = new Set(uids);
|
|
1206
1225
|
await updateStorage((current) => {
|
|
1207
1226
|
const accounts = current.accounts.filter((a) => !removed.has(a.account.uid));
|
|
1208
|
-
const
|
|
1209
|
-
const defaultUid = gone(current.defaultUid);
|
|
1210
|
-
const lastUid = gone(current.lastUid);
|
|
1227
|
+
const lastUid = current.lastUid !== void 0 && removed.has(current.lastUid) ? void 0 : current.lastUid;
|
|
1211
1228
|
return {
|
|
1212
1229
|
accounts,
|
|
1213
|
-
...defaultUid === void 0 ? {} : { defaultUid },
|
|
1214
1230
|
...lastUid === void 0 ? {} : { lastUid }
|
|
1215
1231
|
};
|
|
1216
1232
|
});
|
|
1217
1233
|
}
|
|
1218
1234
|
/**
|
|
1219
|
-
*
|
|
1220
|
-
* @param uid - 默认账号的 uid;传 `undefined` 取消默认账号。
|
|
1221
|
-
*/
|
|
1222
|
-
async function setDefaultAccount(uid) {
|
|
1223
|
-
await updateStorage((current) => {
|
|
1224
|
-
const next = {
|
|
1225
|
-
accounts: current.accounts,
|
|
1226
|
-
...current.lastUid === void 0 ? {} : { lastUid: current.lastUid }
|
|
1227
|
-
};
|
|
1228
|
-
if (uid !== void 0 && current.accounts.some((a) => a.account.uid === uid)) next.defaultUid = uid;
|
|
1229
|
-
return next;
|
|
1230
|
-
});
|
|
1231
|
-
}
|
|
1232
|
-
/**
|
|
1233
|
-
* 记下最后一次被手动切换到的账号(没有默认账号时作为兜底)。
|
|
1235
|
+
* 记下最后一次被手动切换到的账号,作为未选用账号的会话的兜底。
|
|
1234
1236
|
* @param uid - 被切换到的账号 uid。
|
|
1235
1237
|
*/
|
|
1236
1238
|
async function setLastAccount(uid) {
|
|
@@ -1265,7 +1267,8 @@ async function storageFreshness() {
|
|
|
1265
1267
|
* 并发调用会互相取到对方的刷新结果,其中一个的 token 永远停留过期。
|
|
1266
1268
|
*
|
|
1267
1269
|
* 账号由会话决定:某个会话显式选了账号就用它,否则顺延到派生它的会话(子代理、
|
|
1268
|
-
* fork)
|
|
1270
|
+
* fork),再落到账号级兜底(最后一次手动切换的账号,其次是列表第一个)。会话在
|
|
1271
|
+
* 第一次真正发请求时把解析到的账号固定下来,此后不再跟随兜底。目录与计量快照
|
|
1269
1272
|
* 也按账号分别缓存,因为不同账号的可用模型与额度本就可以不同。
|
|
1270
1273
|
*
|
|
1271
1274
|
* @module dsh-llm-codebuddy-power/session
|
|
@@ -1310,7 +1313,8 @@ function resolveCreditRecord(record, table) {
|
|
|
1310
1313
|
}
|
|
1311
1314
|
return {
|
|
1312
1315
|
credit: record.credit,
|
|
1313
|
-
accounts
|
|
1316
|
+
accounts,
|
|
1317
|
+
...record.requestId === void 0 ? {} : { requestId: record.requestId }
|
|
1314
1318
|
};
|
|
1315
1319
|
}
|
|
1316
1320
|
/**
|
|
@@ -1380,7 +1384,7 @@ var CodeBuddySession = class {
|
|
|
1380
1384
|
*
|
|
1381
1385
|
* 集合只由**带会话身份的**读取填充(聊天请求、模型面板),并供拿不到会话身份的
|
|
1382
1386
|
* 调用方({@link knownModel}、`listModels`)查询:那些调用方无法知道该用哪个账号
|
|
1383
|
-
*
|
|
1387
|
+
* 的目录,主动读取只能落在账号级兜底上 —— 而当前会话用的可能正是另一个账号。
|
|
1384
1388
|
*/
|
|
1385
1389
|
knownCatalog = /* @__PURE__ */ new Map();
|
|
1386
1390
|
/** 按账号 uid 单飞的目录读取。 */
|
|
@@ -1497,12 +1501,14 @@ var CodeBuddySession = class {
|
|
|
1497
1501
|
const existing = track.credits.get(turn);
|
|
1498
1502
|
const accounts = existing?.accounts ?? [];
|
|
1499
1503
|
const known = accounts.some((entry) => entry.uid === account.uid);
|
|
1504
|
+
const requestId = existing?.requestId ?? track.turnRequestId;
|
|
1500
1505
|
const merged = {
|
|
1501
1506
|
credit: (existing?.credit ?? 0) + credit,
|
|
1502
1507
|
accounts: known ? accounts : [...accounts, {
|
|
1503
1508
|
nickname: account.nickname,
|
|
1504
1509
|
uid: account.uid
|
|
1505
|
-
}]
|
|
1510
|
+
}],
|
|
1511
|
+
requestId
|
|
1506
1512
|
};
|
|
1507
1513
|
track.credits.set(turn, merged);
|
|
1508
1514
|
updateSessionInfo(sessionId, (info) => {
|
|
@@ -1522,7 +1528,8 @@ var CodeBuddySession = class {
|
|
|
1522
1528
|
...info.credits,
|
|
1523
1529
|
[String(turn)]: {
|
|
1524
1530
|
credit: merged.credit,
|
|
1525
|
-
accounts: keys
|
|
1531
|
+
accounts: keys,
|
|
1532
|
+
...merged.requestId === void 0 ? {} : { requestId: merged.requestId }
|
|
1526
1533
|
}
|
|
1527
1534
|
}
|
|
1528
1535
|
};
|
|
@@ -1571,7 +1578,7 @@ var CodeBuddySession = class {
|
|
|
1571
1578
|
/**
|
|
1572
1579
|
* 由当前缓存的各账号目录重建合并目录。
|
|
1573
1580
|
*
|
|
1574
|
-
* 账号退出后的处理必须是"重建"而不是"清空"
|
|
1581
|
+
* 账号退出后的处理必须是"重建"而不是"清空":清空会让官方列表塌回账号级兜底,
|
|
1575
1582
|
* 而其余仍登录账号的目录本来就在手上 —— 继续并着才是"退出后重新合并当前已有的
|
|
1576
1583
|
* 列表"。重建同时确定性地解决同 id 的归属:只剩下仍在的账号参与竞争。
|
|
1577
1584
|
*/
|
|
@@ -1607,7 +1614,7 @@ var CodeBuddySession = class {
|
|
|
1607
1614
|
* 或每记一次“最后切换的账号”都会被当成账号变更,从而丢掉按账号缓存的计量快照并
|
|
1608
1615
|
* 广播一次模型变更 —— 那些都不是账号增删。真的增删时才会丢弃已退出账号的缓存并
|
|
1609
1616
|
* 重建合并目录:退出一个账号后官方列表应当继续并着其余账号已读到的目录,而不是
|
|
1610
|
-
*
|
|
1617
|
+
* 塌回账号级兜底那一个。
|
|
1611
1618
|
*/
|
|
1612
1619
|
async refreshIfChanged() {
|
|
1613
1620
|
const storageKey = await storageFreshness();
|
|
@@ -1629,8 +1636,9 @@ var CodeBuddySession = class {
|
|
|
1629
1636
|
* 该会话自身及其派生祖先显式选用的账号,由近及远。读取某个会话的信息可能要碰盘,
|
|
1630
1637
|
* 因此本方法是异步的。
|
|
1631
1638
|
*
|
|
1632
|
-
* 走到祖先是因为子代理与 fork
|
|
1633
|
-
*
|
|
1639
|
+
* 走到祖先是因为子代理与 fork 出来的会话在**固定之前**还没有自己的账号:子代理
|
|
1640
|
+
* 界面上从来就没有账号按钮,若不沿用父会话就会静默改用账号级兜底,把一次派发记到
|
|
1641
|
+
* 另一个账号头上;fork 有自己的按钮,但它在被切换之前也应当跟着父会话走。
|
|
1634
1642
|
*/
|
|
1635
1643
|
async accountChain(sessionId) {
|
|
1636
1644
|
const chain = [];
|
|
@@ -1647,17 +1655,39 @@ var CodeBuddySession = class {
|
|
|
1647
1655
|
/**
|
|
1648
1656
|
* 该会话此刻生效的账号 uid。
|
|
1649
1657
|
*
|
|
1650
|
-
* 顺序:会话(或其派生祖先)显式选用的账号 →
|
|
1651
|
-
*
|
|
1652
|
-
*
|
|
1658
|
+
* 顺序:会话(或其派生祖先)显式选用的账号 → 最后一次手动切换的账号 → 列表第一个。
|
|
1659
|
+
* 指向已退出账号的选择会被跳过,而不是当成“没有账号”;全部落空时返回
|
|
1660
|
+
* `undefined`,调用方据此报未登录。
|
|
1653
1661
|
*/
|
|
1654
1662
|
async accountUidFor(sessionId) {
|
|
1655
1663
|
if (sessionId !== void 0) {
|
|
1656
1664
|
for (const uid of await this.accountChain(sessionId)) if (this.accountEntry(uid) !== void 0) return uid;
|
|
1657
1665
|
}
|
|
1658
|
-
|
|
1666
|
+
const last = this.storage?.lastUid;
|
|
1667
|
+
if (this.accountEntry(last) !== void 0) return last;
|
|
1659
1668
|
return this.storage?.accounts[0]?.account.uid;
|
|
1660
1669
|
}
|
|
1670
|
+
/**
|
|
1671
|
+
* 把该会话此刻解析到的账号写进它自己的文件,使它从此不再跟随账号级兜底。
|
|
1672
|
+
*
|
|
1673
|
+
* 已有选择就不再动它。那有两种可能,而两种都不该被覆盖:早已固定的会话重写只是
|
|
1674
|
+
* 同一份事实的重复落盘;而记录指向一个已退出账号时,读取侧按"没有选择"处理它,
|
|
1675
|
+
* 磁盘上的记录则只增不删 —— 覆盖掉它会让该账号重新登录后不再回到这个会话。
|
|
1676
|
+
* @param sessionId - dsh 会话身份。
|
|
1677
|
+
* @param uid - 本次请求实际使用的账号 uid。
|
|
1678
|
+
*/
|
|
1679
|
+
async pinAccount(sessionId, uid) {
|
|
1680
|
+
const info = await this.infoOf(sessionId);
|
|
1681
|
+
if (info.uid !== void 0) return;
|
|
1682
|
+
await updateSessionInfo(sessionId, (current) => current.uid === void 0 ? {
|
|
1683
|
+
...current,
|
|
1684
|
+
uid
|
|
1685
|
+
} : current);
|
|
1686
|
+
this.infos.set(sessionId, {
|
|
1687
|
+
...info,
|
|
1688
|
+
uid
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1661
1691
|
identityOf(entry) {
|
|
1662
1692
|
return {
|
|
1663
1693
|
accessToken: entry.auth.accessToken,
|
|
@@ -1686,13 +1716,21 @@ var CodeBuddySession = class {
|
|
|
1686
1716
|
}
|
|
1687
1717
|
/**
|
|
1688
1718
|
* 已存账号条目,在首次使用、失效之后以及文件被外部改动时从磁盘读取。
|
|
1689
|
-
*
|
|
1719
|
+
*
|
|
1720
|
+
* 解析出的账号在返回前固定进该会话自己的文件,因此"这个会话用哪个账号"只在它
|
|
1721
|
+
* 第一次真正发请求时从全局兜底取一次;此后用户别处切换账号不会再改变它。固定
|
|
1722
|
+
* 挂在这里而不是账号解析处(见 {@link accountUidFor}),是因为本方法只被
|
|
1723
|
+
* {@link requestIdentity} 调用,即只有真实请求经过 —— 界面上打开一个空会话所
|
|
1724
|
+
* 触发的用量与目录读取不会落下任何记录。
|
|
1725
|
+
* @param sessionId - 用哪个会话的账号;会话未选用时按账号级兜底解析。
|
|
1690
1726
|
* @throws NotLoggedInError 未存有任何凭据时。
|
|
1691
1727
|
*/
|
|
1692
1728
|
async require(sessionId) {
|
|
1693
1729
|
await this.refreshIfChanged();
|
|
1694
|
-
const
|
|
1730
|
+
const uid = await this.accountUidFor(sessionId);
|
|
1731
|
+
const entry = this.accountEntry(uid);
|
|
1695
1732
|
if (entry === void 0) throw new NotLoggedInError("CodeBuddy is not signed in. Run `dsh plugin --profile web codebuddy-power` to sign in through your browser; no API key is required.");
|
|
1733
|
+
if (sessionId !== void 0 && uid !== void 0) await this.pinAccount(sessionId, uid);
|
|
1696
1734
|
return entry;
|
|
1697
1735
|
}
|
|
1698
1736
|
/**
|
|
@@ -1752,7 +1790,7 @@ var CodeBuddySession = class {
|
|
|
1752
1790
|
* 两者在**同一次**身份解析中一起给出:分开解析会有两次账号选择,而账号是每次请求
|
|
1753
1791
|
* 解析的(用户可在同一步里切换),两次解析可能落到不同账号上 —— 那样记进这一轮的
|
|
1754
1792
|
* 账号名就与实际发出去的请求不符。缩写由落盘侧按同一规则算出,故此处给出完整账号。
|
|
1755
|
-
* @param sessionId -
|
|
1793
|
+
* @param sessionId - 请求所属的会话;决定用哪个账号,缺省时按账号级兜底。
|
|
1756
1794
|
* @returns 身份请求头与发起该请求的账号。
|
|
1757
1795
|
*/
|
|
1758
1796
|
async requestIdentity(sessionId) {
|
|
@@ -1774,19 +1812,16 @@ var CodeBuddySession = class {
|
|
|
1774
1812
|
return (this.storage?.accounts.length ?? 0) > 0;
|
|
1775
1813
|
}
|
|
1776
1814
|
/**
|
|
1777
|
-
*
|
|
1778
|
-
* @returns 账号列表(顺序与磁盘一致)
|
|
1815
|
+
* 已存账号,供配置页与会话切换按钮展示。
|
|
1816
|
+
* @returns 账号列表(顺序与磁盘一致);未登录时列表为空。
|
|
1779
1817
|
*/
|
|
1780
1818
|
async accountList() {
|
|
1781
1819
|
await this.refreshIfChanged();
|
|
1782
|
-
return
|
|
1783
|
-
accounts: (this.storage?.accounts ?? []).map(accountInfo),
|
|
1784
|
-
...this.storage?.defaultUid === void 0 ? {} : { defaultUid: this.storage.defaultUid }
|
|
1785
|
-
};
|
|
1820
|
+
return (this.storage?.accounts ?? []).map(accountInfo);
|
|
1786
1821
|
}
|
|
1787
1822
|
/**
|
|
1788
1823
|
* 某会话此刻生效的账号 uid。
|
|
1789
|
-
* @param sessionId - dsh
|
|
1824
|
+
* @param sessionId - dsh 会话身份;缺省时按账号级兜底。
|
|
1790
1825
|
* @returns 账号 uid;一个账号也没有时为 `undefined`。
|
|
1791
1826
|
*/
|
|
1792
1827
|
async activeUid(sessionId) {
|
|
@@ -1795,6 +1830,10 @@ var CodeBuddySession = class {
|
|
|
1795
1830
|
}
|
|
1796
1831
|
/**
|
|
1797
1832
|
* 把某会话的账号切换为指定账号,并把它记为最后一次手动切换的账号。
|
|
1833
|
+
*
|
|
1834
|
+
* 该会话已有持久记录才写它自己的文件;一个还从未落过盘的新会话只更新账号级的
|
|
1835
|
+
* `lastUid` —— 它可能永远不会被发过第一条消息,为它建文件会在磁盘上留下无人
|
|
1836
|
+
* 认领的残留文件。这类会话在真正发请求时由 {@link require} 固定下来。
|
|
1798
1837
|
* @param sessionId - dsh 会话身份。
|
|
1799
1838
|
* @param uid - 要使用的账号 uid。
|
|
1800
1839
|
* @returns 该 uid 是已存账号时为 true;否则不做任何改动并返回 false。
|
|
@@ -1802,7 +1841,9 @@ var CodeBuddySession = class {
|
|
|
1802
1841
|
async activate(sessionId, uid) {
|
|
1803
1842
|
await this.refreshIfChanged();
|
|
1804
1843
|
if (this.accountEntry(uid) === void 0) return false;
|
|
1844
|
+
await setLastAccount(uid);
|
|
1805
1845
|
const info = await this.infoOf(sessionId);
|
|
1846
|
+
if (info.uid === void 0 && Object.keys(info.credits).length === 0) return true;
|
|
1806
1847
|
await updateSessionInfo(sessionId, (current) => ({
|
|
1807
1848
|
...current,
|
|
1808
1849
|
uid
|
|
@@ -1811,13 +1852,12 @@ var CodeBuddySession = class {
|
|
|
1811
1852
|
...info,
|
|
1812
1853
|
uid
|
|
1813
1854
|
});
|
|
1814
|
-
await setLastAccount(uid);
|
|
1815
1855
|
return true;
|
|
1816
1856
|
}
|
|
1817
1857
|
/**
|
|
1818
1858
|
* CodeBuddy 模型目录,短暂缓存,并在并发读取者之间共享。
|
|
1819
1859
|
* @param signal - 底层读取的可选取消信号。
|
|
1820
|
-
* @param sessionId -
|
|
1860
|
+
* @param sessionId - 用哪个会话的账号;缺省时按账号级兜底解析。
|
|
1821
1861
|
* @returns 目录中的模型,按服务自身的顺序。
|
|
1822
1862
|
*/
|
|
1823
1863
|
async models(signal, sessionId) {
|
|
@@ -1827,7 +1867,7 @@ var CodeBuddySession = class {
|
|
|
1827
1867
|
* 目录及其推广与档位,在同一个 TTL 下按账号一起缓存,并按账号单飞,使列表与
|
|
1828
1868
|
* 面板富化共享同一次读取。
|
|
1829
1869
|
* @param signal - 底层读取的可选取消信号。
|
|
1830
|
-
* @param sessionId -
|
|
1870
|
+
* @param sessionId - 用哪个会话的账号;缺省时按账号级兜底解析。
|
|
1831
1871
|
* @returns 模型、推广与档位。
|
|
1832
1872
|
*/
|
|
1833
1873
|
async catalogData(signal, sessionId) {
|
|
@@ -1873,7 +1913,7 @@ var CodeBuddySession = class {
|
|
|
1873
1913
|
* 从各账号已读到的目录的并集里查一个模型,**不发起任何请求**。
|
|
1874
1914
|
*
|
|
1875
1915
|
* 供拿不到会话身份的调用方使用(适配器的 `resolveModel`):它无法知道该用哪个
|
|
1876
|
-
*
|
|
1916
|
+
* 账号的目录,主动读取只能落在账号级兜底上,而当前会话用的可能正是另一个账号 ——
|
|
1877
1917
|
* 那会把一个账号独有的模型解析成保守的默认能力(上下文窗口、输出上限、图片输入
|
|
1878
1918
|
* 全部按最保守处理)。并集由带会话身份的读取填充,因此这个查询零成本、也不改变
|
|
1879
1919
|
* "谁在读目录"这件事。
|
|
@@ -1898,7 +1938,7 @@ var CodeBuddySession = class {
|
|
|
1898
1938
|
* 目录及其推广/档位;读不到时返回空值 —— 供面板使用的
|
|
1899
1939
|
* {@link catalogData} 建议性读取孪生体。
|
|
1900
1940
|
* @param signal - 可选取消信号。
|
|
1901
|
-
* @param sessionId -
|
|
1941
|
+
* @param sessionId - 用哪个会话的账号;缺省时按账号级兜底解析。
|
|
1902
1942
|
* @returns 目录与活动,或空列表。
|
|
1903
1943
|
*/
|
|
1904
1944
|
async catalogDataOrEmpty(signal, sessionId) {
|
|
@@ -1922,10 +1962,10 @@ var CodeBuddySession = class {
|
|
|
1922
1962
|
/**
|
|
1923
1963
|
* 目录;读不到时返回空列表。
|
|
1924
1964
|
*
|
|
1925
|
-
*
|
|
1965
|
+
* 列出模型是配置页上的浏览动作,所以失败必须降级为“没有可显示的内容”,
|
|
1926
1966
|
* 而不是让页面崩掉。请求路径直接使用 {@link models} 并保留真实失败。
|
|
1927
1967
|
* @param signal - 可选取消信号。
|
|
1928
|
-
* @param sessionId -
|
|
1968
|
+
* @param sessionId - 用哪个会话的账号;缺省时按账号级兜底解析。
|
|
1929
1969
|
* @returns 目录,或空列表。
|
|
1930
1970
|
*/
|
|
1931
1971
|
async modelsOrEmpty(signal, sessionId) {
|
|
@@ -1946,7 +1986,7 @@ var CodeBuddySession = class {
|
|
|
1946
1986
|
* 其他每种失败(传输、解析、刷新过期)都在一条警告之后解析为
|
|
1947
1987
|
* `undefined`。身份通过与聊天请求相同的按账号单飞刷新解析,所以并发的计量
|
|
1948
1988
|
* 读取绝不会消耗两次刷新 token。
|
|
1949
|
-
* @param sessionId -
|
|
1989
|
+
* @param sessionId - 用哪个会话的账号;缺省时按账号级兜底解析。
|
|
1950
1990
|
* @param signal - 可选取消信号。
|
|
1951
1991
|
* @returns 该快照;未存有任何凭据或计量平面不可达时为 `undefined`。
|
|
1952
1992
|
*/
|
|
@@ -1998,12 +2038,12 @@ async function* parseSse(stream, onComment) {
|
|
|
1998
2038
|
* 把 harness 消息序列化为 CodeBuddy(OpenAI 兼容)chat 请求。
|
|
1999
2039
|
*
|
|
2000
2040
|
* 用户文本被拼接,助手文本成为 `content`,工具调用成为
|
|
2001
|
-
* `tool_calls
|
|
2002
|
-
*
|
|
2003
|
-
*
|
|
2004
|
-
* `
|
|
2005
|
-
*
|
|
2006
|
-
*
|
|
2041
|
+
* `tool_calls`,每个工具结果是一条 `role: 'tool'` 的 harness 消息,直接映射为
|
|
2042
|
+
* 同名 wire 条目。对于支持图像的模型,用户消息里的图**不内联**:改为给出一段
|
|
2043
|
+
* 引导模型自行用 `read_image` 读取的提示词(附件服务解析出的只读绝对路径);
|
|
2044
|
+
* 解析不出路径时退回内联,以免模型既拿不到图也拿不到路径。`read_image` 的工具
|
|
2045
|
+
* 结果仍内联图片本身 —— 那是像素交给模型的唯一途径。生成图工具的结果继续走
|
|
2046
|
+
* 纯文本信封。
|
|
2007
2047
|
*
|
|
2008
2048
|
* @module dsh-llm-codebuddy-power/serialize
|
|
2009
2049
|
*/
|
|
@@ -2015,6 +2055,77 @@ function flattenText(blocks) {
|
|
|
2015
2055
|
function imageDataUrl(version) {
|
|
2016
2056
|
return `data:${version.mediaType};base64,${Buffer.from(version.data).toString("base64")}`;
|
|
2017
2057
|
}
|
|
2058
|
+
/** PTC 模式下模型只能直呼 `run_code`,其余工具经生成的 SDK 以 `tools.<name>` 调用。 */
|
|
2059
|
+
const RUN_CODE_TOOL = "run_code";
|
|
2060
|
+
/** PTC 模式 SDK 中读取图片的绑定名。 */
|
|
2061
|
+
const PTC_READ_IMAGE_BINDING = "tools.read_image";
|
|
2062
|
+
/** 产出需要模型看到的图的那个工具:`read_image`。 */
|
|
2063
|
+
const READ_IMAGE_TOOL = "read_image";
|
|
2064
|
+
/**
|
|
2065
|
+
* 本请求中模型能否按提示词自行读取一张图。
|
|
2066
|
+
*
|
|
2067
|
+
* 三种形态分开处理,因为"能不能读"取决于该会话实际暴露的工具,而不只是路径是否
|
|
2068
|
+
* 可解析:
|
|
2069
|
+
* - `'direct'` —— 工具目录里有 `read_image`,可直接调用;标准模式即此。
|
|
2070
|
+
* - `'ptc'` —— 目录里只有 `run_code`(PTC 模式把其余工具放进生成的 SDK,
|
|
2071
|
+
* `read_image` 只是 `tools.read_image` 绑定),提示词须指明经代码调用。
|
|
2072
|
+
* - `'none'` —— 没有可用的读图途径(极简模式只有持久 shell)。此形态不引导,
|
|
2073
|
+
* 因为那会让模型去调一个不存在的工具,比内联更糟;而极简模式的模型尚未形成
|
|
2074
|
+
* "自己不能看图"的想法(标准模式那个想法来自大段系统提示词与 `read_image`
|
|
2075
|
+
* 描述里的 `Requires the current model to accept image input.`),内联即可。
|
|
2076
|
+
* @param tools - 本次请求的 wire 工具目录。
|
|
2077
|
+
* @returns 本请求的读图形态。
|
|
2078
|
+
*/
|
|
2079
|
+
function readImageRoute(tools) {
|
|
2080
|
+
const names = new Set((tools ?? []).map((tool) => tool.name));
|
|
2081
|
+
if (names.has(READ_IMAGE_TOOL)) return "direct";
|
|
2082
|
+
if (names.has(RUN_CODE_TOOL)) return "ptc";
|
|
2083
|
+
return "none";
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
2086
|
+
* 一张用户消息里的图:有可用的读图途径时只给"自行读取"的提示词,不再内联图片;
|
|
2087
|
+
* 否则退回内联。
|
|
2088
|
+
*
|
|
2089
|
+
* 不内联的理由:网关对非原生多模态模型的兼容路径把图转成文字描述,同时在同一条
|
|
2090
|
+
* 链路上附一句"内容已过滤"的错误声明。图随用户消息到达时,模型会采信那句声明并
|
|
2091
|
+
* 否认看图;改由模型自己调用 `read_image` 取回后,图是它自己取回的返回内容,它
|
|
2092
|
+
* 不再采信那句声明。退回内联则是为了不让模型既拿不到图、又被指去一个不存在的工具。
|
|
2093
|
+
* @param ref - 该次出现的持久化附件引用。
|
|
2094
|
+
* @param version - 该图本次请求已解析的版本,仅在退回内联时使用。
|
|
2095
|
+
* @param resolveAccess - 解析该图在当前执行世界中的只读路径。
|
|
2096
|
+
* @param route - 本请求的读图形态。
|
|
2097
|
+
* @returns 该图的读取提示词,或退回内联时的 image_url 部件。
|
|
2098
|
+
*/
|
|
2099
|
+
function userImageParts(ref, version, resolveAccess, route) {
|
|
2100
|
+
const access = resolveAccess?.(ref);
|
|
2101
|
+
if (route === "none" || access === void 0) return [inlineImagePart(version)];
|
|
2102
|
+
return [{
|
|
2103
|
+
type: "text",
|
|
2104
|
+
text: imageReadPrompt(ref, access, route)
|
|
2105
|
+
}];
|
|
2106
|
+
}
|
|
2107
|
+
/** 内联 base64 图部件。工具结果的像素只能这样交给模型。 */
|
|
2108
|
+
function inlineImagePart(version) {
|
|
2109
|
+
return {
|
|
2110
|
+
type: "image_url",
|
|
2111
|
+
image_url: { url: imageDataUrl(version) }
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* 组装引导模型自行读取该图的提示词。
|
|
2116
|
+
*
|
|
2117
|
+
* 只陈述路径与动作:图并未内联,任何形如"已发送的预览版本"的措辞都会让模型把
|
|
2118
|
+
* 这段话当成图的替代说明,而不是去读取它的指令。
|
|
2119
|
+
* @param ref - 该次出现的持久化附件引用,身份取自它。
|
|
2120
|
+
* @param access - 该图在当前执行世界中的只读路径。
|
|
2121
|
+
* @param route - 本请求的读图形态,决定用哪种调用方式。
|
|
2122
|
+
* @returns 面向模型的读取提示词。
|
|
2123
|
+
*/
|
|
2124
|
+
function imageReadPrompt(ref, access, route) {
|
|
2125
|
+
const identity = ref.name === void 0 ? String(ref.attachmentId) : `${JSON.stringify(ref.name)} (${ref.attachmentId})`;
|
|
2126
|
+
const call = route === "ptc" ? `Call it from your code as ${PTC_READ_IMAGE_BINDING}({ file_path: <path> }).` : `Call the ${READ_IMAGE_TOOL} tool with that path to view it.`;
|
|
2127
|
+
return `[Image ${identity} is at the read-only path ${JSON.stringify(access.readonlyPath)}. ${call} This conversation delivers images through that tool, so read the image yourself before answering about it.]`;
|
|
2128
|
+
}
|
|
2018
2129
|
/**
|
|
2019
2130
|
* 在任何文本扁平化可能静默丢弃图像内容之前先拒绝它。
|
|
2020
2131
|
* @param blocks - 消息内容。
|
|
@@ -2026,12 +2137,14 @@ function assertSupportedContent(blocks, supportsImages) {
|
|
|
2026
2137
|
/**
|
|
2027
2138
|
* 序列化一条用户消息中非工具结果的块。纯文本内容保持紧凑的
|
|
2028
2139
|
* 字符串 wire 形式;含图像的内容变为 OpenAI 兼容的
|
|
2029
|
-
*
|
|
2140
|
+
* 部件数组(图可按读图形态换成读取提示词),并保持块顺序。
|
|
2030
2141
|
* @param blocks - 消息中非工具结果的块。
|
|
2031
2142
|
* @param images - 请求中每个图像已解析的请求版本。
|
|
2143
|
+
* @param resolveAccess - 解析单张图在当前执行世界中的只读路径。
|
|
2144
|
+
* @param route - 本请求的读图形态。
|
|
2032
2145
|
* @returns 用户角色的 wire 内容。
|
|
2033
2146
|
*/
|
|
2034
|
-
function userContent(blocks, images) {
|
|
2147
|
+
function userContent(blocks, images, resolveAccess, route) {
|
|
2035
2148
|
if (!contentHasImage(blocks)) return flattenText(blocks);
|
|
2036
2149
|
const parts = [];
|
|
2037
2150
|
let text = "";
|
|
@@ -2052,10 +2165,7 @@ function userContent(blocks, images) {
|
|
|
2052
2165
|
const version = images.get(block.attachment.attachmentId);
|
|
2053
2166
|
if (version === void 0) throw new LlmError(`CodeBuddy request image ${block.attachment.attachmentId} was not prepared.`, "INVALID_REQUEST");
|
|
2054
2167
|
flush();
|
|
2055
|
-
parts.push(
|
|
2056
|
-
type: "image_url",
|
|
2057
|
-
image_url: { url: imageDataUrl(version) }
|
|
2058
|
-
});
|
|
2168
|
+
parts.push(...userImageParts(block.attachment, version, resolveAccess, route));
|
|
2059
2169
|
}
|
|
2060
2170
|
flush();
|
|
2061
2171
|
return parts;
|
|
@@ -2081,16 +2191,24 @@ function serializeAssistant(message) {
|
|
|
2081
2191
|
}
|
|
2082
2192
|
/**
|
|
2083
2193
|
* 按顺序序列化整个对话。
|
|
2194
|
+
*
|
|
2195
|
+
* 工具结果在 harness 里是**独立的 `role: 'tool'` 消息**(`ToolResultMessage`),
|
|
2196
|
+
* 因此这里逐条映射,而不是按块从 user 消息里筛选。
|
|
2197
|
+
* developer 消息由运行时在 `projectToolUpdates` 中剥离(本 adapter 未声明
|
|
2198
|
+
* `toolUpdate`),因此不会到达这里;万一到达即明确拒绝,而不是静默丢弃。
|
|
2084
2199
|
* @param messages - harness 会话。
|
|
2085
2200
|
* @param supportsImages - 所选模型是否声明支持图像输入。
|
|
2086
2201
|
* @param images - 请求中每个图像已解析的请求版本。
|
|
2087
|
-
* @
|
|
2202
|
+
* @param resolveAccess - 解析单张图在当前执行世界中的只读路径。
|
|
2203
|
+
* @param route - 本请求的读图形态。
|
|
2204
|
+
* @returns wire 消息,工具结果各自成为独立条目。
|
|
2088
2205
|
*/
|
|
2089
|
-
function serializeMessages(messages, supportsImages, images = /* @__PURE__ */ new Map()) {
|
|
2206
|
+
function serializeMessages(messages, supportsImages, images = /* @__PURE__ */ new Map(), resolveAccess, route = "none") {
|
|
2090
2207
|
const wire = [];
|
|
2091
2208
|
const toolNames = collectToolNames(messages);
|
|
2092
2209
|
for (const message of messages) {
|
|
2093
2210
|
assertSupportedContent(message.content, supportsImages);
|
|
2211
|
+
if (message.role === "developer") throw new LlmError("CodeBuddy chat does not support developer messages.", "UNSUPPORTED_CONTENT");
|
|
2094
2212
|
if (message.role === "system") {
|
|
2095
2213
|
wire.push({
|
|
2096
2214
|
role: "system",
|
|
@@ -2102,47 +2220,55 @@ function serializeMessages(messages, supportsImages, images = /* @__PURE__ */ ne
|
|
|
2102
2220
|
wire.push(serializeAssistant(message));
|
|
2103
2221
|
continue;
|
|
2104
2222
|
}
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2223
|
+
if (message.role === "tool") {
|
|
2224
|
+
wire.push(serializeToolResult(message, supportsImages, images, toolNames));
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
const routeForMessage = message.source?.kind === "user" ? route : "none";
|
|
2228
|
+
wire.push({
|
|
2108
2229
|
role: "user",
|
|
2109
|
-
content
|
|
2230
|
+
content: userContent(message.content, images, resolveAccess, routeForMessage)
|
|
2110
2231
|
});
|
|
2111
|
-
for (const result of toolResults) {
|
|
2112
|
-
const text = flattenText(result.content);
|
|
2113
|
-
const imageBlocks = result.content.filter((block) => block.type === "image");
|
|
2114
|
-
if (!(supportsImages && imageBlocks.length > 0 && toolNames.get(result.toolCallId) === READ_IMAGE_TOOL)) {
|
|
2115
|
-
wire.push({
|
|
2116
|
-
role: "tool",
|
|
2117
|
-
tool_call_id: result.toolCallId,
|
|
2118
|
-
content: text.length === 0 ? "(no output)" : text
|
|
2119
|
-
});
|
|
2120
|
-
continue;
|
|
2121
|
-
}
|
|
2122
|
-
const parts = [];
|
|
2123
|
-
if (text.length > 0) parts.push({
|
|
2124
|
-
type: "text",
|
|
2125
|
-
text
|
|
2126
|
-
});
|
|
2127
|
-
for (const block of imageBlocks) {
|
|
2128
|
-
const version = images.get(block.attachment.attachmentId);
|
|
2129
|
-
if (version === void 0) throw new LlmError(`CodeBuddy request image ${block.attachment.attachmentId} was not prepared.`, "INVALID_REQUEST");
|
|
2130
|
-
parts.push({
|
|
2131
|
-
type: "image_url",
|
|
2132
|
-
image_url: { url: imageDataUrl(version) }
|
|
2133
|
-
});
|
|
2134
|
-
}
|
|
2135
|
-
wire.push({
|
|
2136
|
-
role: "tool",
|
|
2137
|
-
tool_call_id: result.toolCallId,
|
|
2138
|
-
content: parts
|
|
2139
|
-
});
|
|
2140
|
-
}
|
|
2141
2232
|
}
|
|
2142
2233
|
return wire;
|
|
2143
2234
|
}
|
|
2144
|
-
/**
|
|
2145
|
-
|
|
2235
|
+
/**
|
|
2236
|
+
* 序列化一条工具结果消息。
|
|
2237
|
+
*
|
|
2238
|
+
* 工具结果的图是否发给模型取决于**产出它的工具**:`read_image` 的存在意义就是让
|
|
2239
|
+
* 模型看图,必须发送;`codebuddy-draw` 只需模型知道"图已生成"(与官方 image_gen
|
|
2240
|
+
* 的纯文本信封一致),发图纯属浪费 token。实测确认服务端接受 tool 消息里的
|
|
2241
|
+
* image_url,因此直接放进该条目的 content 数组。
|
|
2242
|
+
* @param message - 工具结果消息。
|
|
2243
|
+
* @param supportsImages - 所选模型是否声明支持图像输入。
|
|
2244
|
+
* @param images - 请求中每个图像已解析的请求版本。
|
|
2245
|
+
* @param toolNames - toolCallId → 工具名 的索引。
|
|
2246
|
+
* @returns 该工具结果的 wire 条目。
|
|
2247
|
+
*/
|
|
2248
|
+
function serializeToolResult(message, supportsImages, images, toolNames) {
|
|
2249
|
+
const text = flattenText(message.content);
|
|
2250
|
+
const imageBlocks = message.content.filter((block) => block.type === "image");
|
|
2251
|
+
if (!(supportsImages && imageBlocks.length > 0 && toolNames.get(message.toolCallId) === READ_IMAGE_TOOL)) return {
|
|
2252
|
+
role: "tool",
|
|
2253
|
+
tool_call_id: message.toolCallId,
|
|
2254
|
+
content: text.length === 0 ? "(no output)" : text
|
|
2255
|
+
};
|
|
2256
|
+
const parts = [];
|
|
2257
|
+
if (text.length > 0) parts.push({
|
|
2258
|
+
type: "text",
|
|
2259
|
+
text
|
|
2260
|
+
});
|
|
2261
|
+
for (const block of imageBlocks) {
|
|
2262
|
+
const version = images.get(block.attachment.attachmentId);
|
|
2263
|
+
if (version === void 0) throw new LlmError(`CodeBuddy request image ${block.attachment.attachmentId} was not prepared.`, "INVALID_REQUEST");
|
|
2264
|
+
parts.push(inlineImagePart(version));
|
|
2265
|
+
}
|
|
2266
|
+
return {
|
|
2267
|
+
role: "tool",
|
|
2268
|
+
tool_call_id: message.toolCallId,
|
|
2269
|
+
content: parts
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2146
2272
|
/**
|
|
2147
2273
|
* 建立 toolCallId → 工具名 的索引。
|
|
2148
2274
|
*
|
|
@@ -2166,15 +2292,17 @@ function collectToolNames(messages) {
|
|
|
2166
2292
|
* @param options - 组装好的 harness 请求。
|
|
2167
2293
|
* @param supportsImages - 所选模型是否声明支持图像输入。
|
|
2168
2294
|
* @param images - 请求中每个图像已解析的请求版本。
|
|
2295
|
+
* @param resolveAccess - 解析单张图在当前执行世界中的只读路径。
|
|
2296
|
+
* @param route - 本请求的读图形态,由工具目录决定;写入前由调用方计算一次。
|
|
2169
2297
|
* @returns 请求体。
|
|
2170
2298
|
*/
|
|
2171
|
-
function serializeRequest(options, supportsImages, images = /* @__PURE__ */ new Map()) {
|
|
2299
|
+
function serializeRequest(options, supportsImages, images = /* @__PURE__ */ new Map(), resolveAccess) {
|
|
2172
2300
|
const messages = [];
|
|
2173
2301
|
if (options.system !== void 0) messages.push({
|
|
2174
2302
|
role: "system",
|
|
2175
2303
|
content: options.system
|
|
2176
2304
|
});
|
|
2177
|
-
messages.push(...serializeMessages(options.messages, supportsImages, images));
|
|
2305
|
+
messages.push(...serializeMessages(options.messages, supportsImages, images, resolveAccess, readImageRoute(options.tools)));
|
|
2178
2306
|
const tools = options.tools?.map((tool) => ({
|
|
2179
2307
|
type: "function",
|
|
2180
2308
|
function: {
|
|
@@ -2518,48 +2646,45 @@ function requestId(headers) {
|
|
|
2518
2646
|
const value = headers.get("x-request-id") ?? headers.get("x-requestid");
|
|
2519
2647
|
return value === null || value.length === 0 ? void 0 : ProviderRequestId(value);
|
|
2520
2648
|
}
|
|
2521
|
-
/**
|
|
2649
|
+
/** 收集单条消息内容中的全部图像引用。 */
|
|
2522
2650
|
function collectImageRefs(content, refs) {
|
|
2523
2651
|
for (const block of content) if (block.type === "image") refs.set(block.attachment.attachmentId, block.attachment);
|
|
2524
|
-
else if (block.type === "tool-result") collectImageRefs(block.content, refs);
|
|
2525
2652
|
}
|
|
2526
2653
|
/**
|
|
2527
|
-
* 把"长边不超过 longEdge"
|
|
2528
|
-
*
|
|
2529
|
-
* 请求投影只接受总像素上限(`ImageRequestPolicy.maxPixels`),没有长边字段,
|
|
2530
|
-
* 而总像素无法约束长边:8000×460 仅 368 万像素,长边却达 8000。投影按
|
|
2531
|
-
* `sqrt(maxPixels / (width * height))` 等比缩放,因此令
|
|
2532
|
-
* `maxPixels = longEdge² × (短边 / 长边)` 时缩放系数恰为 `longEdge / 长边`,
|
|
2533
|
-
* 结果正好把长边压到该档。
|
|
2654
|
+
* 把"长边不超过 longEdge"折算为该图的目标尺寸。
|
|
2534
2655
|
*
|
|
2535
|
-
*
|
|
2536
|
-
*
|
|
2537
|
-
*
|
|
2538
|
-
* 面积的作用是让**预算值在各档之间重复**,从而被逐档重试的去重跳过,避免
|
|
2539
|
-
* 对同一尺寸重复编码。
|
|
2656
|
+
* 请求投影接受的是目标宽高(`ImageRequestTarget`),因此长边档位直接折算成一对
|
|
2657
|
+
* 宽高,而不必再换算成总像素预算。目标大于源图时投影保持源图尺寸(不放大),
|
|
2658
|
+
* 所以长边本就在档内时直接给源图宽高。
|
|
2540
2659
|
*
|
|
2541
2660
|
* 宽高由会话格式的载荷校验强制为正整数(`payload-validation.ts` 的
|
|
2542
2661
|
* `imageAttachmentValue`),故此处不做防御检查。
|
|
2543
2662
|
* @param width - 原图宽度(像素)。
|
|
2544
2663
|
* @param height - 原图高度(像素)。
|
|
2545
2664
|
* @param longEdge - 本档的长边上限(像素)。
|
|
2546
|
-
* @returns
|
|
2665
|
+
* @returns 该图在该档下的目标宽高。
|
|
2547
2666
|
*/
|
|
2548
|
-
function
|
|
2667
|
+
function dimensionsForLongEdge(width, height, longEdge) {
|
|
2549
2668
|
const source = Math.max(width, height);
|
|
2550
|
-
if (source <= longEdge) return
|
|
2669
|
+
if (source <= longEdge) return {
|
|
2670
|
+
width,
|
|
2671
|
+
height
|
|
2672
|
+
};
|
|
2551
2673
|
const scale = longEdge / source;
|
|
2552
|
-
return
|
|
2674
|
+
return {
|
|
2675
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
2676
|
+
height: Math.max(1, Math.round(height * scale))
|
|
2677
|
+
};
|
|
2553
2678
|
}
|
|
2554
2679
|
/**
|
|
2555
2680
|
* 解析一张图的请求版本:源图未超字节目标时原样复用,超过时才逐档收紧长边。
|
|
2556
2681
|
*
|
|
2557
|
-
*
|
|
2558
|
-
*
|
|
2559
|
-
*
|
|
2682
|
+
* 未超目标的一支不做任何转换:目标取原图宽高,投影因此不缩放,附件服务命中其
|
|
2683
|
+
* 直通分支返回原始字节,既不重编码也不写请求缓存 —— 降到 1920 会白白丢掉分辨率
|
|
2684
|
+
* 与画质,而模型侧的约束只有字节目标。
|
|
2560
2685
|
*
|
|
2561
2686
|
* 超过目标的一支才逐档收紧长边。附件服务的编码阶梯只降质量、不降尺寸,单靠
|
|
2562
|
-
*
|
|
2687
|
+
* 一档无法保证落到字节目标,故需要换更小的长边重来;尺寸相同的档位会被跳过。
|
|
2563
2688
|
* @param ref - 持久化附件引用。
|
|
2564
2689
|
* @param attachments - 已挂载的附件服务。
|
|
2565
2690
|
* @param signal - 本次生成的取消信号。
|
|
@@ -2568,17 +2693,19 @@ function pixelBudgetForLongEdge(width, height, longEdge) {
|
|
|
2568
2693
|
async function requestImageVersion(ref, attachments, signal) {
|
|
2569
2694
|
const maxBytes = REQUEST_IMAGE_MAX_BYTES;
|
|
2570
2695
|
if (ref.bytes <= maxBytes) return attachments.readImageRequest(ref, {
|
|
2571
|
-
|
|
2696
|
+
width: ref.width,
|
|
2697
|
+
height: ref.height,
|
|
2572
2698
|
maxBytes
|
|
2573
2699
|
}, signal);
|
|
2574
2700
|
const tried = /* @__PURE__ */ new Set();
|
|
2575
2701
|
let smallest;
|
|
2576
2702
|
for (const longEdge of REQUEST_IMAGE_DIMENSION_LADDER) {
|
|
2577
|
-
const
|
|
2578
|
-
|
|
2579
|
-
tried.
|
|
2703
|
+
const size = dimensionsForLongEdge(ref.width, ref.height, longEdge);
|
|
2704
|
+
const key = `${size.width}x${size.height}`;
|
|
2705
|
+
if (tried.has(key)) continue;
|
|
2706
|
+
tried.add(key);
|
|
2580
2707
|
const version = await attachments.readImageRequest(ref, {
|
|
2581
|
-
|
|
2708
|
+
...size,
|
|
2582
2709
|
maxBytes
|
|
2583
2710
|
}, signal);
|
|
2584
2711
|
if (version.bytes <= maxBytes) return version;
|
|
@@ -2588,7 +2715,7 @@ async function requestImageVersion(ref, attachments, signal) {
|
|
|
2588
2715
|
}
|
|
2589
2716
|
/**
|
|
2590
2717
|
* 在所选模型接受图像输入时,解析会话所携带每张图像的请求版本。持久化 harness 以引用
|
|
2591
|
-
*
|
|
2718
|
+
* 方式附加;请求字节由附件服务按逐图的目标尺寸与字节预算派生,与官方 DeepSeek 适配器
|
|
2592
2719
|
* 的准备方式一致。
|
|
2593
2720
|
* @param messages - 等待序列化的 harness 消息。
|
|
2594
2721
|
* @param attachments - 持久化附件服务;未挂载时为 undefined。
|
|
@@ -2738,7 +2865,8 @@ var CodeBuddyAdapter = class extends LlmAdapter {
|
|
|
2738
2865
|
const entry = (await this.config.session.modelsOrEmpty(options.signal, sessionId)).find((candidate) => candidate.id === options.model);
|
|
2739
2866
|
const supportsImages = entry?.supportsImages === true;
|
|
2740
2867
|
if (options.tools !== void 0 && options.tools.length > 0 && entry?.supportsToolCall === false) throw new LlmError(`CodeBuddy model "${options.model}" does not support tool calls`, "UNSUPPORTED_OPTION");
|
|
2741
|
-
const
|
|
2868
|
+
const attachments = this.config.resolveAttachments();
|
|
2869
|
+
const body = serializeRequest(options, supportsImages, supportsImages ? await prepareRequestImages(options.messages, attachments, options.signal) : /* @__PURE__ */ new Map(), attachments === void 0 || this.config.resolveImageAccess === void 0 ? void 0 : (ref) => this.config.resolveImageAccess?.(attachments, ref));
|
|
2742
2870
|
const payload = gzipSync(Buffer.from(JSON.stringify(body), "utf8"));
|
|
2743
2871
|
const messageId = compactUuid();
|
|
2744
2872
|
const conversationId = options.sessionId === void 0 ? compactUuid() : this.config.session.conversationIdOf(String(options.sessionId));
|
|
@@ -3026,8 +3154,9 @@ function endpointOf(channel, pathname) {
|
|
|
3026
3154
|
* @param res - 本路由在整个生命周期内持有的响应。
|
|
3027
3155
|
*/
|
|
3028
3156
|
async function serveChannel(rpcCtx, channel, handler, req, res) {
|
|
3029
|
-
const
|
|
3030
|
-
if (rejection
|
|
3157
|
+
const admission = rpcCtx.connection.admit(req);
|
|
3158
|
+
if ("rejection" in admission) {
|
|
3159
|
+
const rejection = admission.rejection;
|
|
3031
3160
|
res.writeHead(rejection);
|
|
3032
3161
|
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
3033
3162
|
return;
|
|
@@ -3085,7 +3214,7 @@ async function serveChannel(rpcCtx, channel, handler, req, res) {
|
|
|
3085
3214
|
res.on("close", () => {
|
|
3086
3215
|
controller.abort();
|
|
3087
3216
|
});
|
|
3088
|
-
const result = await handler(endpoint, message.payload, controller.signal);
|
|
3217
|
+
const result = await handler(endpoint, message.payload, controller.signal, admission.peer);
|
|
3089
3218
|
writeJson(res, 200, serverResponse(message.rpcId, result));
|
|
3090
3219
|
} catch (error) {
|
|
3091
3220
|
res.writeHead(500);
|
|
@@ -3451,10 +3580,11 @@ function enrichModel(model, promotions, tiers, withOverrides) {
|
|
|
3451
3580
|
/**
|
|
3452
3581
|
* 通过私有 RPC 通道暴露给 Web 客户端的宿主侧 OAuth 服务。
|
|
3453
3582
|
*
|
|
3454
|
-
* 浏览器登录是长时操作(要等待用户完成登录)
|
|
3455
|
-
* `startLogin` 生成握手并返回用户必须打开的 URL,`pollLogin`
|
|
3583
|
+
* 浏览器登录是长时操作(要等待用户完成登录),因此拆成三个 RPC endpoint:
|
|
3584
|
+
* `startLogin` 生成握手并返回用户必须打开的 URL,`pollLogin` 检查该握手是否已完成,
|
|
3585
|
+
* `pendingLogin` 报出仍在轮询的握手(浏览器刷新后据此接回);
|
|
3456
3586
|
* 一次完成的登录会**追加**一个账号,而不是取代已有的。`accounts`、`activate`、
|
|
3457
|
-
* `
|
|
3587
|
+
* `logout`、`reorder` 是配置页与会话账号按钮其余时间驱动的读写组合。
|
|
3458
3588
|
*
|
|
3459
3589
|
* @module dsh-llm-codebuddy-power/auth-service
|
|
3460
3590
|
*/
|
|
@@ -3496,6 +3626,7 @@ const CREDIT_UID_DISPLAY_CHARS = 8;
|
|
|
3496
3626
|
*
|
|
3497
3627
|
* uid 在**呈现边界**才截断:存储与解析都保留完整值,截断只影响这一个提示行。把它放在
|
|
3498
3628
|
* 这里而不是存储层,是因为存储层丢掉的信息无法再恢复,而显示层的取舍可以随时改。
|
|
3629
|
+
* 请求 id 不截断,因为它要整个被复制出去。
|
|
3499
3630
|
* @param record - 已按账号表展开的轮次记录。
|
|
3500
3631
|
* @returns 对客户端安全的投影。
|
|
3501
3632
|
*/
|
|
@@ -3505,7 +3636,8 @@ function projectCreditRecord(record) {
|
|
|
3505
3636
|
accounts: record.accounts.map((account) => ({
|
|
3506
3637
|
nickname: account.nickname,
|
|
3507
3638
|
uidPrefix: account.uid.slice(0, CREDIT_UID_DISPLAY_CHARS)
|
|
3508
|
-
}))
|
|
3639
|
+
})),
|
|
3640
|
+
...record.requestId === void 0 ? {} : { requestId: record.requestId }
|
|
3509
3641
|
};
|
|
3510
3642
|
}
|
|
3511
3643
|
/**
|
|
@@ -3525,13 +3657,18 @@ function projectWindow(window) {
|
|
|
3525
3657
|
/**
|
|
3526
3658
|
* CodeBuddy 认证 RPC 服务。
|
|
3527
3659
|
*
|
|
3528
|
-
* 握手由 `startLogin`
|
|
3529
|
-
* `CodeBuddySession` 在下一个请求中取用 → 因此通过 UI
|
|
3530
|
-
*
|
|
3660
|
+
* 握手由 `startLogin` 发起并**脱手**开跑,客户端经 `pollLogin` 反复查问它是否已结算,
|
|
3661
|
+
* 其账号由适配器的 `CodeBuddySession` 在下一个请求中取用 → 因此通过 UI 完成的登录
|
|
3662
|
+
* 无需重启即可到达运行中的 harness。`logout` 移除指定账号并使会话缓存失效。
|
|
3531
3663
|
*/
|
|
3532
3664
|
var CodeBuddyAuthService = class {
|
|
3533
3665
|
session;
|
|
3534
|
-
/**
|
|
3666
|
+
/**
|
|
3667
|
+
* 按 state id 索引的握手。
|
|
3668
|
+
*
|
|
3669
|
+
* 同时存放进行中的与**刚结算、尚在保留窗口内**的条目 —— 后者供客户端取回结果,
|
|
3670
|
+
* 由 `settle()` 安排回收,因此本表不是"进行中握手"的集合。
|
|
3671
|
+
*/
|
|
3535
3672
|
pending = /* @__PURE__ */ new Map();
|
|
3536
3673
|
constructor(ctx, session) {
|
|
3537
3674
|
this.session = session;
|
|
@@ -3542,14 +3679,11 @@ var CodeBuddyAuthService = class {
|
|
|
3542
3679
|
switch (endpoint) {
|
|
3543
3680
|
case "accounts": return ok(await this.accounts());
|
|
3544
3681
|
case "startLogin": return ok(await this.startLogin());
|
|
3545
|
-
case "pollLogin": return ok(
|
|
3682
|
+
case "pollLogin": return ok(this.pollLogin(stringField(payload, "state")));
|
|
3683
|
+
case "pendingLogin": return ok(this.pendingLogin());
|
|
3546
3684
|
case "cancelLogin": return this.cancelLogin(stringField(payload, "state")) ? ok(void 0) : err("bad-request", "a login handshake id is required");
|
|
3547
3685
|
case "sessionAccount": return ok(await this.sessionAccount(stringField(payload, "sessionId")));
|
|
3548
3686
|
case "activate": return await this.activate(stringField(payload, "sessionId"), stringField(payload, "uid")) ? ok(void 0) : err("unknown-account", "the requested account is not stored");
|
|
3549
|
-
case "setDefault": {
|
|
3550
|
-
const uid = stringField(payload, "uid");
|
|
3551
|
-
return await this.setDefault(uid.length === 0 ? void 0 : uid) ? ok(void 0) : err("unknown-account", "the requested account is not stored");
|
|
3552
|
-
}
|
|
3553
3687
|
case "logout":
|
|
3554
3688
|
await this.logout(stringListField(payload, "uids"));
|
|
3555
3689
|
return ok(void 0);
|
|
@@ -3572,46 +3706,79 @@ var CodeBuddyAuthService = class {
|
|
|
3572
3706
|
}
|
|
3573
3707
|
}
|
|
3574
3708
|
/**
|
|
3575
|
-
*
|
|
3709
|
+
* 读取已存账号,不要求必须存在。
|
|
3576
3710
|
* @returns 账号集合;未存任何内容时为空列表。
|
|
3577
3711
|
*/
|
|
3578
3712
|
async accounts() {
|
|
3579
3713
|
if (this.session === void 0) return { accounts: [] };
|
|
3580
|
-
return this.session.accountList();
|
|
3714
|
+
return { accounts: await this.session.accountList() };
|
|
3581
3715
|
}
|
|
3582
3716
|
/**
|
|
3583
3717
|
* 发起一次浏览器登录握手。
|
|
3584
|
-
* @returns 用户必须打开的 URL
|
|
3718
|
+
* @returns 用户必须打开的 URL,以及该握手的 id 与发起时刻。
|
|
3585
3719
|
*/
|
|
3586
3720
|
async startLogin() {
|
|
3587
3721
|
const handshake = await requestAuthState();
|
|
3588
3722
|
const controller = new AbortController();
|
|
3723
|
+
const startedAt = Date.now();
|
|
3589
3724
|
const pending = {
|
|
3590
3725
|
state: handshake.state,
|
|
3591
|
-
|
|
3726
|
+
startedAt,
|
|
3727
|
+
account: void 0,
|
|
3728
|
+
settled: false,
|
|
3592
3729
|
controller
|
|
3593
3730
|
};
|
|
3594
3731
|
this.pending.set(handshake.state, pending);
|
|
3595
|
-
|
|
3596
|
-
if (this.pending.get(handshake.state) === pending) this.pending.delete(handshake.state);
|
|
3597
|
-
});
|
|
3732
|
+
this.runLogin(handshake.state, controller.signal).catch(() => {});
|
|
3598
3733
|
return {
|
|
3599
3734
|
authUrl: handshake.authUrl,
|
|
3600
|
-
state: handshake.state
|
|
3735
|
+
state: handshake.state,
|
|
3736
|
+
startedAt
|
|
3737
|
+
};
|
|
3738
|
+
}
|
|
3739
|
+
/**
|
|
3740
|
+
* 仍在进行的握手。
|
|
3741
|
+
*
|
|
3742
|
+
* 客户端的「登录中」状态只活在浏览器内存里,刷新即丢,而本表里的握手仍在轮询,
|
|
3743
|
+
* 浏览器重新挂载后据此接回(否则界面显示可再次登录,而宿主那边已有一次握手在等,
|
|
3744
|
+
* 用户点了就会平白多出第二次)。
|
|
3745
|
+
*
|
|
3746
|
+
* **只报尚未结算的握手**:已结算的条目会在表里多留一会儿供客户端取结果,若把它们
|
|
3747
|
+
* 也算作"进行中",刷新后接回的就是一次已经结束(甚至已经失败)的登录,界面会重新
|
|
3748
|
+
* 显示「登录中…」并空转到截止时间。
|
|
3749
|
+
*
|
|
3750
|
+
* 同时存在多次时给出最近发起的那次:客户端的轮询、取消与超时都只针对一个握手,
|
|
3751
|
+
* 而最晚发起的那次正是用户此刻在等的那次。
|
|
3752
|
+
* @returns 该握手;没有进行中的登录时缺省。
|
|
3753
|
+
*/
|
|
3754
|
+
pendingLogin() {
|
|
3755
|
+
let latest;
|
|
3756
|
+
for (const pending of this.pending.values()) {
|
|
3757
|
+
if (pending.settled) continue;
|
|
3758
|
+
if (latest === void 0 || pending.startedAt > latest.startedAt) latest = pending;
|
|
3759
|
+
}
|
|
3760
|
+
return latest === void 0 ? void 0 : {
|
|
3761
|
+
state: latest.state,
|
|
3762
|
+
startedAt: latest.startedAt
|
|
3601
3763
|
};
|
|
3602
3764
|
}
|
|
3603
3765
|
/**
|
|
3604
3766
|
* 检查已发起的握手是否完成。
|
|
3767
|
+
*
|
|
3768
|
+
* **立即返回**,不等待后台握手:`runLogin` 可能还要跑满十分钟,若在这里 await 它,
|
|
3769
|
+
* 浏览器的一次请求就会在整个登录窗口内停在待处理,占住一条连接不放;而客户端是
|
|
3770
|
+
* 按自己的间隔反复来问的,让它每次都立刻拿到"还没好"才是这条 endpoint 的职责。
|
|
3605
3771
|
* @param state - 来自 `startLogin` 的握手 id。
|
|
3606
3772
|
* @returns 登录是否完成、账号是否已持久化。
|
|
3607
3773
|
*/
|
|
3608
|
-
|
|
3774
|
+
pollLogin(state) {
|
|
3609
3775
|
const pending = this.pending.get(state);
|
|
3610
3776
|
if (pending === void 0) return { done: false };
|
|
3611
|
-
|
|
3777
|
+
if (!pending.settled) return { done: false };
|
|
3778
|
+
const account = pending.account;
|
|
3612
3779
|
return {
|
|
3613
|
-
done:
|
|
3614
|
-
...
|
|
3780
|
+
done: account !== void 0,
|
|
3781
|
+
...account === void 0 ? {} : { nickname: account.account.nickname }
|
|
3615
3782
|
};
|
|
3616
3783
|
}
|
|
3617
3784
|
/**
|
|
@@ -3652,19 +3819,6 @@ var CodeBuddyAuthService = class {
|
|
|
3652
3819
|
return true;
|
|
3653
3820
|
}
|
|
3654
3821
|
/**
|
|
3655
|
-
* 指定或取消默认账号。
|
|
3656
|
-
* @param uid - 默认账号 uid;`undefined` 表示取消默认账号。
|
|
3657
|
-
* @returns 指定时该账号已存、或取消成功时为 true。
|
|
3658
|
-
*/
|
|
3659
|
-
async setDefault(uid) {
|
|
3660
|
-
if (uid !== void 0) {
|
|
3661
|
-
if (!(await this.accounts()).accounts.some((account) => account.uid === uid)) return false;
|
|
3662
|
-
}
|
|
3663
|
-
await setDefaultAccount(uid);
|
|
3664
|
-
this.session?.invalidate();
|
|
3665
|
-
return true;
|
|
3666
|
-
}
|
|
3667
|
-
/**
|
|
3668
3822
|
* 移除若干账号。
|
|
3669
3823
|
* @param uids - 要移除的账号 uid。
|
|
3670
3824
|
*/
|
|
@@ -3676,7 +3830,7 @@ var CodeBuddyAuthService = class {
|
|
|
3676
3830
|
/**
|
|
3677
3831
|
* 按给定顺序重排账号。
|
|
3678
3832
|
*
|
|
3679
|
-
*
|
|
3833
|
+
* 顺序即展示顺序,也是没有手动切换记录时的兜底账号,因此这里真的
|
|
3680
3834
|
* 改变"用谁"。重排会让输入区控件组读到的账号集合顺序变化,故同样使会话缓存失效。
|
|
3681
3835
|
* @param uids - 目标顺序上的账号 uid。
|
|
3682
3836
|
*/
|
|
@@ -3714,27 +3868,49 @@ var CodeBuddyAuthService = class {
|
|
|
3714
3868
|
};
|
|
3715
3869
|
}
|
|
3716
3870
|
/**
|
|
3717
|
-
*
|
|
3871
|
+
* 把一次握手推进到持久化的账号,并把结果写回 {@link pending} 里对应条目。
|
|
3718
3872
|
*
|
|
3719
3873
|
* 复用 `buildAccount`,使落盘结构与 CLI 登录完全一致。
|
|
3720
|
-
*
|
|
3874
|
+
* 任何失败都记为"结算为失败",让客户端的轮询得到 `done: false`,
|
|
3721
3875
|
* 从而可以从 `startLogin` 重试。
|
|
3876
|
+
*
|
|
3877
|
+
* 本方法由 `startLogin` **脱手**调用,`pollLogin` 不 await 它;因此它自己不返回
|
|
3878
|
+
* 结果,只负责把结果写进条目并安排回收。
|
|
3722
3879
|
*/
|
|
3723
3880
|
async runLogin(state, signal) {
|
|
3881
|
+
let entry;
|
|
3724
3882
|
try {
|
|
3725
3883
|
const token = await pollAuthToken(state, signal);
|
|
3726
|
-
if (token
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3884
|
+
if (token !== void 0) {
|
|
3885
|
+
entry = buildAccount(token, await getLoginAccount(state, token.accessToken, token.domain));
|
|
3886
|
+
await upsertAccount(entry);
|
|
3887
|
+
this.session?.invalidate();
|
|
3888
|
+
}
|
|
3731
3889
|
} catch {
|
|
3732
|
-
|
|
3890
|
+
entry = void 0;
|
|
3733
3891
|
}
|
|
3892
|
+
this.settle(state, entry);
|
|
3893
|
+
}
|
|
3894
|
+
/**
|
|
3895
|
+
* 记录一次握手的结局,并在保留窗口之后回收该条目。
|
|
3896
|
+
*
|
|
3897
|
+
* 回收之所以要延迟:客户端是按自己的间隔来问结果的,一结算就删会让那次本已成功的
|
|
3898
|
+
* 登录再也答不出 `done: true`。取消过的握手已不在表里,这里的写入自然丢弃。
|
|
3899
|
+
* @param state - 握手 id。
|
|
3900
|
+
* @param account - 已落盘的账号;失败或取消时为 `undefined`。
|
|
3901
|
+
*/
|
|
3902
|
+
settle(state, account) {
|
|
3903
|
+
const pending = this.pending.get(state);
|
|
3904
|
+
if (pending === void 0) return;
|
|
3905
|
+
pending.settled = true;
|
|
3906
|
+
pending.account = account;
|
|
3907
|
+
setTimeout(() => {
|
|
3908
|
+
if (this.pending.get(state) === pending) this.pending.delete(state);
|
|
3909
|
+
}, LOGIN_RESULT_RETENTION_MS).unref();
|
|
3734
3910
|
}
|
|
3735
3911
|
/**
|
|
3736
3912
|
* 供客户端面板使用的增强版 CodeBuddy 目录:应用了促销/等级展示信息与覆盖配置的模型。
|
|
3737
|
-
* @param sessionId -
|
|
3913
|
+
* @param sessionId - 用哪个会话的账号读目录;为空时按账号级兜底。
|
|
3738
3914
|
* @returns 增强结果;会话不可用或该账号没有目录时为空行。
|
|
3739
3915
|
*/
|
|
3740
3916
|
async enrichedModels(sessionId) {
|
|
@@ -3774,21 +3950,6 @@ var CodeBuddyAuthService = class {
|
|
|
3774
3950
|
};
|
|
3775
3951
|
//#endregion
|
|
3776
3952
|
//#region lib/types/prefs.js
|
|
3777
|
-
/**
|
|
3778
|
-
* CodeBuddy 界面偏好的命名空间契约,Host 与 Client 两面共享。
|
|
3779
|
-
*
|
|
3780
|
-
* 这些偏好是**插件自己的设置命名空间**里的一段:`Config` 提供部署级 base,
|
|
3781
|
-
* 用户在 Web 设置页的写入落在 harness 的用户设置文档里,因此换浏览器、重启
|
|
3782
|
-
* harness 后仍然生效 —— 其中「极简模式画图工具」还会被宿主读取来决定
|
|
3783
|
-
* `codebuddy-draw` 对模型是否可见,存在浏览器里会让同一次部署因浏览器而异。
|
|
3784
|
-
*
|
|
3785
|
-
* 本文件必须保持零依赖:两面都会编译它,而 Client 面若因此引入 schemastery
|
|
3786
|
-
* 就会把它打进浏览器产物(校验 schema 是宿主面的事,见 `./settings.ts`)。
|
|
3787
|
-
*
|
|
3788
|
-
* @module dsh-llm-codebuddy-power/prefs
|
|
3789
|
-
*/
|
|
3790
|
-
/** 本插件拥有的设置命名空间,与插件名一致。 */
|
|
3791
|
-
const CODEBUDDY_SETTINGS_NAMESPACE = "llm-codebuddy-power";
|
|
3792
3953
|
/** 未覆盖任何一层时使用的偏好默认值。 */
|
|
3793
3954
|
const DEFAULT_CODEBUDDY_SETTINGS = {
|
|
3794
3955
|
showUsage: true,
|
|
@@ -3798,11 +3959,11 @@ const DEFAULT_CODEBUDDY_SETTINGS = {
|
|
|
3798
3959
|
//#endregion
|
|
3799
3960
|
//#region lib/types/settings.js
|
|
3800
3961
|
/**
|
|
3801
|
-
*
|
|
3962
|
+
* 本插件的 `Config` schema —— 连接事实与界面偏好合一。
|
|
3802
3963
|
*
|
|
3803
|
-
*
|
|
3804
|
-
* `
|
|
3805
|
-
*
|
|
3964
|
+
* 设置表单只投影**入口自己的 `Config`** 里声明为 `.volatile()` 的字段(入口 id
|
|
3965
|
+
* 即命名空间),因此界面偏好字段与连接字段同在 `Config` 中,只有偏好那三个是
|
|
3966
|
+
* volatile。
|
|
3806
3967
|
*
|
|
3807
3968
|
* 字段的默认值一律取自 `DEFAULT_CODEBUDDY_SETTINGS`,不在此重复字面量 ——
|
|
3808
3969
|
* 两处各写一份时,改了一处就会出现「宿主默认 90、界面显示 80」这种只有用户
|
|
@@ -3810,12 +3971,12 @@ const DEFAULT_CODEBUDDY_SETTINGS = {
|
|
|
3810
3971
|
*
|
|
3811
3972
|
* @module dsh-llm-codebuddy-power/settings
|
|
3812
3973
|
*/
|
|
3813
|
-
/**
|
|
3814
|
-
const
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3974
|
+
/** `Config.showUsage` 的 schema。 */
|
|
3975
|
+
const ShowUsageField = z.boolean().default(DEFAULT_CODEBUDDY_SETTINGS.showUsage).volatile();
|
|
3976
|
+
/** `Config.dangerPct` 的 schema。 */
|
|
3977
|
+
const DangerPctField = z.number().step(1).min(1).max(100).default(DEFAULT_CODEBUDDY_SETTINGS.dangerPct).volatile();
|
|
3978
|
+
/** `Config.drawToolInMinimal` 的 schema。 */
|
|
3979
|
+
const DrawToolInMinimalField = z.boolean().default(DEFAULT_CODEBUDDY_SETTINGS.drawToolInMinimal).volatile();
|
|
3819
3980
|
//#endregion
|
|
3820
3981
|
//#region lib/types/index.js
|
|
3821
3982
|
/**
|
|
@@ -3834,9 +3995,9 @@ const CodeBuddySettingsSchema = z.object({
|
|
|
3834
3995
|
const name = "llm-codebuddy-power";
|
|
3835
3996
|
/**
|
|
3836
3997
|
* 本插件的激活服务。`llm` 承载适配器路由,`tools` 接受文生图工具注册,
|
|
3837
|
-
* `agents` 用于极简模式会话中按 agent
|
|
3838
|
-
*
|
|
3839
|
-
*
|
|
3998
|
+
* `agents` 用于极简模式会话中按 agent 限制画图工具可见性。生成图片经标准附件通道
|
|
3999
|
+
* (attachments.saveImage + uiConversation.imageUrl)持久化与展示,工具注册在
|
|
4000
|
+
* 附件服务挂载后(`ctx.inject(['attachments'])`)。
|
|
3840
4001
|
*/
|
|
3841
4002
|
const inject = [
|
|
3842
4003
|
"llm",
|
|
@@ -3844,14 +4005,29 @@ const inject = [
|
|
|
3844
4005
|
"agents"
|
|
3845
4006
|
];
|
|
3846
4007
|
/**
|
|
3847
|
-
*
|
|
4008
|
+
* {@link Config} 的运行时 schema。
|
|
4009
|
+
*
|
|
4010
|
+
* 三个界面偏好字段必须是 volatile 才会被设置表单投影,而 volatile 只从
|
|
4011
|
+
* schemastery schema 产生;连接字段保持普通字段,因此不在表单里暴露 ——
|
|
4012
|
+
* 它们由 profile 的 cordis.patch.yml 配置。
|
|
4013
|
+
*/
|
|
4014
|
+
const Config = z.object({
|
|
4015
|
+
baseURL: z.string(),
|
|
4016
|
+
defaultContextWindow: z.number().step(1).min(1),
|
|
4017
|
+
defaultMaxTokens: z.number().step(1).min(1),
|
|
4018
|
+
streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE),
|
|
4019
|
+
showUsage: ShowUsageField,
|
|
4020
|
+
dangerPct: DangerPctField,
|
|
4021
|
+
drawToolInMinimal: DrawToolInMinimalField
|
|
4022
|
+
});
|
|
4023
|
+
/**
|
|
4024
|
+
* 从配置解析连接事实。
|
|
3848
4025
|
*
|
|
3849
|
-
*
|
|
3850
|
-
*
|
|
3851
|
-
* @param config - 原始入口配置。
|
|
4026
|
+
* 连接字段在 `Config` 里都是可选的,缺省值即公开的 CodeBuddy 服务。
|
|
4027
|
+
* @param config - 入口配置。
|
|
3852
4028
|
* @returns 解析出的连接事实。
|
|
3853
4029
|
*/
|
|
3854
|
-
function resolveConnectionOptions(config
|
|
4030
|
+
function resolveConnectionOptions(config) {
|
|
3855
4031
|
const positiveInteger = (value, field, fallback) => {
|
|
3856
4032
|
if (value === void 0) return fallback;
|
|
3857
4033
|
if (!Number.isInteger(value) || value <= 0) throw new Error(`dsh-llm-codebuddy-power: ${field} must be a positive integer`);
|
|
@@ -3869,34 +4045,23 @@ function resolveConnectionOptions(config = {}) {
|
|
|
3869
4045
|
};
|
|
3870
4046
|
}
|
|
3871
4047
|
/**
|
|
3872
|
-
*
|
|
3873
|
-
*
|
|
3874
|
-
*
|
|
3875
|
-
* 因此坏值在此判定并在加载时失败。
|
|
3876
|
-
* @param config - 原始入口配置。
|
|
3877
|
-
* @returns 设置命名空间的 base 值。
|
|
4048
|
+
* 读取界面偏好的当前值。
|
|
4049
|
+
* @param config - 入口配置。
|
|
4050
|
+
* @returns 三个界面偏好。
|
|
3878
4051
|
*/
|
|
3879
|
-
function
|
|
3880
|
-
const dangerPct = config.dangerPct ?? DEFAULT_CODEBUDDY_SETTINGS.dangerPct;
|
|
3881
|
-
if (!Number.isInteger(dangerPct) || dangerPct < 1 || dangerPct > 100) throw new Error(`dsh-llm-codebuddy-power: dangerPct must be an integer within 1..100`);
|
|
4052
|
+
function resolvePrefs(config) {
|
|
3882
4053
|
return {
|
|
3883
|
-
showUsage: config.showUsage
|
|
3884
|
-
dangerPct,
|
|
3885
|
-
drawToolInMinimal: config.drawToolInMinimal
|
|
4054
|
+
showUsage: config.showUsage.get(),
|
|
4055
|
+
dangerPct: config.dangerPct.get(),
|
|
4056
|
+
drawToolInMinimal: config.drawToolInMinimal.get()
|
|
3886
4057
|
};
|
|
3887
4058
|
}
|
|
3888
4059
|
/** 挂载插件:解析配置,然后注册路由。 */
|
|
3889
|
-
function apply(ctx, config
|
|
4060
|
+
function apply(ctx, config) {
|
|
3890
4061
|
const resolved = resolveConnectionOptions(config);
|
|
3891
|
-
const
|
|
3892
|
-
let prefsSource = () => prefsBase;
|
|
4062
|
+
const prefsSource = () => resolvePrefs(config);
|
|
3893
4063
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
3894
|
-
settingsCtx.settings.
|
|
3895
|
-
setSource: (current) => {
|
|
3896
|
-
prefsSource = current;
|
|
3897
|
-
},
|
|
3898
|
-
onChange: () => {}
|
|
3899
|
-
});
|
|
4064
|
+
settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber));
|
|
3900
4065
|
});
|
|
3901
4066
|
const session = new CodeBuddySession(ctx.logger, () => {
|
|
3902
4067
|
ctx.emit("llm/adapters-updated");
|
|
@@ -3905,6 +4070,7 @@ function apply(ctx, config = {}) {
|
|
|
3905
4070
|
session,
|
|
3906
4071
|
options: () => resolved,
|
|
3907
4072
|
resolveAttachments: () => ctx.get("attachments"),
|
|
4073
|
+
resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(attachments, (hostPath) => ctx.get("fs")?.processPathFromHostPath(hostPath), ref),
|
|
3908
4074
|
resolveOverrides: async (modelId) => {
|
|
3909
4075
|
return (await loadOverrides())[modelId];
|
|
3910
4076
|
}
|
|
@@ -3950,8 +4116,8 @@ function apply(ctx, config = {}) {
|
|
|
3950
4116
|
ctx.on("session/disposed", onSessionDisposed, { global: true });
|
|
3951
4117
|
session.isLoggedIn().then((loggedIn) => {
|
|
3952
4118
|
if (loggedIn) return;
|
|
3953
|
-
ctx.logger.info("llm-codebuddy-power: no CodeBuddy session stored; sign in through the
|
|
4119
|
+
ctx.logger.info("llm-codebuddy-power: no CodeBuddy session stored; sign in through this plugin's card on the Web sidebar's Plugins page, or run `dsh plugin --profile web codebuddy-power` (no API key needed).");
|
|
3954
4120
|
}).catch(() => {});
|
|
3955
4121
|
}
|
|
3956
4122
|
//#endregion
|
|
3957
|
-
export { AUTH_PENDING_CODE, BLOCKED_MODEL_IDS, CODEBUDDY_AUTH_CHANNEL, CODEBUDDY_AXIOS_USER_AGENT, CODEBUDDY_CHAT_BASE, CODEBUDDY_DISPLAY_NAME, CODEBUDDY_ENDPOINT, CODEBUDDY_IDE_VERSION, CODEBUDDY_PRODUCT_NAME, CODEBUDDY_PROVIDER, CodeBuddyAdapter, CodeBuddyAuthService, CodeBuddySession, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_TOKENS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, LOGIN_POLL_INTERVAL_MS, LOGIN_TIMEOUT_MS, NotLoggedInError, REQUEST_IMAGE_DIMENSION_LADDER, REQUEST_IMAGE_MAX_BYTES, apply, fetchEnterpriseUsage, fetchPersonalUsage, fetchUsage, getStoragePath, hasDisclosedCapacity, httpErrorCode, inject, isBlockedModelId, loadSessionInfo, loadStorage, login, name, parseUsage, removeAccounts, resolveConnectionOptions,
|
|
4123
|
+
export { AUTH_PENDING_CODE, BLOCKED_MODEL_IDS, CODEBUDDY_AUTH_CHANNEL, CODEBUDDY_AXIOS_USER_AGENT, CODEBUDDY_CHAT_BASE, CODEBUDDY_DISPLAY_NAME, CODEBUDDY_ENDPOINT, CODEBUDDY_GIFT_SUB_PRODUCT_CODE, CODEBUDDY_IDE_VERSION, CODEBUDDY_PRODUCT_NAME, CODEBUDDY_PROVIDER, CodeBuddyAdapter, CodeBuddyAuthService, CodeBuddySession, Config, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_TOKENS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, LOGIN_POLL_INTERVAL_MS, LOGIN_RESULT_RETENTION_MS, LOGIN_TIMEOUT_MS, NotLoggedInError, REQUEST_IMAGE_DIMENSION_LADDER, REQUEST_IMAGE_MAX_BYTES, apply, fetchEnterpriseUsage, fetchPersonalUsage, fetchUsage, getStoragePath, hasDisclosedCapacity, httpErrorCode, inject, isBlockedModelId, loadSessionInfo, loadStorage, login, name, parseUsage, removeAccounts, resolveConnectionOptions, resolvePrefs, setLastAccount, updateSessionInfo, updateStorage, upsertAccount };
|