c0de-agent 1.11.0 → 1.13.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/README.md +63 -16
- package/dist/cli/commands/acp.js +26 -8
- package/dist/cli/commands/chat.js +151 -1
- package/dist/cli/commands/config.d.ts +2 -0
- package/dist/cli/commands/config.js +63 -3
- package/dist/cli/commands/serve.js +11 -2
- package/dist/cli/commands/sessions.js +109 -3
- package/dist/cli/commands/trust.d.ts +11 -0
- package/dist/cli/commands/trust.js +31 -0
- package/dist/cli/commands/update.js +4 -0
- package/dist/cli/deps.d.ts +9 -6
- package/dist/cli/deps.js +66 -19
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.js +110 -11
- package/dist/cli/modes/acp.js +39 -8
- package/dist/cli/modes/print.d.ts +3 -0
- package/dist/cli/modes/print.js +78 -8
- package/dist/core/agent.d.ts +1 -1
- package/dist/core/agent.js +9 -4
- package/dist/core/agents/builtin.js +2 -2
- package/dist/core/agents/index.d.ts +0 -1
- package/dist/core/agents/index.js +0 -1
- package/dist/core/compact.d.ts +2 -0
- package/dist/core/compact.js +1 -0
- package/dist/core/config.d.ts +53 -2
- package/dist/core/config.js +235 -19
- package/dist/core/loop/compaction.d.ts +2 -1
- package/dist/core/loop/compaction.js +19 -6
- package/dist/core/loop/segment.d.ts +3 -1
- package/dist/core/loop/segment.js +24 -1
- package/dist/core/loop/stream-collect.js +9 -1
- package/dist/core/loop/subagent.js +88 -37
- package/dist/core/loop.d.ts +7 -0
- package/dist/core/loop.js +90 -6
- package/dist/core/redact.d.ts +11 -0
- package/dist/core/redact.js +49 -0
- package/dist/core/slash.d.ts +10 -0
- package/dist/core/slash.js +237 -65
- package/dist/core/title.js +7 -1
- package/dist/core/types.d.ts +22 -1
- package/dist/core/workflows/context.js +44 -4
- package/dist/core/workflows/discovery.d.ts +2 -2
- package/dist/core/workflows/discovery.js +5 -7
- package/dist/core/workflows/index.d.ts +2 -1
- package/dist/core/workflows/index.js +2 -1
- package/dist/core/workflows/registry.d.ts +13 -3
- package/dist/core/workflows/registry.js +14 -9
- package/dist/core/workflows/resolve.d.ts +22 -0
- package/dist/core/workflows/resolve.js +23 -0
- package/dist/core/workflows/runtime.d.ts +6 -1
- package/dist/core/workflows/runtime.js +19 -2
- package/dist/db/schema.d.ts +373 -1
- package/dist/db/schema.js +60 -4
- package/dist/kanban/index.d.ts +2 -1
- package/dist/kanban/index.js +1 -1
- package/dist/kanban/store.d.ts +87 -1
- package/dist/kanban/store.js +429 -22
- package/dist/llm/index.d.ts +1 -1
- package/dist/llm/index.js +1 -1
- package/dist/llm/provider.js +1 -1
- package/dist/llm/registry.d.ts +7 -1
- package/dist/llm/registry.js +7 -1
- package/dist/llm/retry.d.ts +5 -2
- package/dist/llm/retry.js +6 -5
- package/dist/llm/routing.d.ts +24 -1
- package/dist/llm/routing.js +29 -1
- package/dist/plugins/init.d.ts +7 -0
- package/dist/plugins/init.js +22 -1
- package/dist/plugins/loader.d.ts +7 -1
- package/dist/plugins/loader.js +12 -4
- package/dist/project/index.d.ts +1 -1
- package/dist/project/index.js +1 -1
- package/dist/project/project.d.ts +37 -0
- package/dist/project/project.js +142 -1
- package/dist/project/trust.d.ts +65 -0
- package/dist/project/trust.js +328 -0
- package/dist/server/agent-manager.d.ts +15 -2
- package/dist/server/agent-manager.js +18 -3
- package/dist/server/app.js +45 -30
- package/dist/server/auth-manager.d.ts +26 -9
- package/dist/server/auth-manager.js +78 -42
- package/dist/server/context.js +14 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/middleware/auth.d.ts +11 -1
- package/dist/server/middleware/auth.js +25 -3
- package/dist/server/permission/interactive.d.ts +11 -3
- package/dist/server/permission/interactive.js +23 -4
- package/dist/server/permission/store.d.ts +14 -2
- package/dist/server/permission/store.js +39 -3
- package/dist/server/registry-config.d.ts +14 -0
- package/dist/server/registry-config.js +44 -0
- package/dist/server/routes/auth.js +66 -10
- package/dist/server/routes/chat.d.ts +21 -0
- package/dist/server/routes/chat.js +685 -52
- package/dist/server/routes/commands.d.ts +4 -2
- package/dist/server/routes/commands.js +9 -4
- package/dist/server/routes/config.js +180 -23
- package/dist/server/routes/files.js +142 -28
- package/dist/server/routes/kanban.js +218 -26
- package/dist/server/routes/permissions.d.ts +0 -4
- package/dist/server/routes/permissions.js +140 -7
- package/dist/server/routes/project.js +132 -29
- package/dist/server/routes/provider.js +54 -3
- package/dist/server/routes/session.js +463 -23
- package/dist/server/routes/tool.js +24 -5
- package/dist/server/routes/update.d.ts +11 -5
- package/dist/server/routes/update.js +145 -29
- package/dist/server/routes/usage.d.ts +4 -0
- package/dist/server/routes/usage.js +133 -0
- package/dist/server/routes/workflows.js +476 -87
- package/dist/server/server.d.ts +16 -13
- package/dist/server/server.js +364 -107
- package/dist/server/terminal/pty-manager.d.ts +12 -0
- package/dist/server/terminal/pty-manager.js +63 -7
- package/dist/server/types.d.ts +16 -0
- package/dist/session/archive.d.ts +3 -1
- package/dist/session/archive.js +13 -1
- package/dist/session/branch.d.ts +2 -1
- package/dist/session/branch.js +92 -9
- package/dist/session/context.js +28 -2
- package/dist/session/import.d.ts +44 -0
- package/dist/session/import.js +83 -0
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.js +1 -1
- package/dist/session/jobs.d.ts +12 -0
- package/dist/session/jobs.js +53 -0
- package/dist/session/message.d.ts +7 -1
- package/dist/session/message.js +21 -1
- package/dist/session/session.d.ts +186 -12
- package/dist/session/session.js +577 -33
- package/dist/session/snapshot.d.ts +5 -2
- package/dist/session/snapshot.js +10 -2
- package/dist/session/types.d.ts +13 -1
- package/dist/session/usage.d.ts +62 -0
- package/dist/session/usage.js +194 -0
- package/dist/shared/types/agent.d.ts +29 -2
- package/dist/shared/types/config.d.ts +64 -5
- package/dist/shared/types/kanban.d.ts +17 -2
- package/dist/shared/types/message.d.ts +23 -0
- package/dist/tools/builtin/kanban.js +13 -2
- package/dist/tools/index.d.ts +14 -3
- package/dist/tools/index.js +23 -5
- package/dist/tools/websearch/index.d.ts +5 -1
- package/dist/tools/websearch/index.js +46 -7
- package/dist/tools/websearch/providers/brave.js +2 -2
- package/dist/tools/websearch/providers/duckduckgo.js +2 -1
- package/dist/tools/websearch/providers/tavily.js +2 -2
- package/dist/tools/websearch/types.d.ts +5 -1
- package/dist/tools/websearch/types.js +7 -1
- package/dist/update/index.d.ts +3 -3
- package/dist/update/index.js +2 -2
- package/dist/update/ipc.d.ts +13 -2
- package/dist/update/ipc.js +36 -2
- package/dist/update/scheduler.js +5 -2
- package/dist/update/snapshot.d.ts +18 -3
- package/dist/update/snapshot.js +5 -2
- package/dist/update/updater.d.ts +17 -1
- package/dist/update/updater.js +47 -18
- package/dist/update/version.d.ts +3 -0
- package/dist/update/version.js +9 -3
- package/drizzle/0008_tranquil_lily_hollister.sql +1 -0
- package/drizzle/0010_usage_events_ledger.sql +17 -0
- package/drizzle/0011_slow_slapstick.sql +12 -0
- package/drizzle/0012_chief_timeslip.sql +1 -0
- package/drizzle/0013_swift_zeigeist.sql +1 -0
- package/drizzle/0014_flimsy_legion.sql +1 -0
- package/drizzle/meta/0008_snapshot.json +875 -0
- package/drizzle/meta/0010_snapshot.json +1010 -0
- package/drizzle/meta/0011_snapshot.json +1055 -0
- package/drizzle/meta/0012_snapshot.json +1062 -0
- package/drizzle/meta/0013_snapshot.json +1068 -0
- package/drizzle/meta/0014_snapshot.json +1075 -0
- package/drizzle/meta/_journal.json +42 -0
- package/package.json +42 -42
- package/dist/core/agents/discovery.d.ts +0 -6
- package/dist/core/agents/discovery.js +0 -96
package/dist/kanban/store.js
CHANGED
|
@@ -1,10 +1,53 @@
|
|
|
1
|
-
import { and, asc, eq, max } from 'drizzle-orm';
|
|
1
|
+
import { and, asc, eq, inArray, isNotNull, isNull, max, sql } from 'drizzle-orm';
|
|
2
2
|
import { kanbanBoards, kanbanCards } from '../db/schema.js';
|
|
3
3
|
import { DEFAULT_KANBAN_COLUMNS } from '../shared/types/kanban.js';
|
|
4
4
|
/** Default column when none is specified. */
|
|
5
5
|
const DEFAULT_COLUMN_ID = 'todo';
|
|
6
6
|
/** Position increment — large gap avoids frequent re-indexing on reorder. */
|
|
7
7
|
const POSITION_GAP = 1000;
|
|
8
|
+
/** 合法优先级枚举(运行时校验用——REST/tool 层输入未经 TS 约束)。 */
|
|
9
|
+
const VALID_PRIORITIES = new Set(['high', 'medium', 'low']);
|
|
10
|
+
/** 列内仍有卡片时禁止删除该列(否则卡片静默不可见)。 */
|
|
11
|
+
class KanbanColumnInUseError extends Error {
|
|
12
|
+
constructor(columnNames, cardCount, cardIds = []) {
|
|
13
|
+
const idHint = cardIds.length > 0
|
|
14
|
+
? `(示例卡片 id:${cardIds.slice(0, 5).join('、')}${cardIds.length > 5 ? ' 等' : ''},可直接按 id 删除这些卡片)`
|
|
15
|
+
: '';
|
|
16
|
+
super(`列 [${columnNames.join(', ')}] 中仍有 ${cardCount} 张卡片,无法删除。` +
|
|
17
|
+
`请先移动或删除这些卡片。${idHint}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 目标列不存在(add/move 引用未配置的列)——与 import/删列的悬空列防护同口径:
|
|
22
|
+
* 此前运行时 add/move 不校验,LLM 幻觉列名写入后卡片静默不可见,且幽灵卡片
|
|
23
|
+
* 会反过来冻结列配置删除(KanbanColumnInUseError 死锁)。
|
|
24
|
+
* 错误信息回传可用列清单,供 LLM 自纠而非再猜。
|
|
25
|
+
*/
|
|
26
|
+
class KanbanColumnNotFoundError extends Error {
|
|
27
|
+
columnId;
|
|
28
|
+
constructor(columnId, availableColumns, op) {
|
|
29
|
+
const avail = availableColumns.length > 0 ? availableColumns.join('、') : '(看板无列)';
|
|
30
|
+
super(`列 "${columnId}" 不存在,无法${op === 'add' ? '创建' : '移动'}卡片。` +
|
|
31
|
+
`可用列:${avail}。请改用其中一个列 id。`);
|
|
32
|
+
this.columnId = columnId;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** 非法优先级(REST PATCH / kanbanTool 透传未校验值)。 */
|
|
36
|
+
class KanbanInvalidPriorityError extends Error {
|
|
37
|
+
value;
|
|
38
|
+
constructor(value) {
|
|
39
|
+
super(`无效优先级 "${value}",允许值:high、medium、low`);
|
|
40
|
+
this.value = value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** 卡片不存在(update/move 目标 id 无效)。 */
|
|
44
|
+
class KanbanCardNotFoundError extends Error {
|
|
45
|
+
cardId;
|
|
46
|
+
constructor(cardId) {
|
|
47
|
+
super(`Kanban card not found: ${cardId}`);
|
|
48
|
+
this.cardId = cardId;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
8
51
|
// ── Row → API mappers ──────────────────────────────────────
|
|
9
52
|
function rowToBoard(row) {
|
|
10
53
|
return {
|
|
@@ -50,9 +93,14 @@ function createKanbanStore(handle, projectId) {
|
|
|
50
93
|
const [row] = await db
|
|
51
94
|
.select({ id: kanbanBoards.id })
|
|
52
95
|
.from(kanbanBoards)
|
|
53
|
-
.where(eq(kanbanBoards.projectId, projectId))
|
|
96
|
+
.where(and(eq(kanbanBoards.projectId, projectId), isNull(kanbanBoards.deletedAt)))
|
|
54
97
|
.limit(1);
|
|
55
|
-
//
|
|
98
|
+
// 正常路径行一定存在(insert + onConflictDoNothing 已保证)。防御性兜底:
|
|
99
|
+
// 唯一 projectId 槽位被软删行占用等异常态下不再返回 undefined id
|
|
100
|
+
//(后续查询会静默查空、上层误判「板不存在」),显式抛错暴露问题。
|
|
101
|
+
if (!row) {
|
|
102
|
+
throw new Error(`Kanban board for project ${projectId} missing after ensure-insert (unique slot likely held by a soft-deleted row)`);
|
|
103
|
+
}
|
|
56
104
|
return row.id;
|
|
57
105
|
}
|
|
58
106
|
/** Max position in a column (0 if empty). */
|
|
@@ -63,7 +111,45 @@ function createKanbanStore(handle, projectId) {
|
|
|
63
111
|
.where(and(eq(kanbanCards.boardId, boardId), eq(kanbanCards.columnId, columnId)));
|
|
64
112
|
return row?.m ?? 0;
|
|
65
113
|
}
|
|
114
|
+
/** 当前板的列配置(add/move 的悬空列校验用)。 */
|
|
115
|
+
async function boardColumns(boardId) {
|
|
116
|
+
const [row] = await db
|
|
117
|
+
.select({ columns: kanbanBoards.columns })
|
|
118
|
+
.from(kanbanBoards)
|
|
119
|
+
.where(eq(kanbanBoards.id, boardId))
|
|
120
|
+
.limit(1);
|
|
121
|
+
return (row?.columns ?? []);
|
|
122
|
+
}
|
|
123
|
+
/** 校验列存在;不存在抛 KanbanColumnNotFoundError(消息含可用列清单)。 */
|
|
124
|
+
function assertColumn(columns, columnId, op) {
|
|
125
|
+
if (!columns.some((c) => c.id === columnId)) {
|
|
126
|
+
throw new KanbanColumnNotFoundError(columnId, columns.map((c) => c.id), op);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** 校验优先级合法;非法抛 KanbanInvalidPriorityError。 */
|
|
130
|
+
function assertPriority(priority) {
|
|
131
|
+
if (priority !== undefined && !VALID_PRIORITIES.has(priority)) {
|
|
132
|
+
throw new KanbanInvalidPriorityError(String(priority));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
66
135
|
return {
|
|
136
|
+
/** 只读获取板(不创建):导出等纯读路径用——此前导出经 getBoard 懒建板,
|
|
137
|
+
* 「导出」一个从未打开过看板的项目会凭空产生空板行。无板返回 null。 */
|
|
138
|
+
async peekBoard() {
|
|
139
|
+
const [boardRow] = await db
|
|
140
|
+
.select()
|
|
141
|
+
.from(kanbanBoards)
|
|
142
|
+
.where(and(eq(kanbanBoards.projectId, projectId), isNull(kanbanBoards.deletedAt)))
|
|
143
|
+
.limit(1);
|
|
144
|
+
if (!boardRow)
|
|
145
|
+
return null;
|
|
146
|
+
const cards = await db
|
|
147
|
+
.select()
|
|
148
|
+
.from(kanbanCards)
|
|
149
|
+
.where(eq(kanbanCards.boardId, boardRow.id))
|
|
150
|
+
.orderBy(asc(kanbanCards.columnId), asc(kanbanCards.position));
|
|
151
|
+
return { ...rowToBoard(boardRow), cards: cards.map(rowToCard) };
|
|
152
|
+
},
|
|
67
153
|
async getBoard() {
|
|
68
154
|
const boardId = await getOrCreateBoardId();
|
|
69
155
|
const [boardRow] = await db
|
|
@@ -81,7 +167,12 @@ function createKanbanStore(handle, projectId) {
|
|
|
81
167
|
},
|
|
82
168
|
async addCard(input) {
|
|
83
169
|
const boardId = await getOrCreateBoardId();
|
|
84
|
-
const
|
|
170
|
+
const columns = await boardColumns(boardId);
|
|
171
|
+
// 默认列 = 板上第一列(而非硬编码 'todo'):todo 列被删除/重排后
|
|
172
|
+
// 硬编码默认会让无参 addCard 静默落入不存在的列(卡片不可见)。
|
|
173
|
+
const columnId = input.columnId ?? columns[0]?.id ?? DEFAULT_COLUMN_ID;
|
|
174
|
+
assertColumn(columns, columnId, 'add');
|
|
175
|
+
assertPriority(input.priority);
|
|
85
176
|
const position = (await maxPos(boardId, columnId)) + POSITION_GAP;
|
|
86
177
|
const [cardRow] = await db
|
|
87
178
|
.insert(kanbanCards)
|
|
@@ -99,6 +190,7 @@ function createKanbanStore(handle, projectId) {
|
|
|
99
190
|
return rowToCard(row);
|
|
100
191
|
},
|
|
101
192
|
async updateCard(id, patch) {
|
|
193
|
+
assertPriority(patch.priority);
|
|
102
194
|
const [row] = await db
|
|
103
195
|
.update(kanbanCards)
|
|
104
196
|
.set({
|
|
@@ -111,36 +203,69 @@ function createKanbanStore(handle, projectId) {
|
|
|
111
203
|
.where(eq(kanbanCards.id, id))
|
|
112
204
|
.returning();
|
|
113
205
|
if (!row)
|
|
114
|
-
throw new
|
|
206
|
+
throw new KanbanCardNotFoundError(id);
|
|
115
207
|
return rowToCard(row);
|
|
116
208
|
},
|
|
117
209
|
async moveCard(id, columnId, position) {
|
|
210
|
+
const [card] = await db
|
|
211
|
+
.select({ boardId: kanbanCards.boardId })
|
|
212
|
+
.from(kanbanCards)
|
|
213
|
+
.where(eq(kanbanCards.id, id))
|
|
214
|
+
.limit(1);
|
|
215
|
+
if (!card)
|
|
216
|
+
throw new KanbanCardNotFoundError(id);
|
|
217
|
+
// 悬空列校验:目标列必须存在于当前列配置,否则卡片静默不可见。
|
|
218
|
+
const columns = await boardColumns(card.boardId);
|
|
219
|
+
assertColumn(columns, columnId, 'move');
|
|
118
220
|
// If no explicit position, append to end of target column.
|
|
119
|
-
|
|
120
|
-
if (newPos === undefined) {
|
|
121
|
-
const [card] = await db
|
|
122
|
-
.select({ boardId: kanbanCards.boardId })
|
|
123
|
-
.from(kanbanCards)
|
|
124
|
-
.where(eq(kanbanCards.id, id))
|
|
125
|
-
.limit(1);
|
|
126
|
-
if (!card)
|
|
127
|
-
throw new Error(`Kanban card not found: ${id}`);
|
|
128
|
-
newPos = (await maxPos(card.boardId, columnId)) + POSITION_GAP;
|
|
129
|
-
}
|
|
221
|
+
const newPos = position ?? (await maxPos(card.boardId, columnId)) + POSITION_GAP;
|
|
130
222
|
const [row] = await db
|
|
131
223
|
.update(kanbanCards)
|
|
132
224
|
.set({ columnId, position: newPos, updatedAt: new Date() })
|
|
133
225
|
.where(eq(kanbanCards.id, id))
|
|
134
226
|
.returning();
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return rowToCard(row);
|
|
227
|
+
const moved = row;
|
|
228
|
+
return rowToCard(moved);
|
|
138
229
|
},
|
|
139
230
|
async deleteCard(id) {
|
|
140
231
|
await db.delete(kanbanCards).where(eq(kanbanCards.id, id));
|
|
141
232
|
},
|
|
142
233
|
async updateBoard(patch) {
|
|
143
|
-
await getOrCreateBoardId();
|
|
234
|
+
const boardId = await getOrCreateBoardId();
|
|
235
|
+
// P1-4:删除列前校验该列内是否有卡片;删除标签前把悬空 labelId 从卡片上清掉。
|
|
236
|
+
if (patch.columns !== undefined) {
|
|
237
|
+
const cards = await db
|
|
238
|
+
.select({ id: kanbanCards.id, columnId: kanbanCards.columnId })
|
|
239
|
+
.from(kanbanCards)
|
|
240
|
+
.where(eq(kanbanCards.boardId, boardId));
|
|
241
|
+
const newColumnIds = new Set(patch.columns.map((c) => c.id));
|
|
242
|
+
const removedWithCards = new Map();
|
|
243
|
+
const orphanCardIds = [];
|
|
244
|
+
for (const card of cards) {
|
|
245
|
+
if (!newColumnIds.has(card.columnId)) {
|
|
246
|
+
removedWithCards.set(card.columnId, (removedWithCards.get(card.columnId) ?? 0) + 1);
|
|
247
|
+
orphanCardIds.push(card.id);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (removedWithCards.size > 0) {
|
|
251
|
+
const total = Array.from(removedWithCards.values()).reduce((a, b) => a + b, 0);
|
|
252
|
+
throw new KanbanColumnInUseError(Array.from(removedWithCards.keys()), total, orphanCardIds);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (patch.labels !== undefined) {
|
|
256
|
+
const labelIds = new Set(patch.labels.map((l) => l.id));
|
|
257
|
+
const cards = await db
|
|
258
|
+
.select({ id: kanbanCards.id, labels: kanbanCards.labels })
|
|
259
|
+
.from(kanbanCards)
|
|
260
|
+
.where(eq(kanbanCards.boardId, boardId));
|
|
261
|
+
for (const card of cards) {
|
|
262
|
+
const current = (card.labels ?? []);
|
|
263
|
+
const kept = current.filter((lid) => labelIds.has(lid));
|
|
264
|
+
if (kept.length !== current.length) {
|
|
265
|
+
await db.update(kanbanCards).set({ labels: kept }).where(eq(kanbanCards.id, card.id));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
144
269
|
const [boardRow] = await db
|
|
145
270
|
.update(kanbanBoards)
|
|
146
271
|
.set({
|
|
@@ -148,11 +273,293 @@ function createKanbanStore(handle, projectId) {
|
|
|
148
273
|
...(patch.labels !== undefined && { labels: patch.labels }),
|
|
149
274
|
updatedAt: new Date(),
|
|
150
275
|
})
|
|
151
|
-
.where(eq(kanbanBoards.projectId, projectId))
|
|
276
|
+
.where(and(eq(kanbanBoards.projectId, projectId), isNull(kanbanBoards.deletedAt)))
|
|
152
277
|
.returning();
|
|
153
278
|
const row = boardRow;
|
|
154
279
|
return rowToBoard(row);
|
|
155
280
|
},
|
|
281
|
+
/** 整板替换(导入):board 配置 + 卡片在单事务内重建;中途失败整体回滚,
|
|
282
|
+
* 不残留「配置已换、卡片半新半旧」的混合状态。 */
|
|
283
|
+
async replaceBoard(input) {
|
|
284
|
+
const boardId = await getOrCreateBoardId();
|
|
285
|
+
await db.transaction(async (tx) => {
|
|
286
|
+
await tx.delete(kanbanCards).where(eq(kanbanCards.boardId, boardId));
|
|
287
|
+
await tx
|
|
288
|
+
.update(kanbanBoards)
|
|
289
|
+
.set({ columns: input.columns, labels: input.labels, updatedAt: new Date() })
|
|
290
|
+
.where(eq(kanbanBoards.id, boardId));
|
|
291
|
+
if (input.cards.length > 0) {
|
|
292
|
+
await tx.insert(kanbanCards).values(input.cards.map((c) => ({
|
|
293
|
+
boardId,
|
|
294
|
+
title: c.title,
|
|
295
|
+
description: c.description ?? null,
|
|
296
|
+
columnId: c.columnId,
|
|
297
|
+
priority: c.priority,
|
|
298
|
+
position: c.position,
|
|
299
|
+
labels: c.labels,
|
|
300
|
+
})));
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
const boardRow = await db
|
|
304
|
+
.select()
|
|
305
|
+
.from(kanbanBoards)
|
|
306
|
+
.where(eq(kanbanBoards.id, boardId))
|
|
307
|
+
.limit(1);
|
|
308
|
+
const board = boardRow[0];
|
|
309
|
+
const cards = await db
|
|
310
|
+
.select()
|
|
311
|
+
.from(kanbanCards)
|
|
312
|
+
.where(eq(kanbanCards.boardId, boardId))
|
|
313
|
+
.orderBy(asc(kanbanCards.columnId), asc(kanbanCards.position));
|
|
314
|
+
return { ...rowToBoard(board), cards: cards.map(rowToCard) };
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
export { createKanbanStore, KanbanCardNotFoundError, KanbanColumnInUseError, KanbanColumnNotFoundError, KanbanInvalidPriorityError, };
|
|
319
|
+
/** 把活动看板软删除进回收站(记录原项目名与工作目录)。项目行删除后 FK 将 projectId 置 null。 */
|
|
320
|
+
export async function softDeleteKanbanBoard(handle, projectId, projectName, worktree) {
|
|
321
|
+
const rows = await handle.db
|
|
322
|
+
.update(kanbanBoards)
|
|
323
|
+
.set({
|
|
324
|
+
deletedAt: new Date(),
|
|
325
|
+
deletedProjectName: projectName ?? undefined,
|
|
326
|
+
deletedProjectWorktree: worktree ?? undefined,
|
|
327
|
+
// 恢复后重删的看板须重新经历「到期 → 宽限」周期,宽限标记不跨删除周期生效。
|
|
328
|
+
purgePendingAt: null,
|
|
329
|
+
updatedAt: new Date(),
|
|
330
|
+
})
|
|
331
|
+
.where(and(eq(kanbanBoards.projectId, projectId), isNull(kanbanBoards.deletedAt)))
|
|
332
|
+
.returning({ id: kanbanBoards.id });
|
|
333
|
+
return rows.length > 0;
|
|
334
|
+
}
|
|
335
|
+
/** 行 + 卡片数 → 回收站条目(时间戳统一转 ms,兼容 Date/null/字符串)。 */
|
|
336
|
+
function toDeletedBoard(b, cardCount) {
|
|
337
|
+
const toMs = (d) => {
|
|
338
|
+
if (d == null)
|
|
339
|
+
return null;
|
|
340
|
+
const t = d instanceof Date ? d.getTime() : new Date(d).getTime();
|
|
341
|
+
return Number.isFinite(t) ? t : null;
|
|
342
|
+
};
|
|
343
|
+
return {
|
|
344
|
+
id: b.id,
|
|
345
|
+
projectName: b.deletedProjectName ?? '未知项目',
|
|
346
|
+
cardCount,
|
|
347
|
+
deletedAt: toMs(b.deletedAt) ?? 0,
|
|
348
|
+
purgePendingAt: toMs(b.purgePendingAt),
|
|
349
|
+
deletedProjectWorktree: b.deletedProjectWorktree ?? null,
|
|
156
350
|
};
|
|
157
351
|
}
|
|
158
|
-
|
|
352
|
+
/** 回收站看板列表(含卡片数)。 */
|
|
353
|
+
export async function listDeletedKanbanBoards(handle) {
|
|
354
|
+
const boards = await handle.db
|
|
355
|
+
.select()
|
|
356
|
+
.from(kanbanBoards)
|
|
357
|
+
.where(isNotNull(kanbanBoards.deletedAt))
|
|
358
|
+
.orderBy(asc(kanbanBoards.deletedAt));
|
|
359
|
+
if (boards.length === 0)
|
|
360
|
+
return [];
|
|
361
|
+
const counts = await handle.db
|
|
362
|
+
.select({ boardId: kanbanCards.boardId, n: sql `count(*)::int` })
|
|
363
|
+
.from(kanbanCards)
|
|
364
|
+
.where(inArray(kanbanCards.boardId, boards.map((b) => b.id)))
|
|
365
|
+
.groupBy(kanbanCards.boardId);
|
|
366
|
+
const countMap = new Map(counts.map((c) => [c.boardId, c.n]));
|
|
367
|
+
return boards.map((b) => toDeletedBoard(b, countMap.get(b.id) ?? 0));
|
|
368
|
+
}
|
|
369
|
+
/** 按 id 取单个回收站看板(供「重建原项目」恢复读取原工作目录)。 */
|
|
370
|
+
export async function getDeletedKanbanBoard(handle, boardId) {
|
|
371
|
+
const [b] = await handle.db
|
|
372
|
+
.select()
|
|
373
|
+
.from(kanbanBoards)
|
|
374
|
+
.where(and(eq(kanbanBoards.id, boardId), isNotNull(kanbanBoards.deletedAt)))
|
|
375
|
+
.limit(1);
|
|
376
|
+
if (!b)
|
|
377
|
+
return null;
|
|
378
|
+
const counts = await handle.db
|
|
379
|
+
.select({ n: sql `count(*)::int` })
|
|
380
|
+
.from(kanbanCards)
|
|
381
|
+
.where(eq(kanbanCards.boardId, boardId));
|
|
382
|
+
return toDeletedBoard(b, counts[0]?.n ?? 0);
|
|
383
|
+
}
|
|
384
|
+
/** 恢复看板到指定项目:目标项目已有活动看板 → 409 语义(不覆盖用户现有看板)。
|
|
385
|
+
* P2 修复:检查与更新放进单事务,并发下撞 uq_kanban_boards_project 唯一索引时
|
|
386
|
+
* 降级为 TARGET_HAS_BOARD(此前抛 PG 错误 500)。 */
|
|
387
|
+
export async function restoreKanbanBoard(handle, boardId, projectId) {
|
|
388
|
+
try {
|
|
389
|
+
return await handle.db.transaction(async (tx) => {
|
|
390
|
+
const [active] = await tx
|
|
391
|
+
.select({ id: kanbanBoards.id })
|
|
392
|
+
.from(kanbanBoards)
|
|
393
|
+
.where(and(eq(kanbanBoards.projectId, projectId), isNull(kanbanBoards.deletedAt)))
|
|
394
|
+
.limit(1);
|
|
395
|
+
if (active)
|
|
396
|
+
return { ok: false, reason: 'TARGET_HAS_BOARD' };
|
|
397
|
+
const rows = await tx
|
|
398
|
+
.update(kanbanBoards)
|
|
399
|
+
.set({
|
|
400
|
+
projectId,
|
|
401
|
+
deletedAt: null,
|
|
402
|
+
deletedProjectName: null,
|
|
403
|
+
deletedProjectWorktree: null,
|
|
404
|
+
purgePendingAt: null,
|
|
405
|
+
updatedAt: new Date(),
|
|
406
|
+
})
|
|
407
|
+
.where(and(eq(kanbanBoards.id, boardId), isNotNull(kanbanBoards.deletedAt)))
|
|
408
|
+
.returning({ id: kanbanBoards.id });
|
|
409
|
+
if (rows.length === 0)
|
|
410
|
+
return { ok: false, reason: 'BOARD_NOT_FOUND' };
|
|
411
|
+
return { ok: true };
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
// PG 23505 = unique_violation:并发窗口内目标板被创建。
|
|
416
|
+
if (err.code === '23505') {
|
|
417
|
+
return { ok: false, reason: 'TARGET_HAS_BOARD' };
|
|
418
|
+
}
|
|
419
|
+
throw err;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* P1 修复:把回收站看板合并进目标项目的活动看板(TARGET_HAS_BOARD 死胡同的出口)。
|
|
424
|
+
* 此前「目标项目已有看板」时恢复 409,而产品没有删除活动看板的能力,用户无法
|
|
425
|
+
* 把旧项目卡片拿回现有项目——现在 merge 模式下:
|
|
426
|
+
* - 目标无活动看板 → 等价普通恢复(重新归属);
|
|
427
|
+
* - 目标有活动看板 → 追加缺失列,卡片并入对应列末尾(position 重新计算避免
|
|
428
|
+
* 与目标卡片重叠);源卡片引用源板已不存在列时落到目标第一列(保证可见);
|
|
429
|
+
* - 合并成功后源看板物理删除(卡片已迁移,FK cascade 清源卡)。
|
|
430
|
+
* 全程单事务:失败整体回滚,不残留半合并状态。
|
|
431
|
+
*/
|
|
432
|
+
export async function mergeKanbanBoard(handle, boardId, projectId) {
|
|
433
|
+
return await handle.db.transaction(async (tx) => {
|
|
434
|
+
const [src] = await tx
|
|
435
|
+
.select({ id: kanbanBoards.id, columns: kanbanBoards.columns })
|
|
436
|
+
.from(kanbanBoards)
|
|
437
|
+
.where(and(eq(kanbanBoards.id, boardId), isNotNull(kanbanBoards.deletedAt)))
|
|
438
|
+
.limit(1);
|
|
439
|
+
if (!src)
|
|
440
|
+
return { ok: false, reason: 'BOARD_NOT_FOUND' };
|
|
441
|
+
const [target] = await tx
|
|
442
|
+
.select({ id: kanbanBoards.id, columns: kanbanBoards.columns })
|
|
443
|
+
.from(kanbanBoards)
|
|
444
|
+
.where(and(eq(kanbanBoards.projectId, projectId), isNull(kanbanBoards.deletedAt)))
|
|
445
|
+
.limit(1);
|
|
446
|
+
// 目标无活动看板 → 重新归属(等价普通恢复,但保持 merge 调用幂等语义)。
|
|
447
|
+
if (!target) {
|
|
448
|
+
const rows = await tx
|
|
449
|
+
.update(kanbanBoards)
|
|
450
|
+
.set({
|
|
451
|
+
projectId,
|
|
452
|
+
deletedAt: null,
|
|
453
|
+
deletedProjectName: null,
|
|
454
|
+
deletedProjectWorktree: null,
|
|
455
|
+
purgePendingAt: null,
|
|
456
|
+
updatedAt: new Date(),
|
|
457
|
+
})
|
|
458
|
+
.where(and(eq(kanbanBoards.id, boardId), isNotNull(kanbanBoards.deletedAt)))
|
|
459
|
+
.returning({ id: kanbanBoards.id });
|
|
460
|
+
if (rows.length === 0)
|
|
461
|
+
return { ok: false, reason: 'BOARD_NOT_FOUND' };
|
|
462
|
+
return { ok: true, mergedColumns: 0, mergedCards: 0 };
|
|
463
|
+
}
|
|
464
|
+
// —— 合并路径 ——
|
|
465
|
+
const srcCols = (src.columns ?? []);
|
|
466
|
+
const targetCols = (target.columns ?? []);
|
|
467
|
+
const targetIds = new Set(targetCols.map((c) => c.id));
|
|
468
|
+
const newCols = srcCols.filter((c) => !targetIds.has(c.id));
|
|
469
|
+
if (newCols.length > 0) {
|
|
470
|
+
await tx
|
|
471
|
+
.update(kanbanBoards)
|
|
472
|
+
.set({ columns: [...targetCols, ...newCols], updatedAt: new Date() })
|
|
473
|
+
.where(eq(kanbanBoards.id, target.id));
|
|
474
|
+
}
|
|
475
|
+
const mergedCols = [...targetCols, ...newCols];
|
|
476
|
+
const fallbackCol = mergedCols[0]?.id ?? DEFAULT_COLUMN_ID;
|
|
477
|
+
const srcCards = await tx
|
|
478
|
+
.select()
|
|
479
|
+
.from(kanbanCards)
|
|
480
|
+
.where(eq(kanbanCards.boardId, boardId))
|
|
481
|
+
.orderBy(asc(kanbanCards.position));
|
|
482
|
+
// 目标各列现有 max(position):合并卡片追加到末尾,避免 position 重叠。
|
|
483
|
+
const targetMax = await tx
|
|
484
|
+
.select({ columnId: kanbanCards.columnId, m: max(kanbanCards.position) })
|
|
485
|
+
.from(kanbanCards)
|
|
486
|
+
.where(eq(kanbanCards.boardId, target.id))
|
|
487
|
+
.groupBy(kanbanCards.columnId);
|
|
488
|
+
const maxMap = new Map(targetMax.map((r) => [r.columnId, r.m ?? 0]));
|
|
489
|
+
const counters = new Map();
|
|
490
|
+
let mergedCards = 0;
|
|
491
|
+
for (const card of srcCards) {
|
|
492
|
+
const colId = mergedCols.some((c) => c.id === card.columnId) ? card.columnId : fallbackCol;
|
|
493
|
+
const idx = counters.get(colId) ?? 0;
|
|
494
|
+
counters.set(colId, idx + 1);
|
|
495
|
+
await tx.insert(kanbanCards).values({
|
|
496
|
+
boardId: target.id,
|
|
497
|
+
title: card.title,
|
|
498
|
+
description: card.description,
|
|
499
|
+
columnId: colId,
|
|
500
|
+
priority: card.priority,
|
|
501
|
+
position: (maxMap.get(colId) ?? 0) + POSITION_GAP * (idx + 1),
|
|
502
|
+
labels: card.labels ?? [],
|
|
503
|
+
});
|
|
504
|
+
mergedCards += 1;
|
|
505
|
+
}
|
|
506
|
+
// 源看板物理删除(卡片已迁移;卡片行经 FK cascade 连带清除)。
|
|
507
|
+
await tx.delete(kanbanBoards).where(eq(kanbanBoards.id, boardId));
|
|
508
|
+
return { ok: true, mergedColumns: newCols.length, mergedCards };
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
/** 回收站内彻底删除看板(不可恢复)。 */
|
|
512
|
+
export async function permanentlyDeleteKanbanBoard(handle, boardId) {
|
|
513
|
+
const rows = await handle.db
|
|
514
|
+
.delete(kanbanBoards)
|
|
515
|
+
.where(and(eq(kanbanBoards.id, boardId), isNotNull(kanbanBoards.deletedAt)))
|
|
516
|
+
.returning({ id: kanbanBoards.id });
|
|
517
|
+
return rows.length;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* 看板回收站两阶段清理(与会话 purgeDeletedSessions 同策略,杜绝「到期即静默清空」):
|
|
521
|
+
* - 阶段一:软删除超过 retentionMs 且尚未标记 → 写 purgePendingAt(进入宽限期,
|
|
522
|
+
* UI 显示「即将清除」仍可恢复);
|
|
523
|
+
* - 阶段二:purgePendingAt 早于 graceMs 截止 → 物理清除(卡片经 FK cascade 连带删除)。
|
|
524
|
+
* 看板回收站全局单一分组、无「首次查看才起算」语义,保留期自删除时刻起算,稳定可预期。
|
|
525
|
+
*/
|
|
526
|
+
export async function purgeDeletedKanbanBoards(handle, retentionMs, graceMs) {
|
|
527
|
+
const retentionCutoff = new Date(Date.now() - retentionMs);
|
|
528
|
+
const graceCutoff = new Date(Date.now() - graceMs);
|
|
529
|
+
const rows = await handle.db
|
|
530
|
+
.select({
|
|
531
|
+
id: kanbanBoards.id,
|
|
532
|
+
deletedAt: kanbanBoards.deletedAt,
|
|
533
|
+
purgePendingAt: kanbanBoards.purgePendingAt,
|
|
534
|
+
})
|
|
535
|
+
.from(kanbanBoards)
|
|
536
|
+
.where(isNotNull(kanbanBoards.deletedAt));
|
|
537
|
+
// —— 阶段一:到期标记(先宽限,不直接清除)——
|
|
538
|
+
let marked = 0;
|
|
539
|
+
for (const r of rows) {
|
|
540
|
+
if (!r.deletedAt)
|
|
541
|
+
continue;
|
|
542
|
+
if (r.purgePendingAt)
|
|
543
|
+
continue;
|
|
544
|
+
if (r.deletedAt.getTime() >= retentionCutoff.getTime())
|
|
545
|
+
continue;
|
|
546
|
+
await handle.db
|
|
547
|
+
.update(kanbanBoards)
|
|
548
|
+
.set({ purgePendingAt: new Date(), updatedAt: new Date() })
|
|
549
|
+
.where(eq(kanbanBoards.id, r.id));
|
|
550
|
+
marked += 1;
|
|
551
|
+
}
|
|
552
|
+
// —— 阶段二:宽限期满物理清除 ——
|
|
553
|
+
const pendingIds = rows
|
|
554
|
+
.filter((r) => r.purgePendingAt && r.purgePendingAt.getTime() < graceCutoff.getTime())
|
|
555
|
+
.map((r) => r.id);
|
|
556
|
+
let deleted = 0;
|
|
557
|
+
if (pendingIds.length > 0) {
|
|
558
|
+
const removed = await handle.db
|
|
559
|
+
.delete(kanbanBoards)
|
|
560
|
+
.where(inArray(kanbanBoards.id, pendingIds))
|
|
561
|
+
.returning({ id: kanbanBoards.id });
|
|
562
|
+
deleted = removed.length;
|
|
563
|
+
}
|
|
564
|
+
return { marked, deleted };
|
|
565
|
+
}
|
package/dist/llm/index.d.ts
CHANGED
|
@@ -8,6 +8,6 @@ export { builtinCapabilities, createRegistry, DEFAULT_MODEL_CAPABILITIES, overri
|
|
|
8
8
|
export type { Retryable, RetryOptions } from './retry.js';
|
|
9
9
|
export { delay, retryable, withRetry } from './retry.js';
|
|
10
10
|
export type { FallbackChain } from './routing.js';
|
|
11
|
-
export { runWithFallback, shouldFallOver } from './routing.js';
|
|
11
|
+
export { buildFallbackChain, runWithFallback, shouldFallOver } from './routing.js';
|
|
12
12
|
export * from './schema/index.js';
|
|
13
13
|
export { estimateTokens } from './token.js';
|
package/dist/llm/index.js
CHANGED
|
@@ -3,6 +3,6 @@ export { buildInternalRequest, chat, chatStream, toInternalMessage, toStreamChun
|
|
|
3
3
|
export { isContextOverflow, isContextOverflowFailure } from './provider-error.js';
|
|
4
4
|
export { builtinCapabilities, createRegistry, DEFAULT_MODEL_CAPABILITIES, overrideToCapabilities, registerProvider, resolveModelByRole, resolveRoute, setRole, } from './registry.js';
|
|
5
5
|
export { delay, retryable, withRetry } from './retry.js';
|
|
6
|
-
export { runWithFallback, shouldFallOver } from './routing.js';
|
|
6
|
+
export { buildFallbackChain, runWithFallback, shouldFallOver } from './routing.js';
|
|
7
7
|
export * from './schema/index.js';
|
|
8
8
|
export { estimateTokens } from './token.js';
|
package/dist/llm/provider.js
CHANGED
|
@@ -135,7 +135,7 @@ const streamTarget = async function* (ctx, request, target, chain) {
|
|
|
135
135
|
headers: { authorization: `Bearer ${authHeader}`, ...resolved.route.headers() },
|
|
136
136
|
signal: ctx.signal,
|
|
137
137
|
fetchImpl: ctx.fetchImpl,
|
|
138
|
-
}), { maxRetries: chain.maxRetries, sleep: chain.sleep });
|
|
138
|
+
}), { maxRetries: chain.maxRetries, sleep: chain.sleep, initialDelayMs: chain.retryDelay });
|
|
139
139
|
let state = initialStepState();
|
|
140
140
|
for await (const frame of sseFraming(stream)) {
|
|
141
141
|
const chunk = parseChunk(resolved.route.id, frame);
|
package/dist/llm/registry.d.ts
CHANGED
|
@@ -81,7 +81,13 @@ declare const setRole: (registry: Registry, role: ModelRole, provider: string, m
|
|
|
81
81
|
* live request handlers (e.g. config hot-reload).
|
|
82
82
|
*/
|
|
83
83
|
declare const rebuildRegistry: (registry: Registry, builder: (next: Registry) => void) => void;
|
|
84
|
+
/**
|
|
85
|
+
* 内置价目表版本(H3):记录 builtinCapabilities 最近一次人工核价的日期。
|
|
86
|
+
* LLM 价格频繁变动,前端在用量/成本面板展示此版本提醒「估算可能过期」;
|
|
87
|
+
* 发布新版本时同步核对 builtinCapabilities 并更新此常量(release checklist)。
|
|
88
|
+
*/
|
|
89
|
+
declare const PRICE_CATALOG_VERSION = "2026-09-09";
|
|
84
90
|
/** Default role + a starter catalog of well-known models. */
|
|
85
91
|
declare const builtinCapabilities: Record<string, Record<string, ModelCapabilities>>;
|
|
86
92
|
export type { ProviderInput, Registry, ResolveResult, RouteEntry, RouteTable };
|
|
87
|
-
export { builtinCapabilities, createRegistry, DEFAULT_MODEL_CAPABILITIES, overrideToCapabilities, rebuildRegistry, registerProvider, resolveModelByRole, resolveRoute, setRole, };
|
|
93
|
+
export { builtinCapabilities, createRegistry, DEFAULT_MODEL_CAPABILITIES, overrideToCapabilities, PRICE_CATALOG_VERSION, rebuildRegistry, registerProvider, resolveModelByRole, resolveRoute, setRole, };
|
package/dist/llm/registry.js
CHANGED
|
@@ -117,6 +117,12 @@ const rebuildRegistry = (registry, builder) => {
|
|
|
117
117
|
builder(next);
|
|
118
118
|
registry.table = next.table;
|
|
119
119
|
};
|
|
120
|
+
/**
|
|
121
|
+
* 内置价目表版本(H3):记录 builtinCapabilities 最近一次人工核价的日期。
|
|
122
|
+
* LLM 价格频繁变动,前端在用量/成本面板展示此版本提醒「估算可能过期」;
|
|
123
|
+
* 发布新版本时同步核对 builtinCapabilities 并更新此常量(release checklist)。
|
|
124
|
+
*/
|
|
125
|
+
const PRICE_CATALOG_VERSION = '2026-09-09';
|
|
120
126
|
/** Default role + a starter catalog of well-known models. */
|
|
121
127
|
const builtinCapabilities = {
|
|
122
128
|
openai: {
|
|
@@ -151,4 +157,4 @@ const builtinCapabilities = {
|
|
|
151
157
|
},
|
|
152
158
|
},
|
|
153
159
|
};
|
|
154
|
-
export { builtinCapabilities, createRegistry, DEFAULT_MODEL_CAPABILITIES, overrideToCapabilities, rebuildRegistry, registerProvider, resolveModelByRole, resolveRoute, setRole, };
|
|
160
|
+
export { builtinCapabilities, createRegistry, DEFAULT_MODEL_CAPABILITIES, overrideToCapabilities, PRICE_CATALOG_VERSION, rebuildRegistry, registerProvider, resolveModelByRole, resolveRoute, setRole, };
|
package/dist/llm/retry.d.ts
CHANGED
|
@@ -4,9 +4,10 @@ declare const RETRY_MAX_DELAY_NO_HEADERS = 30000;
|
|
|
4
4
|
declare const RETRY_MAX_DELAY = 2147483647;
|
|
5
5
|
/**
|
|
6
6
|
* Compute the delay before the next retry attempt (ms).
|
|
7
|
-
* Honors retry-after / retry-after-ms headers when present, else exponential backoff
|
|
7
|
+
* Honors retry-after / retry-after-ms headers when present, else exponential backoff
|
|
8
|
+
* from the given base delay (defaults to RETRY_INITIAL_DELAY).
|
|
8
9
|
*/
|
|
9
|
-
declare const delay: (attempt: number, error?: unknown) => number;
|
|
10
|
+
declare const delay: (attempt: number, error?: unknown, initialDelayMs?: number) => number;
|
|
10
11
|
/** A normalized, retryable error descriptor for the session layer. */
|
|
11
12
|
type Retryable = {
|
|
12
13
|
message: string;
|
|
@@ -29,6 +30,8 @@ type RetryOptions = {
|
|
|
29
30
|
delayMs: number;
|
|
30
31
|
error: unknown;
|
|
31
32
|
}) => void;
|
|
33
|
+
/** Base delay for exponential backoff (default RETRY_INITIAL_DELAY). */
|
|
34
|
+
initialDelayMs?: number;
|
|
32
35
|
};
|
|
33
36
|
/**
|
|
34
37
|
* Run an async operation with retry. Retries only on retryable LLM errors.
|
package/dist/llm/retry.js
CHANGED
|
@@ -17,9 +17,10 @@ const errorHeaders = (error) => {
|
|
|
17
17
|
const capDelay = (ms) => Math.min(ms, RETRY_MAX_DELAY);
|
|
18
18
|
/**
|
|
19
19
|
* Compute the delay before the next retry attempt (ms).
|
|
20
|
-
* Honors retry-after / retry-after-ms headers when present, else exponential backoff
|
|
20
|
+
* Honors retry-after / retry-after-ms headers when present, else exponential backoff
|
|
21
|
+
* from the given base delay (defaults to RETRY_INITIAL_DELAY).
|
|
21
22
|
*/
|
|
22
|
-
const delay = (attempt, error) => {
|
|
23
|
+
const delay = (attempt, error, initialDelayMs = RETRY_INITIAL_DELAY) => {
|
|
23
24
|
const headers = errorHeaders(error);
|
|
24
25
|
if (headers) {
|
|
25
26
|
const retryAfterMs = headers['retry-after-ms'];
|
|
@@ -37,9 +38,9 @@ const delay = (attempt, error) => {
|
|
|
37
38
|
if (!Number.isNaN(parsed) && parsed > 0)
|
|
38
39
|
return capDelay(Math.ceil(parsed));
|
|
39
40
|
}
|
|
40
|
-
return capDelay(
|
|
41
|
+
return capDelay(initialDelayMs * RETRY_BACKOFF_FACTOR ** (attempt - 1));
|
|
41
42
|
}
|
|
42
|
-
return capDelay(Math.min(
|
|
43
|
+
return capDelay(Math.min(initialDelayMs * RETRY_BACKOFF_FACTOR ** (attempt - 1), RETRY_MAX_DELAY_NO_HEADERS));
|
|
43
44
|
};
|
|
44
45
|
/**
|
|
45
46
|
* Decide whether a thrown error is retryable. Returns undefined when not retryable
|
|
@@ -74,7 +75,7 @@ const withRetry = async (fn, options) => {
|
|
|
74
75
|
const fallbackReason = isLLMError(error)
|
|
75
76
|
? error.reason
|
|
76
77
|
: { _tag: 'InvalidRequest', message: '' };
|
|
77
|
-
const delayMs = Math.min(reasonRetryAfterMs(fallbackReason) ?? delay(attempt, error), canRetry.policy.maxDelay);
|
|
78
|
+
const delayMs = Math.min(reasonRetryAfterMs(fallbackReason) ?? delay(attempt, error, options.initialDelayMs), canRetry.policy.maxDelay);
|
|
78
79
|
options.onRetry?.({ attempt, delayMs, error });
|
|
79
80
|
await sleep(delayMs);
|
|
80
81
|
}
|
package/dist/llm/routing.d.ts
CHANGED
|
@@ -32,5 +32,28 @@ declare const runWithFallback: <T>(registry: Registry, chain: FallbackChain, run
|
|
|
32
32
|
* errors propagate without trying fallbacks.
|
|
33
33
|
*/
|
|
34
34
|
declare const shouldFallOver: (error: unknown) => boolean;
|
|
35
|
+
/** 回退目标所需的最小 provider 信息。 */
|
|
36
|
+
type FallbackSourceProvider = {
|
|
37
|
+
name?: string;
|
|
38
|
+
_tag?: string;
|
|
39
|
+
/** 声明了模型清单的 provider 只对清单内的模型回退;未声明视为不支持(保守)。 */
|
|
40
|
+
models?: Record<string, unknown>;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* 从 config.fallback 构建回退链(主 chat 循环/压缩/标题等 LLM 调用的接线点)。
|
|
44
|
+
* - enabled=false → undefined(保持内置默认:3 次重试、无跨 provider 回退)。
|
|
45
|
+
* - enabled=true → 按配置 maxRetries/retryDelay 重试,并对「声明了同一模型」的
|
|
46
|
+
* 其他 provider 依次回退。无合格回退目标时 fallbacks 为空——重试参数仍然生效。
|
|
47
|
+
* 回退只接受声明了该模型的 provider:对未声明的 provider 盲目回退会在其 API 处
|
|
48
|
+
* 得到 InvalidRequest 并中断整条链(shouldFallOver 不放过非回退错误)。
|
|
49
|
+
*/
|
|
50
|
+
declare function buildFallbackChain(config: {
|
|
51
|
+
providers: FallbackSourceProvider[];
|
|
52
|
+
fallback: {
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
maxRetries: number;
|
|
55
|
+
retryDelay: number;
|
|
56
|
+
};
|
|
57
|
+
}, provider: string, model: string): FallbackChain | undefined;
|
|
35
58
|
export type { FallbackChain };
|
|
36
|
-
export { runWithFallback, shouldFallOver };
|
|
59
|
+
export { buildFallbackChain, runWithFallback, shouldFallOver };
|