dskcode 0.1.29 → 0.1.30
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/dist/index.js +941 -236
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -807,7 +807,7 @@ var LOGO_LINES = [
|
|
|
807
807
|
// src/ui/ChatSession.tsx
|
|
808
808
|
import { Box as Box9, Text as Text10, useInput, Static } from "ink";
|
|
809
809
|
import TextInput from "ink-text-input";
|
|
810
|
-
import { useEffect as
|
|
810
|
+
import { useEffect as useEffect4, useState as useState4, useCallback as useCallback2, useMemo, useRef as useRef3 } from "react";
|
|
811
811
|
|
|
812
812
|
// src/ui/useDoubleCtrlC.ts
|
|
813
813
|
import { useState as useState2, useCallback, useEffect as useEffect2, useRef } from "react";
|
|
@@ -842,6 +842,7 @@ import InkSpinner2 from "ink-spinner";
|
|
|
842
842
|
|
|
843
843
|
// src/ui/AssistantMessage.tsx
|
|
844
844
|
import { Box as Box5, Text as Text6 } from "ink";
|
|
845
|
+
import { useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
|
|
845
846
|
|
|
846
847
|
// src/provider/cost-tracker.ts
|
|
847
848
|
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
@@ -1674,12 +1675,90 @@ function HighlightedText({ children: text }) {
|
|
|
1674
1675
|
}
|
|
1675
1676
|
|
|
1676
1677
|
// src/ui/AssistantMessage.tsx
|
|
1677
|
-
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1678
|
+
import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1678
1679
|
function formatElapsed(ms) {
|
|
1679
1680
|
if (ms < 1e3) return `${ms}ms`;
|
|
1680
1681
|
const seconds = (ms / 1e3).toFixed(1);
|
|
1681
1682
|
return `${seconds}s`;
|
|
1682
1683
|
}
|
|
1684
|
+
function AnimatedUsage({ usage, cost }) {
|
|
1685
|
+
const targetTokens = usage ? usage.promptTokens + usage.completionTokens : 0;
|
|
1686
|
+
const targetCost = cost ?? 0;
|
|
1687
|
+
const [displayedTokens, setDisplayedTokens] = useState3(targetTokens);
|
|
1688
|
+
const [displayedCost, setDisplayedCost] = useState3(targetCost);
|
|
1689
|
+
const startRef = useRef2(null);
|
|
1690
|
+
const rafRef = useRef2(null);
|
|
1691
|
+
useEffect3(() => {
|
|
1692
|
+
if (startRef.current && startRef.current.toTokens === targetTokens && startRef.current.toCost === targetCost) {
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
if (targetTokens <= displayedTokens && targetCost <= displayedCost) {
|
|
1696
|
+
setDisplayedTokens(targetTokens);
|
|
1697
|
+
setDisplayedCost(targetCost);
|
|
1698
|
+
startRef.current = {
|
|
1699
|
+
fromTokens: targetTokens,
|
|
1700
|
+
fromCost: targetCost,
|
|
1701
|
+
toTokens: targetTokens,
|
|
1702
|
+
toCost: targetCost,
|
|
1703
|
+
startMs: Date.now(),
|
|
1704
|
+
durationMs: 0
|
|
1705
|
+
};
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
const tokensDelta = targetTokens - displayedTokens;
|
|
1709
|
+
const costDelta = targetCost - displayedCost;
|
|
1710
|
+
const durationMs = Math.min(
|
|
1711
|
+
600,
|
|
1712
|
+
Math.max(220, Math.max(tokensDelta, 0) * 1.2 + 220)
|
|
1713
|
+
);
|
|
1714
|
+
startRef.current = {
|
|
1715
|
+
fromTokens: displayedTokens,
|
|
1716
|
+
fromCost: displayedCost,
|
|
1717
|
+
toTokens: targetTokens,
|
|
1718
|
+
toCost: targetCost,
|
|
1719
|
+
startMs: Date.now(),
|
|
1720
|
+
durationMs
|
|
1721
|
+
};
|
|
1722
|
+
if (rafRef.current) clearInterval(rafRef.current);
|
|
1723
|
+
rafRef.current = setInterval(() => {
|
|
1724
|
+
const s = startRef.current;
|
|
1725
|
+
if (!s) return;
|
|
1726
|
+
const elapsed = Date.now() - s.startMs;
|
|
1727
|
+
if (elapsed >= s.durationMs) {
|
|
1728
|
+
setDisplayedTokens(s.toTokens);
|
|
1729
|
+
setDisplayedCost(s.toCost);
|
|
1730
|
+
if (rafRef.current) {
|
|
1731
|
+
clearInterval(rafRef.current);
|
|
1732
|
+
rafRef.current = null;
|
|
1733
|
+
}
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
const t = elapsed / s.durationMs;
|
|
1737
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
1738
|
+
const tokens = s.fromTokens + (s.toTokens - s.fromTokens) * eased;
|
|
1739
|
+
const cost2 = s.fromCost + (s.toCost - s.fromCost) * eased;
|
|
1740
|
+
setDisplayedTokens(tokens);
|
|
1741
|
+
setDisplayedCost(cost2);
|
|
1742
|
+
}, 33);
|
|
1743
|
+
return () => {
|
|
1744
|
+
if (rafRef.current) {
|
|
1745
|
+
clearInterval(rafRef.current);
|
|
1746
|
+
rafRef.current = null;
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
}, [targetTokens, targetCost]);
|
|
1750
|
+
return /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
1751
|
+
usage && /* @__PURE__ */ jsxs5(Text6, { color: "#888888", children: [
|
|
1752
|
+
Math.round(displayedTokens).toLocaleString(),
|
|
1753
|
+
" tokens"
|
|
1754
|
+
] }),
|
|
1755
|
+
cost !== void 0 && cost > 0 && /* @__PURE__ */ jsxs5(Text6, { color: "#888888", children: [
|
|
1756
|
+
" \xB7 ",
|
|
1757
|
+
"\xA5",
|
|
1758
|
+
displayedCost.toFixed(4)
|
|
1759
|
+
] })
|
|
1760
|
+
] });
|
|
1761
|
+
}
|
|
1683
1762
|
function AssistantMessage({
|
|
1684
1763
|
content,
|
|
1685
1764
|
toolCalls,
|
|
@@ -1701,6 +1780,11 @@ function AssistantMessage({
|
|
|
1701
1780
|
] })
|
|
1702
1781
|
] }),
|
|
1703
1782
|
toolCalls && toolCalls.length > 0 && /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", children: toolCalls.map((tc, i) => /* @__PURE__ */ jsx5(ToolCallBlock, { call: tc }, tc.id ?? i)) }),
|
|
1783
|
+
isStreaming && (usage || cost !== void 0) && /* @__PURE__ */ jsx5(Box5, { flexDirection: "row", marginTop: 1, marginLeft: 3, children: /* @__PURE__ */ jsxs5(Text6, { color: "#666666", dimColor: true, children: [
|
|
1784
|
+
"\u23F3 \u5DF2\u6D88\u8017",
|
|
1785
|
+
" ",
|
|
1786
|
+
/* @__PURE__ */ jsx5(AnimatedUsage, { usage, cost })
|
|
1787
|
+
] }) }),
|
|
1704
1788
|
!isStreaming && (usage || elapsed !== void 0) && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, marginLeft: 3, children: [
|
|
1705
1789
|
/* @__PURE__ */ jsx5(Text6, { color: "#555555", children: "\u2500".repeat(36) }),
|
|
1706
1790
|
/* @__PURE__ */ jsxs5(Box5, { flexDirection: "row", gap: 2, children: [
|
|
@@ -2114,6 +2198,389 @@ var ToolRegistry = class {
|
|
|
2114
2198
|
}
|
|
2115
2199
|
};
|
|
2116
2200
|
|
|
2201
|
+
// src/agent/tool-executor.ts
|
|
2202
|
+
var ToolExecutor = class {
|
|
2203
|
+
/** 工具注册表(只读引用) */
|
|
2204
|
+
#registry;
|
|
2205
|
+
/** 权限门(只读引用) */
|
|
2206
|
+
#gate;
|
|
2207
|
+
/** 工具执行上下文(每次执行都共用) */
|
|
2208
|
+
#baseCtx;
|
|
2209
|
+
/** 并行执行的最大同时执行数(默认 8) */
|
|
2210
|
+
#maxParallel;
|
|
2211
|
+
/**
|
|
2212
|
+
* 构造一个 ToolExecutor。
|
|
2213
|
+
*
|
|
2214
|
+
* @param deps — 注入依赖(registry / gate / baseCtx / maxParallel)
|
|
2215
|
+
* @pure 仅保存引用,不调用任何外部 IO
|
|
2216
|
+
*/
|
|
2217
|
+
constructor(deps) {
|
|
2218
|
+
this.#registry = deps.registry;
|
|
2219
|
+
this.#gate = deps.gate;
|
|
2220
|
+
this.#baseCtx = {
|
|
2221
|
+
cwd: deps.baseCtx.cwd,
|
|
2222
|
+
signal: deps.baseCtx.signal,
|
|
2223
|
+
writeRoots: deps.baseCtx.writeRoots,
|
|
2224
|
+
...deps.baseCtx.timeout !== void 0 ? { timeout: deps.baseCtx.timeout } : {}
|
|
2225
|
+
};
|
|
2226
|
+
this.#maxParallel = deps.maxParallel ?? 8;
|
|
2227
|
+
}
|
|
2228
|
+
/**
|
|
2229
|
+
* 执行一批工具调用。
|
|
2230
|
+
*
|
|
2231
|
+
* 自动选择并行 / 串行:
|
|
2232
|
+
* - 全部为只读工具 且 calls.length > 1:分批并行(每批最多 #maxParallel 个)
|
|
2233
|
+
* - 其他:严格串行
|
|
2234
|
+
*
|
|
2235
|
+
* @param calls — LLM 决定的本轮工具调用列表
|
|
2236
|
+
* @returns items(全部结果)+ records(仅失败项,用于风暴检测)
|
|
2237
|
+
*
|
|
2238
|
+
* @sideEffect 调用 registry / gate / tool.execute;不修改 Session.messages
|
|
2239
|
+
*/
|
|
2240
|
+
async executeBatch(calls) {
|
|
2241
|
+
if (calls.length === 0) return { items: [], records: [] };
|
|
2242
|
+
if (this.#allReadOnly(calls) && calls.length > 1) {
|
|
2243
|
+
return this.#executeParallel(calls);
|
|
2244
|
+
}
|
|
2245
|
+
return this.#executeSequential(calls);
|
|
2246
|
+
}
|
|
2247
|
+
// -------------------------------------------------------------------------
|
|
2248
|
+
// 内部
|
|
2249
|
+
// -------------------------------------------------------------------------
|
|
2250
|
+
/**
|
|
2251
|
+
* 判定这一批调用是否全部为只读工具。
|
|
2252
|
+
* 工具未找到时按"只读"处理(不会因为没注册就拒绝并行)。
|
|
2253
|
+
*
|
|
2254
|
+
* @pure 不修改任何状态
|
|
2255
|
+
*/
|
|
2256
|
+
#allReadOnly(calls) {
|
|
2257
|
+
return calls.every((tc) => {
|
|
2258
|
+
const tool = this.#registry.get(tc.name);
|
|
2259
|
+
return tool ? isReadOnly(tool.kind) : true;
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* 并行执行:按 #maxParallel 分批,每批内 Promise.all。
|
|
2264
|
+
* 用于"全部只读 + 多于 1 个"的场景。
|
|
2265
|
+
*
|
|
2266
|
+
* @param calls — 全部只读的工具调用
|
|
2267
|
+
* @returns items + records
|
|
2268
|
+
*
|
|
2269
|
+
* @sideEffect 调用 tool.execute
|
|
2270
|
+
*/
|
|
2271
|
+
async #executeParallel(calls) {
|
|
2272
|
+
const items = [];
|
|
2273
|
+
const records = [];
|
|
2274
|
+
for (let i = 0; i < calls.length; i += this.#maxParallel) {
|
|
2275
|
+
const batch = calls.slice(i, i + this.#maxParallel);
|
|
2276
|
+
const results = await Promise.all(batch.map((tc) => this.#executeOne(tc)));
|
|
2277
|
+
for (const r of results) {
|
|
2278
|
+
items.push(r.item);
|
|
2279
|
+
if (!r.record.success) records.push(r.record);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
return { items, records };
|
|
2283
|
+
}
|
|
2284
|
+
/**
|
|
2285
|
+
* 串行执行:按 calls 顺序逐个执行。
|
|
2286
|
+
* 用于含写工具的场景(保证副作用顺序与 LLM 给出顺序一致)。
|
|
2287
|
+
*
|
|
2288
|
+
* @param calls — 工具调用列表
|
|
2289
|
+
* @returns items + records
|
|
2290
|
+
*
|
|
2291
|
+
* @sideEffect 按序调用 tool.execute
|
|
2292
|
+
*/
|
|
2293
|
+
async #executeSequential(calls) {
|
|
2294
|
+
const items = [];
|
|
2295
|
+
const records = [];
|
|
2296
|
+
for (const tc of calls) {
|
|
2297
|
+
const r = await this.#executeOne(tc);
|
|
2298
|
+
items.push(r.item);
|
|
2299
|
+
if (!r.record.success) records.push(r.record);
|
|
2300
|
+
}
|
|
2301
|
+
return { items, records };
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* 执行单个工具调用,包含查找 / 参数解析 / 权限门 / 写工具 preview / 异常捕获。
|
|
2305
|
+
*
|
|
2306
|
+
* 失败映射:
|
|
2307
|
+
* - 工具不存在或被禁用 → `TOOL_NOT_FOUND`
|
|
2308
|
+
* - 权限门拒绝 → `GATE_DENIED`
|
|
2309
|
+
* - 工具 execute 抛错 → `EXECUTION_ERROR`
|
|
2310
|
+
*
|
|
2311
|
+
* @param tc — 单个工具调用(含 id / name / arguments 字符串)
|
|
2312
|
+
* @returns { item, record } — item 喂回 Session;record 用于风暴检测
|
|
2313
|
+
*
|
|
2314
|
+
* @sideEffect 调 tool.execute、可能调 tool.preview;不抛错给调用方
|
|
2315
|
+
*/
|
|
2316
|
+
async #executeOne(tc) {
|
|
2317
|
+
const toolName = tc.name;
|
|
2318
|
+
const timestamp = Date.now();
|
|
2319
|
+
const tool = this.#registry.get(toolName);
|
|
2320
|
+
if (!tool) {
|
|
2321
|
+
const errMsg = `\u5DE5\u5177 "${toolName}" \u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u7981\u7528`;
|
|
2322
|
+
return {
|
|
2323
|
+
item: { name: toolName, callId: tc.id, result: { success: false, data: errMsg, error: "TOOL_NOT_FOUND" } },
|
|
2324
|
+
record: { name: toolName, success: false, error: "TOOL_NOT_FOUND", timestamp }
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
let toolArgs;
|
|
2328
|
+
try {
|
|
2329
|
+
toolArgs = tc.arguments ? JSON.parse(tc.arguments) : {};
|
|
2330
|
+
} catch {
|
|
2331
|
+
toolArgs = {};
|
|
2332
|
+
}
|
|
2333
|
+
const gateResult = await this.#gate.check(toolName, toolArgs);
|
|
2334
|
+
if (!gateResult) {
|
|
2335
|
+
const errMsg = `\u5DE5\u5177 "${toolName}" \u88AB\u6743\u9650\u95E8\u62D2\u7EDD`;
|
|
2336
|
+
return {
|
|
2337
|
+
item: { name: toolName, callId: tc.id, result: { success: false, data: errMsg, error: "GATE_DENIED" } },
|
|
2338
|
+
record: { name: toolName, success: false, error: "GATE_DENIED", timestamp }
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
if (!isReadOnly(tool.kind)) {
|
|
2342
|
+
const maybePreview = tool.preview;
|
|
2343
|
+
if (typeof maybePreview === "function") {
|
|
2344
|
+
try {
|
|
2345
|
+
await maybePreview(toolArgs, this.#baseCtx);
|
|
2346
|
+
} catch {
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
try {
|
|
2351
|
+
const result = await this.#invokeTool(tool, toolArgs);
|
|
2352
|
+
return {
|
|
2353
|
+
item: { name: toolName, callId: tc.id, result },
|
|
2354
|
+
record: { name: toolName, success: result.success, error: result.error, timestamp }
|
|
2355
|
+
};
|
|
2356
|
+
} catch (err) {
|
|
2357
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2358
|
+
const errorResult = {
|
|
2359
|
+
success: false,
|
|
2360
|
+
data: `\u5DE5\u5177 "${toolName}" \u6267\u884C\u5F02\u5E38\uFF1A${message}`,
|
|
2361
|
+
error: "EXECUTION_ERROR"
|
|
2362
|
+
};
|
|
2363
|
+
return {
|
|
2364
|
+
item: { name: toolName, callId: tc.id, result: errorResult },
|
|
2365
|
+
record: { name: toolName, success: false, error: "EXECUTION_ERROR", timestamp }
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
/**
|
|
2370
|
+
* 真正调 tool.execute 的薄包装(拆出来便于以后插入横切关注点,如重试 / 计时)。
|
|
2371
|
+
*
|
|
2372
|
+
* @param tool — 类型擦除后的工具
|
|
2373
|
+
* @param args — 解析后的参数(unknown)
|
|
2374
|
+
* @returns 工具的 ToolResult
|
|
2375
|
+
*
|
|
2376
|
+
* @sideEffect 调 tool.execute
|
|
2377
|
+
*/
|
|
2378
|
+
async #invokeTool(tool, args) {
|
|
2379
|
+
return tool.execute(args, this.#baseCtx);
|
|
2380
|
+
}
|
|
2381
|
+
};
|
|
2382
|
+
|
|
2383
|
+
// src/agent/storm-detector.ts
|
|
2384
|
+
var StormDetector = class {
|
|
2385
|
+
/** 触发风暴需要的最少连续失败次数(默认 3) */
|
|
2386
|
+
#threshold;
|
|
2387
|
+
/**
|
|
2388
|
+
* 构造一个 StormDetector。
|
|
2389
|
+
*
|
|
2390
|
+
* @param opts — 判定选项(threshold)
|
|
2391
|
+
* @pure 仅保存配置
|
|
2392
|
+
*/
|
|
2393
|
+
constructor(opts = {}) {
|
|
2394
|
+
this.#threshold = opts.threshold ?? 3;
|
|
2395
|
+
}
|
|
2396
|
+
/**
|
|
2397
|
+
* 判定本轮是否应该中断风暴。
|
|
2398
|
+
*
|
|
2399
|
+
* @param recentRecords — 历史的失败记录(不含成功的;通常来自 ToolExecutor 的 records)
|
|
2400
|
+
* @param currentCalls — 本轮 LLM 决定调用的工具
|
|
2401
|
+
* @returns true 表示触发风暴,调用方应放弃本轮工具执行并提示切换策略
|
|
2402
|
+
*
|
|
2403
|
+
* @pure 不修改任何状态,仅做比较判断
|
|
2404
|
+
*/
|
|
2405
|
+
shouldBreak(recentRecords, currentCalls) {
|
|
2406
|
+
if (recentRecords.length < this.#threshold) return false;
|
|
2407
|
+
const lastN = recentRecords.slice(-this.#threshold);
|
|
2408
|
+
if (lastN.length < this.#threshold) return false;
|
|
2409
|
+
const first = lastN[0];
|
|
2410
|
+
const allSame = lastN.every((r) => r.name === first.name && r.error === first.error && !r.success);
|
|
2411
|
+
if (!allSame) return false;
|
|
2412
|
+
return currentCalls.some((tc) => tc.name === first.name);
|
|
2413
|
+
}
|
|
2414
|
+
};
|
|
2415
|
+
|
|
2416
|
+
// src/agent/reflector.ts
|
|
2417
|
+
var Reflector = class {
|
|
2418
|
+
/** 触发 R1 需要的最小连续失败次数 */
|
|
2419
|
+
#repeatThreshold;
|
|
2420
|
+
/** 注入到 prompt 的最大反射数 */
|
|
2421
|
+
#maxReflections;
|
|
2422
|
+
/** 单条 hint 的最大字符数 */
|
|
2423
|
+
#maxHintChars;
|
|
2424
|
+
/**
|
|
2425
|
+
* 构造一个 Reflector。
|
|
2426
|
+
*
|
|
2427
|
+
* @param options — 限流与阈值配置
|
|
2428
|
+
* @pure 仅保存配置
|
|
2429
|
+
*/
|
|
2430
|
+
constructor(options = {}) {
|
|
2431
|
+
this.#repeatThreshold = options.repeatThreshold ?? 2;
|
|
2432
|
+
this.#maxReflections = options.maxReflections ?? 5;
|
|
2433
|
+
this.#maxHintChars = options.maxHintChars ?? 800;
|
|
2434
|
+
}
|
|
2435
|
+
/**
|
|
2436
|
+
* 分析一批工具执行结果,返回要注入的反射列表。
|
|
2437
|
+
*
|
|
2438
|
+
* 行为:
|
|
2439
|
+
* - 成功项直接跳过
|
|
2440
|
+
* - 失败项按 R1 → R2 → R3 → R4 顺序检查,先命中先用,单条目最多产生 1 条
|
|
2441
|
+
* - 总数超过 maxReflections 时截断到前 N 条
|
|
2442
|
+
*
|
|
2443
|
+
* @param items — 本轮工具执行结果列表
|
|
2444
|
+
* @param ctx — 上下文(writeRoots / cwd,用于生成 hint)
|
|
2445
|
+
* @returns 反射列表(可能为空;最多 maxReflections 条)
|
|
2446
|
+
*
|
|
2447
|
+
* @pure 不修改任何状态
|
|
2448
|
+
*/
|
|
2449
|
+
analyze(items, ctx) {
|
|
2450
|
+
const reflections = [];
|
|
2451
|
+
for (const item of items) {
|
|
2452
|
+
if (item.result.success) continue;
|
|
2453
|
+
const r = this.#ruleRepeatedFailure(item) ?? this.#ruleFileNotFound(item) ?? this.#rulePermissionDenied(item) ?? this.#ruleOutOfWriteRoot(item, ctx.writeRoots);
|
|
2454
|
+
if (r) reflections.push(r);
|
|
2455
|
+
if (reflections.length >= this.#maxReflections) break;
|
|
2456
|
+
}
|
|
2457
|
+
return reflections;
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* 把反射列表拼成"反思"section,追加到 system prompt 末尾。
|
|
2461
|
+
*
|
|
2462
|
+
* 输出格式:
|
|
2463
|
+
* ```
|
|
2464
|
+
* {原 prompt}
|
|
2465
|
+
*
|
|
2466
|
+
* ## ⚠️ 上一轮工具调用的反思
|
|
2467
|
+
* - 工具 `<tool>`:<hint>
|
|
2468
|
+
* - 工具 `<tool>`:<hint>
|
|
2469
|
+
* ```
|
|
2470
|
+
*
|
|
2471
|
+
* @param systemPrompt — 原始 system prompt
|
|
2472
|
+
* @param reflections — 反射列表(空数组时原样返回)
|
|
2473
|
+
* @returns 拼装后的新 prompt
|
|
2474
|
+
*
|
|
2475
|
+
* @pure 不修改入参
|
|
2476
|
+
*/
|
|
2477
|
+
injectIntoPrompt(systemPrompt, reflections) {
|
|
2478
|
+
if (reflections.length === 0) return systemPrompt;
|
|
2479
|
+
const lines = reflections.map((r) => {
|
|
2480
|
+
const hint = this.#truncate(r.hint);
|
|
2481
|
+
return `- \u5DE5\u5177 \`${r.toolName}\` \u5931\u8D25\uFF1A${hint}`;
|
|
2482
|
+
});
|
|
2483
|
+
const section = [
|
|
2484
|
+
"## \u26A0\uFE0F \u4E0A\u4E00\u8F6E\u5DE5\u5177\u8C03\u7528\u7684\u53CD\u601D",
|
|
2485
|
+
"\u4EE5\u4E0B\u662F\u672C\u8F6E\u5DE5\u5177\u6267\u884C\u5931\u8D25\u7684\u539F\u56E0\u5206\u6790\u3002\u8BF7\u5728\u4E0B\u4E00\u6B21\u8C03\u7528\u524D\u5148\u89E3\u51B3\u8FD9\u4E9B\u95EE\u9898\uFF0C\u4E0D\u8981\u91CD\u590D\u76F8\u540C\u7684\u9519\u8BEF\u8C03\u7528\u3002",
|
|
2486
|
+
...lines
|
|
2487
|
+
].join("\n");
|
|
2488
|
+
return systemPrompt + "\n\n" + section;
|
|
2489
|
+
}
|
|
2490
|
+
// -------------------------------------------------------------------------
|
|
2491
|
+
// 规则实现(按 R1 → R2 → R3 → R4 顺序)
|
|
2492
|
+
// -------------------------------------------------------------------------
|
|
2493
|
+
/**
|
|
2494
|
+
* R1 连续失败:同一工具连续 ≥#repeatThreshold 次相同错误码失败。
|
|
2495
|
+
*
|
|
2496
|
+
* @pure 仅读入参
|
|
2497
|
+
*/
|
|
2498
|
+
#ruleRepeatedFailure(item) {
|
|
2499
|
+
const recent = item.recentSameTool;
|
|
2500
|
+
if (recent.length < this.#repeatThreshold) return null;
|
|
2501
|
+
const lastN = recent.slice(-this.#repeatThreshold);
|
|
2502
|
+
const first = lastN[0];
|
|
2503
|
+
if (!lastN.every((r) => !r.success && r.error === first.error)) {
|
|
2504
|
+
return null;
|
|
2505
|
+
}
|
|
2506
|
+
const errText = first.error ?? "\u672A\u77E5\u9519\u8BEF";
|
|
2507
|
+
return {
|
|
2508
|
+
category: "repeated_failure",
|
|
2509
|
+
toolName: item.name,
|
|
2510
|
+
hint: `\u4F60\u5DF2\u8FDE\u7EED ${this.#repeatThreshold} \u6B21\u8C03\u7528 \`${item.name}\` \u5931\u8D25\uFF08\u9519\u8BEF\uFF1A${errText}\uFF09\u3002\u8BF7\u5148 \`read_file\` \u770B\u6E05\u73B0\u573A\u6216\u6362\u5176\u4ED6\u5DE5\u5177\uFF0C\u4E0D\u8981\u7EE7\u7EED\u540C\u6837\u7684\u9519\u8BEF\u8C03\u7528\u3002`
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
/**
|
|
2514
|
+
* R2 文件不存在:TOOL_NOT_FOUND 错误码 或 data 含 ENOENT / not found / No such file。
|
|
2515
|
+
*
|
|
2516
|
+
* @pure 仅读入参
|
|
2517
|
+
*/
|
|
2518
|
+
#ruleFileNotFound(item) {
|
|
2519
|
+
const isToolNotFound = item.result.error === "TOOL_NOT_FOUND";
|
|
2520
|
+
const data = item.result.data ?? "";
|
|
2521
|
+
const hasKeyword = data.includes("ENOENT") || data.includes("No such file") || /\bnot found\b/i.test(data);
|
|
2522
|
+
if (!isToolNotFound && !hasKeyword) return null;
|
|
2523
|
+
return {
|
|
2524
|
+
category: "file_not_found",
|
|
2525
|
+
toolName: item.name,
|
|
2526
|
+
hint: `\`${item.name}\` \u5931\u8D25\uFF1A\u6587\u4EF6\u4E0D\u5B58\u5728\u3002\u8BF7\u5148\u7528 \`ls\` / \`glob\` \u770B\u4E00\u4E0B\u76EE\u5F55\u7ED3\u6784\uFF0C\u6216\u786E\u8BA4\u8DEF\u5F84\u62FC\u5199\uFF08\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u89C1 system prompt\uFF09\u3002`
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
/**
|
|
2530
|
+
* R3 权限拒绝:GATE_DENIED 错误码 或 data 含 EACCES / permission / denied(不区分大小写)。
|
|
2531
|
+
*
|
|
2532
|
+
* @pure 仅读入参
|
|
2533
|
+
*/
|
|
2534
|
+
#rulePermissionDenied(item) {
|
|
2535
|
+
const isGateDenied = item.result.error === "GATE_DENIED";
|
|
2536
|
+
const data = item.result.data ?? "";
|
|
2537
|
+
const hasKeyword = data.includes("EACCES") || /\bpermission\b/i.test(data) || /\bdenied\b/i.test(data);
|
|
2538
|
+
if (!isGateDenied && !hasKeyword) return null;
|
|
2539
|
+
return {
|
|
2540
|
+
category: "permission_denied",
|
|
2541
|
+
toolName: item.name,
|
|
2542
|
+
hint: `\`${item.name}\` \u88AB\u6743\u9650\u7CFB\u7EDF\u62D2\u7EDD\u3002\u8BF7\u68C0\u67E5\uFF1A1) \u8DEF\u5F84\u662F\u5426\u5728\u5141\u8BB8\u6839\u76EE\u5F55\u5185\uFF1B2) \u662F\u5426\u9700\u8981\u5207\u6362\u6388\u6743\u6A21\u5F0F\uFF1B3) \u6539\u7528\u5176\u4ED6\u4E0D\u9700\u8981\u6743\u9650\u7684\u5DE5\u5177\u3002`
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
/**
|
|
2546
|
+
* R4 写根外:kind ∈ Edit/Delete/Move 且失败。
|
|
2547
|
+
*
|
|
2548
|
+
* @pure 仅读入参
|
|
2549
|
+
*/
|
|
2550
|
+
#ruleOutOfWriteRoot(item, writeRoots) {
|
|
2551
|
+
const isWriteKind = item.kind === "edit" /* Edit */ || item.kind === "delete" /* Delete */ || item.kind === "move" /* Move */;
|
|
2552
|
+
if (!isWriteKind) return null;
|
|
2553
|
+
const primary = writeRoots[0] ?? "<\u672A\u914D\u7F6E>";
|
|
2554
|
+
return {
|
|
2555
|
+
category: "out_of_write_root",
|
|
2556
|
+
toolName: item.name,
|
|
2557
|
+
hint: `\`${item.name}\` \u662F\u5199\u64CD\u4F5C\u4F46\u76EE\u6807\u8DEF\u5F84\u4E0D\u5728\u5141\u8BB8\u7684\u6839\u76EE\u5F55\u5185\u3002\u8BF7\u6539\u7528 \`${primary}\` \u4E0B\u7684\u8DEF\u5F84\uFF0C\u6216\u8C03\u6574\u9879\u76EE\u7684 \`writeRoots\` \u914D\u7F6E\u3002`
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
/**
|
|
2561
|
+
* 截断超长 hint,末尾加省略号。
|
|
2562
|
+
*
|
|
2563
|
+
* @pure 字符串处理
|
|
2564
|
+
*/
|
|
2565
|
+
#truncate(text) {
|
|
2566
|
+
if (text.length <= this.#maxHintChars) return text;
|
|
2567
|
+
return text.slice(0, this.#maxHintChars - 3) + "...";
|
|
2568
|
+
}
|
|
2569
|
+
};
|
|
2570
|
+
|
|
2571
|
+
// src/agent/tool-definitions.ts
|
|
2572
|
+
function buildToolDefinitions(registry2, mode) {
|
|
2573
|
+
const tools = mode === "plan" ? registry2.listReadTools() : registry2.list();
|
|
2574
|
+
return tools.map((t) => ({
|
|
2575
|
+
type: "function",
|
|
2576
|
+
function: {
|
|
2577
|
+
name: t.name,
|
|
2578
|
+
description: t.description,
|
|
2579
|
+
parameters: t.parameters
|
|
2580
|
+
}
|
|
2581
|
+
}));
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2117
2584
|
// src/checkpoint/git-checkpoint.ts
|
|
2118
2585
|
import { execFile } from "child_process";
|
|
2119
2586
|
import { promisify } from "util";
|
|
@@ -2279,7 +2746,9 @@ function isENOENT(err) {
|
|
|
2279
2746
|
|
|
2280
2747
|
// src/logger/logger.ts
|
|
2281
2748
|
import { mkdir as mkdir5, appendFile } from "fs/promises";
|
|
2282
|
-
import {
|
|
2749
|
+
import { fileURLToPath } from "url";
|
|
2750
|
+
import { join as join5, basename, sep } from "path";
|
|
2751
|
+
var LOGGER_DIR = "logger";
|
|
2283
2752
|
var MAX_DATA_LEN = 2e3;
|
|
2284
2753
|
var MAX_QUEUE = 500;
|
|
2285
2754
|
function defaultLogsDir() {
|
|
@@ -2331,14 +2800,19 @@ var ConversationLogger = class _ConversationLogger {
|
|
|
2331
2800
|
/**
|
|
2332
2801
|
* 记录一个事件。
|
|
2333
2802
|
*
|
|
2334
|
-
* 内部将事件序列化为 JSON
|
|
2803
|
+
* 内部将事件序列化为 pretty JSON 并以多行块的形式追加到日志文件。
|
|
2335
2804
|
* 写入操作串行排队,保证事件顺序。调用方无需 await。
|
|
2336
2805
|
*/
|
|
2337
2806
|
log(event) {
|
|
2338
2807
|
if (!this.#enabled || this.#closed) return;
|
|
2339
2808
|
if (this.#queueLen >= MAX_QUEUE) return;
|
|
2809
|
+
const enriched = {
|
|
2810
|
+
...event,
|
|
2811
|
+
time: event.time ?? formatTime(event.ts),
|
|
2812
|
+
loc: event.loc ?? captureCallerLocation()
|
|
2813
|
+
};
|
|
2340
2814
|
this.#queueLen++;
|
|
2341
|
-
this.#queue = this.#queue.then(() => this.#
|
|
2815
|
+
this.#queue = this.#queue.then(() => this.#writeBlock(enriched)).catch(() => {
|
|
2342
2816
|
}).finally(() => this.#queueLen--);
|
|
2343
2817
|
}
|
|
2344
2818
|
/**
|
|
@@ -2353,12 +2827,12 @@ var ConversationLogger = class _ConversationLogger {
|
|
|
2353
2827
|
});
|
|
2354
2828
|
_ConversationLogger.#instances.delete(this);
|
|
2355
2829
|
}
|
|
2356
|
-
/**
|
|
2357
|
-
async #
|
|
2830
|
+
/** 将一条事件以多行块的形式写入日志文件 */
|
|
2831
|
+
async #writeBlock(event) {
|
|
2358
2832
|
const dir = join5(this.#logPath, "..");
|
|
2359
2833
|
await mkdir5(dir, { recursive: true });
|
|
2360
|
-
const
|
|
2361
|
-
await appendFile(this.#logPath,
|
|
2834
|
+
const block = formatBlock(event);
|
|
2835
|
+
await appendFile(this.#logPath, block, "utf-8");
|
|
2362
2836
|
}
|
|
2363
2837
|
// -----------------------------------------------------------------------
|
|
2364
2838
|
// 便捷方法 — 封装常见事件类型,减少调用方样板代码
|
|
@@ -2418,28 +2892,123 @@ var ConversationLogger = class _ConversationLogger {
|
|
|
2418
2892
|
logSessionEnd(elapsed) {
|
|
2419
2893
|
this.log({ ts: Date.now(), type: "session_end", elapsed });
|
|
2420
2894
|
}
|
|
2895
|
+
/** 记录反射(工具失败归因注入到下一轮 prompt 时) */
|
|
2896
|
+
logReflections(items) {
|
|
2897
|
+
this.log({ ts: Date.now(), type: "reflection", items });
|
|
2898
|
+
}
|
|
2421
2899
|
};
|
|
2422
2900
|
function truncate(text) {
|
|
2423
2901
|
if (text.length <= MAX_DATA_LEN) return text;
|
|
2424
2902
|
return text.slice(0, MAX_DATA_LEN) + `...[\u5DF2\u622A\u65AD\uFF0C\u539F\u59CB\u957F\u5EA6 ${text.length}]`;
|
|
2425
2903
|
}
|
|
2904
|
+
function formatTime(ts) {
|
|
2905
|
+
const d = new Date(ts);
|
|
2906
|
+
const pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
2907
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
|
|
2908
|
+
}
|
|
2909
|
+
var projectRootCache = null;
|
|
2910
|
+
function getProjectRoot() {
|
|
2911
|
+
if (projectRootCache !== null) return projectRootCache;
|
|
2912
|
+
try {
|
|
2913
|
+
const here = fileURLToPath(import.meta.url);
|
|
2914
|
+
const segments = here.split(sep);
|
|
2915
|
+
const srcIdx = segments.lastIndexOf("src");
|
|
2916
|
+
projectRootCache = srcIdx > 0 ? segments.slice(0, srcIdx).join(sep) : here;
|
|
2917
|
+
} catch {
|
|
2918
|
+
projectRootCache = "";
|
|
2919
|
+
}
|
|
2920
|
+
return projectRootCache;
|
|
2921
|
+
}
|
|
2922
|
+
var STACK_FRAME_RE = /\s+at\s+.+?\((.+):(\d+):\d+\)|\s+at\s+(.+):(\d+):\d+/;
|
|
2923
|
+
function captureCallerLocation() {
|
|
2924
|
+
let stack;
|
|
2925
|
+
try {
|
|
2926
|
+
const err = new Error();
|
|
2927
|
+
Error.captureStackTrace?.(err, captureCallerLocation);
|
|
2928
|
+
stack = err.stack;
|
|
2929
|
+
} catch {
|
|
2930
|
+
return { file: "<unknown>", line: 0 };
|
|
2931
|
+
}
|
|
2932
|
+
if (!stack) return { file: "<unknown>", line: 0 };
|
|
2933
|
+
const root = getProjectRoot();
|
|
2934
|
+
const lines = stack.split("\n");
|
|
2935
|
+
for (const raw of lines) {
|
|
2936
|
+
const m = STACK_FRAME_RE.exec(raw);
|
|
2937
|
+
if (!m) continue;
|
|
2938
|
+
const filePath = m[1] ?? m[3] ?? "";
|
|
2939
|
+
const lineNo = Number(m[2] ?? m[4] ?? "0");
|
|
2940
|
+
if (!filePath) continue;
|
|
2941
|
+
const normalized = filePath.replace(/^file:\/\/\//, "").replace(/^file:\/\//, "");
|
|
2942
|
+
if (normalized.includes(`${sep}${LOGGER_DIR}${sep}`) || normalized.endsWith(`${sep}${LOGGER_DIR}`)) {
|
|
2943
|
+
continue;
|
|
2944
|
+
}
|
|
2945
|
+
if (normalized.includes(`${sep}node_modules${sep}`)) continue;
|
|
2946
|
+
let rel = normalized;
|
|
2947
|
+
if (root && normalized.startsWith(root)) {
|
|
2948
|
+
rel = normalized.slice(root.length).replace(/^[/\\]/, "") || basename(normalized);
|
|
2949
|
+
}
|
|
2950
|
+
return { file: rel || basename(normalized), line: lineNo };
|
|
2951
|
+
}
|
|
2952
|
+
return { file: "<unknown>", line: 0 };
|
|
2953
|
+
}
|
|
2954
|
+
var SEPARATOR_WIDTH = 80;
|
|
2955
|
+
var SEPARATOR = "\u2500".repeat(SEPARATOR_WIDTH);
|
|
2956
|
+
function formatBlock(event) {
|
|
2957
|
+
const header = `[${event.time}] [${event.type}] @ ${event.loc.file}:${event.loc.line}`;
|
|
2958
|
+
const body = JSON.stringify(event, null, 2);
|
|
2959
|
+
return `${SEPARATOR}
|
|
2960
|
+
${header}
|
|
2961
|
+
${body}
|
|
2962
|
+
|
|
2963
|
+
`;
|
|
2964
|
+
}
|
|
2426
2965
|
|
|
2427
2966
|
// src/agent/index.ts
|
|
2428
2967
|
var Session = class _Session {
|
|
2968
|
+
/** 当前会话的消息历史(含 system/user/assistant/tool),可外部只读 */
|
|
2429
2969
|
#messages = [];
|
|
2970
|
+
/** LLM Provider(DeepSeek 等) */
|
|
2430
2971
|
#provider;
|
|
2972
|
+
/** 工具注册表(所有可用工具) */
|
|
2431
2973
|
#toolRegistry;
|
|
2974
|
+
/** 成本追踪器(今日 + 本会话) */
|
|
2432
2975
|
#costTracker;
|
|
2976
|
+
/** 归一化后的构造选项(带默认值) */
|
|
2433
2977
|
#options;
|
|
2978
|
+
/** 中止信号控制器(abort() 时触发,传递给 LLM 和工具) */
|
|
2434
2979
|
#abortController = new AbortController();
|
|
2980
|
+
/** 会话唯一 ID(UUID) */
|
|
2435
2981
|
#sessionId;
|
|
2982
|
+
/** 持久化存储;传 false 时为 null */
|
|
2436
2983
|
#store;
|
|
2984
|
+
/** 会话创建时间(毫秒) */
|
|
2437
2985
|
#createdAt;
|
|
2986
|
+
/** 节流持久化定时器(500ms debounce) */
|
|
2438
2987
|
#persistTimer = null;
|
|
2988
|
+
/** user 消息 → Checkpoint 映射(仅给 user 消息建点) */
|
|
2439
2989
|
#checkpoints = /* @__PURE__ */ new Map();
|
|
2990
|
+
/** 最近的失败记录(仅失败的,用于风暴检测) */
|
|
2440
2991
|
#stormRecords = [];
|
|
2992
|
+
/** 当前会话模式:code(默认)/ plan(只读) */
|
|
2441
2993
|
#mode = "code";
|
|
2994
|
+
/** 对话日志记录器(写入 .dskcode/logs/) */
|
|
2442
2995
|
#logger;
|
|
2996
|
+
/** 风暴检测器(连续 3 次同工具同错码失败时中断本轮) */
|
|
2997
|
+
#stormDetector;
|
|
2998
|
+
/** 失败归因 Reflector(将本轮失败原因拼到下一轮 prompt;null 表示已关闭) */
|
|
2999
|
+
#reflector;
|
|
3000
|
+
/** 本轮工具执行结果(仅在 chat() 循环内使用;用于下一轮注入 reflection) */
|
|
3001
|
+
#lastRoundResults = null;
|
|
3002
|
+
/**
|
|
3003
|
+
* 构造一个 Session。
|
|
3004
|
+
*
|
|
3005
|
+
* @param provider — LLM Provider 实例(必须)
|
|
3006
|
+
* @param tools — 工具列表或已初始化的 ToolRegistry(默认空)
|
|
3007
|
+
* @param costTracker — 成本追踪器(默认新建)
|
|
3008
|
+
* @param options — 会话选项(详见 SessionOptions)
|
|
3009
|
+
*
|
|
3010
|
+
* @sideEffect 创建 .dskcode/logs/ 下的日志文件并写入 session_start 事件
|
|
3011
|
+
*/
|
|
2443
3012
|
constructor(provider, tools = [], costTracker, options) {
|
|
2444
3013
|
this.#provider = provider;
|
|
2445
3014
|
if (tools instanceof ToolRegistry) {
|
|
@@ -2458,7 +3027,8 @@ var Session = class _Session {
|
|
|
2458
3027
|
gate: options?.gate ?? new AlwaysAllowGate(),
|
|
2459
3028
|
writeRoots: options?.writeRoots ?? [options?.cwd ?? process.cwd()],
|
|
2460
3029
|
enableCheckpoint: options?.enableCheckpoint ?? true,
|
|
2461
|
-
enableLog: options?.enableLog ?? true
|
|
3030
|
+
enableLog: options?.enableLog ?? true,
|
|
3031
|
+
enableReflection: options?.enableReflection !== false
|
|
2462
3032
|
};
|
|
2463
3033
|
this.#sessionId = options?.sessionId ?? SessionStore.newId();
|
|
2464
3034
|
this.#store = options?.store === false ? null : options?.store ?? new SessionStore();
|
|
@@ -2467,38 +3037,73 @@ var Session = class _Session {
|
|
|
2467
3037
|
enabled: this.#options.enableLog
|
|
2468
3038
|
});
|
|
2469
3039
|
this.#logger.logSessionStart(this.#sessionId, this.#options.cwd, this.#provider.model(), this.#mode);
|
|
3040
|
+
this.#stormDetector = new StormDetector({ threshold: 3 });
|
|
3041
|
+
this.#reflector = this.#options.enableReflection ? new Reflector() : null;
|
|
2470
3042
|
}
|
|
3043
|
+
/** 当前会话的完整消息历史(只读视图) */
|
|
2471
3044
|
get messages() {
|
|
2472
3045
|
return this.#messages;
|
|
2473
3046
|
}
|
|
3047
|
+
/** 本次会话累计成本(人民币元) */
|
|
2474
3048
|
get accumulatedCost() {
|
|
2475
3049
|
return this.#costTracker.sessionTotalCost;
|
|
2476
3050
|
}
|
|
3051
|
+
/** 成本追踪器(含今日总成本、会话成本、模型维度统计) */
|
|
2477
3052
|
get costTracker() {
|
|
2478
3053
|
return this.#costTracker;
|
|
2479
3054
|
}
|
|
3055
|
+
/** 当前模型标识(如 "deepseek-v4-flash") */
|
|
2480
3056
|
get model() {
|
|
2481
3057
|
return this.#provider.model();
|
|
2482
3058
|
}
|
|
3059
|
+
/** 工具注册表(可外部读 / 运行时 register) */
|
|
2483
3060
|
get toolRegistry() {
|
|
2484
3061
|
return this.#toolRegistry;
|
|
2485
3062
|
}
|
|
3063
|
+
/** 当前模式:"code" | "plan" */
|
|
2486
3064
|
get mode() {
|
|
2487
3065
|
return this.#mode;
|
|
2488
3066
|
}
|
|
3067
|
+
/** 会话 ID(UUID) */
|
|
2489
3068
|
get id() {
|
|
2490
3069
|
return this.#sessionId;
|
|
2491
3070
|
}
|
|
3071
|
+
/** 持久化存储实例(禁用持久化时为 null) */
|
|
2492
3072
|
get store() {
|
|
2493
3073
|
return this.#store;
|
|
2494
3074
|
}
|
|
3075
|
+
/** 会话创建时间戳(毫秒) */
|
|
2495
3076
|
get createdAt() {
|
|
2496
3077
|
return this.#createdAt;
|
|
2497
3078
|
}
|
|
3079
|
+
/**
|
|
3080
|
+
* 切换会话模式。
|
|
3081
|
+
*
|
|
3082
|
+
* @param mode — "code":全部工具;"plan":只暴露只读工具
|
|
3083
|
+
* @returns 设置后的模式(便于链式调用)
|
|
3084
|
+
*/
|
|
2498
3085
|
setMode(mode) {
|
|
2499
3086
|
this.#mode = mode;
|
|
2500
3087
|
return this.#mode;
|
|
2501
3088
|
}
|
|
3089
|
+
/**
|
|
3090
|
+
* chat() — 接收一轮用户输入,串起 LLM 调用 / 工具执行 / 风暴中断 / 持久化。
|
|
3091
|
+
*
|
|
3092
|
+
* 主循环每轮做:
|
|
3093
|
+
* 1. 构建 system prompt + 裁剪消息 + 拼装 apiMessages
|
|
3094
|
+
* 2. 调 provider.chat() 拿流式 chunks,向外 yield text_delta / usage 事件
|
|
3095
|
+
* 3. 若本轮有 tool_calls:
|
|
3096
|
+
* a. 先调 StormDetector.shouldBreak 判定是否中断风暴
|
|
3097
|
+
* b. 调 ToolExecutor.executeBatch 执行工具
|
|
3098
|
+
* c. 把工具结果 yield 出去并写入 messages
|
|
3099
|
+
* 4. 持续到 LLM 不再调工具 或 达到 maxToolRounds
|
|
3100
|
+
*
|
|
3101
|
+
* @param userInput — 用户本轮输入
|
|
3102
|
+
* @param opts — 透传给 provider.chat() 的 ChatOptions(thinking / tool_choice 等)
|
|
3103
|
+
* @yields AgentEvent — text_delta / tool_calls / tool_result / usage / done / error
|
|
3104
|
+
*
|
|
3105
|
+
* @sideEffect 写入 messages、logger、costTracker、可能触发 checkpoint / persist
|
|
3106
|
+
*/
|
|
2502
3107
|
async *chat(userInput, opts) {
|
|
2503
3108
|
this.#messages.push({ role: "user", content: userInput });
|
|
2504
3109
|
this.#logger.logUserMessage(userInput);
|
|
@@ -2512,9 +3117,23 @@ var Session = class _Session {
|
|
|
2512
3117
|
}
|
|
2513
3118
|
const startTime = Date.now();
|
|
2514
3119
|
let toolRounds = 0;
|
|
3120
|
+
const toolExecutor = this.#buildToolExecutor();
|
|
2515
3121
|
try {
|
|
2516
3122
|
while (toolRounds < this.#options.maxToolRounds) {
|
|
2517
|
-
|
|
3123
|
+
let systemPrompt = this.#buildSystemPrompt();
|
|
3124
|
+
if (this.#reflector && this.#lastRoundResults) {
|
|
3125
|
+
const reflections = this.#reflector.analyze(this.#lastRoundResults, {
|
|
3126
|
+
writeRoots: this.#options.writeRoots,
|
|
3127
|
+
cwd: this.#options.cwd
|
|
3128
|
+
});
|
|
3129
|
+
if (reflections.length > 0) {
|
|
3130
|
+
systemPrompt = this.#reflector.injectIntoPrompt(systemPrompt, reflections);
|
|
3131
|
+
this.#logger.logReflections(
|
|
3132
|
+
reflections.map((r) => ({ category: r.category, toolName: r.toolName, hint: r.hint }))
|
|
3133
|
+
);
|
|
3134
|
+
}
|
|
3135
|
+
this.#lastRoundResults = null;
|
|
3136
|
+
}
|
|
2518
3137
|
const [trimmed] = trimMessages(
|
|
2519
3138
|
[...this.#messages],
|
|
2520
3139
|
{
|
|
@@ -2525,7 +3144,7 @@ var Session = class _Session {
|
|
|
2525
3144
|
}
|
|
2526
3145
|
);
|
|
2527
3146
|
const apiMessages = buildApiMessages(systemPrompt, trimmed);
|
|
2528
|
-
const toolDefs = this.#
|
|
3147
|
+
const toolDefs = buildToolDefinitions(this.#toolRegistry, this.#mode);
|
|
2529
3148
|
const stream = this.#provider.chat(apiMessages, {
|
|
2530
3149
|
signal: this.#abortController.signal,
|
|
2531
3150
|
tools: toolDefs.length > 0 ? toolDefs : void 0,
|
|
@@ -2534,21 +3153,69 @@ var Session = class _Session {
|
|
|
2534
3153
|
responseFormat: opts?.responseFormat,
|
|
2535
3154
|
toolChoice: opts?.toolChoice
|
|
2536
3155
|
});
|
|
3156
|
+
const modelId = this.#provider.model();
|
|
2537
3157
|
let accumulatedText = "";
|
|
2538
3158
|
let lastUsage;
|
|
2539
3159
|
let lastToolCalls;
|
|
2540
3160
|
let _lastFinishReason = null;
|
|
3161
|
+
let estimatedInputTokens = 0;
|
|
3162
|
+
for (const m of apiMessages) {
|
|
3163
|
+
estimatedInputTokens += this.#provider.countTokens(m.content) + 10;
|
|
3164
|
+
}
|
|
3165
|
+
let lastEmitMs = 0;
|
|
3166
|
+
const EST_EMIT_INTERVAL_MS = 300;
|
|
2541
3167
|
for await (const chunk of stream) {
|
|
2542
3168
|
if (chunk.content) {
|
|
2543
3169
|
accumulatedText += chunk.content;
|
|
2544
3170
|
yield { type: "text_delta", content: chunk.content };
|
|
3171
|
+
const now = Date.now();
|
|
3172
|
+
if (now - lastEmitMs >= EST_EMIT_INTERVAL_MS) {
|
|
3173
|
+
lastEmitMs = now;
|
|
3174
|
+
const estOutput = this.#provider.countTokens(accumulatedText);
|
|
3175
|
+
const estUsage = {
|
|
3176
|
+
promptTokens: estimatedInputTokens,
|
|
3177
|
+
completionTokens: estOutput
|
|
3178
|
+
};
|
|
3179
|
+
const estCost = calculateCost(estUsage, modelId);
|
|
3180
|
+
yield {
|
|
3181
|
+
type: "usage",
|
|
3182
|
+
usage: estUsage,
|
|
3183
|
+
model: modelId,
|
|
3184
|
+
cost: estCost.totalCost,
|
|
3185
|
+
estimated: true
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
2545
3188
|
}
|
|
2546
3189
|
if (chunk.toolCalls && chunk.toolCalls.length > 0) lastToolCalls = chunk.toolCalls;
|
|
2547
|
-
if (chunk.usage)
|
|
3190
|
+
if (chunk.usage) {
|
|
3191
|
+
lastUsage = chunk.usage;
|
|
3192
|
+
const realCost = calculateCost(chunk.usage, modelId);
|
|
3193
|
+
yield {
|
|
3194
|
+
type: "usage",
|
|
3195
|
+
usage: chunk.usage,
|
|
3196
|
+
model: modelId,
|
|
3197
|
+
cost: realCost.totalCost,
|
|
3198
|
+
estimated: false
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
2548
3201
|
if (chunk.finishReason) _lastFinishReason = chunk.finishReason;
|
|
2549
3202
|
}
|
|
3203
|
+
if (accumulatedText && !lastUsage) {
|
|
3204
|
+
const estOutput = this.#provider.countTokens(accumulatedText);
|
|
3205
|
+
const estUsage = {
|
|
3206
|
+
promptTokens: estimatedInputTokens,
|
|
3207
|
+
completionTokens: estOutput
|
|
3208
|
+
};
|
|
3209
|
+
const estCost = calculateCost(estUsage, modelId);
|
|
3210
|
+
yield {
|
|
3211
|
+
type: "usage",
|
|
3212
|
+
usage: estUsage,
|
|
3213
|
+
model: modelId,
|
|
3214
|
+
cost: estCost.totalCost,
|
|
3215
|
+
estimated: true
|
|
3216
|
+
};
|
|
3217
|
+
}
|
|
2550
3218
|
if (lastUsage) {
|
|
2551
|
-
const modelId = this.#provider.model();
|
|
2552
3219
|
this.#costTracker.record(lastUsage, modelId);
|
|
2553
3220
|
const cost = calculateCost(lastUsage, modelId);
|
|
2554
3221
|
this.#logger.logUsage(
|
|
@@ -2570,7 +3237,7 @@ var Session = class _Session {
|
|
|
2570
3237
|
for (const tc of lastToolCalls) {
|
|
2571
3238
|
this.#logger.logToolCall(tc.name, tc.id, tc.arguments, toolRounds);
|
|
2572
3239
|
}
|
|
2573
|
-
const stormBroken = this.#
|
|
3240
|
+
const stormBroken = this.#stormDetector.shouldBreak(this.#stormRecords, lastToolCalls);
|
|
2574
3241
|
if (stormBroken) {
|
|
2575
3242
|
const stormMsg = "\n\u26A0\uFE0F \u540C\u4E00\u5DE5\u5177\u91CD\u590D\u51FA\u9519\uFF0C\u5DF2\u5F3A\u5236\u5207\u6362\u7B56\u7565\n";
|
|
2576
3243
|
yield { type: "text_delta", content: stormMsg };
|
|
@@ -2580,8 +3247,18 @@ var Session = class _Session {
|
|
|
2580
3247
|
toolRounds++;
|
|
2581
3248
|
continue;
|
|
2582
3249
|
}
|
|
2583
|
-
const results = await
|
|
3250
|
+
const results = await toolExecutor.executeBatch(lastToolCalls);
|
|
2584
3251
|
this.#stormRecords = results.records;
|
|
3252
|
+
console.log("results", results);
|
|
3253
|
+
this.#lastRoundResults = results.items.map((it) => {
|
|
3254
|
+
const tool = this.#toolRegistry.get(it.name);
|
|
3255
|
+
return {
|
|
3256
|
+
name: it.name,
|
|
3257
|
+
result: it.result,
|
|
3258
|
+
kind: tool?.kind ?? "other" /* Other */,
|
|
3259
|
+
recentSameTool: results.records.filter((r) => r.name === it.name).map((r) => ({ success: r.success, error: r.error }))
|
|
3260
|
+
};
|
|
3261
|
+
});
|
|
2585
3262
|
for (const item of results.items) {
|
|
2586
3263
|
yield { type: "tool_result", name: item.name, result: item.result };
|
|
2587
3264
|
this.#logger.logToolResult(
|
|
@@ -2626,98 +3303,15 @@ ${item.result.diff.patch}`;
|
|
|
2626
3303
|
});
|
|
2627
3304
|
yield { type: "done", elapsed };
|
|
2628
3305
|
}
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
});
|
|
2639
|
-
if (allReadOnly && calls.length > 1) {
|
|
2640
|
-
const MAX_PARALLEL = 8;
|
|
2641
|
-
const items2 = [];
|
|
2642
|
-
const records2 = [];
|
|
2643
|
-
for (let i = 0; i < calls.length; i += MAX_PARALLEL) {
|
|
2644
|
-
const batch = calls.slice(i, i + MAX_PARALLEL);
|
|
2645
|
-
const promises = batch.map((tc) => this.#executeOne(tc, toolCtx));
|
|
2646
|
-
const batchResults = await Promise.all(promises);
|
|
2647
|
-
for (const r of batchResults) {
|
|
2648
|
-
items2.push(r.item);
|
|
2649
|
-
records2.push(r.record);
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
return { items: items2, records: records2 };
|
|
2653
|
-
}
|
|
2654
|
-
const items = [];
|
|
2655
|
-
const records = [];
|
|
2656
|
-
for (const tc of calls) {
|
|
2657
|
-
const r = await this.#executeOne(tc, toolCtx);
|
|
2658
|
-
items.push(r.item);
|
|
2659
|
-
records.push(r.record);
|
|
2660
|
-
}
|
|
2661
|
-
return { items, records };
|
|
2662
|
-
}
|
|
2663
|
-
async #executeOne(tc, ctx) {
|
|
2664
|
-
const toolName = tc.name;
|
|
2665
|
-
const timestamp = Date.now();
|
|
2666
|
-
const tool = this.#toolRegistry.get(toolName);
|
|
2667
|
-
if (!tool) {
|
|
2668
|
-
const errMsg = `\u5DE5\u5177 "${toolName}" \u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u7981\u7528`;
|
|
2669
|
-
return {
|
|
2670
|
-
item: { name: toolName, callId: tc.id, result: { success: false, data: errMsg, error: "TOOL_NOT_FOUND" } },
|
|
2671
|
-
record: { name: toolName, success: false, error: "TOOL_NOT_FOUND", timestamp }
|
|
2672
|
-
};
|
|
2673
|
-
}
|
|
2674
|
-
let toolArgs;
|
|
2675
|
-
try {
|
|
2676
|
-
toolArgs = tc.arguments ? JSON.parse(tc.arguments) : {};
|
|
2677
|
-
} catch {
|
|
2678
|
-
toolArgs = {};
|
|
2679
|
-
}
|
|
2680
|
-
const gateResult = await this.#options.gate.check(toolName, toolArgs);
|
|
2681
|
-
if (!gateResult) {
|
|
2682
|
-
const errMsg = `\u5DE5\u5177 "${toolName}" \u88AB\u6743\u9650\u95E8\u62D2\u7EDD`;
|
|
2683
|
-
return {
|
|
2684
|
-
item: { name: toolName, callId: tc.id, result: { success: false, data: errMsg, error: "GATE_DENIED" } },
|
|
2685
|
-
record: { name: toolName, success: false, error: "GATE_DENIED", timestamp }
|
|
2686
|
-
};
|
|
2687
|
-
}
|
|
2688
|
-
if (!isReadOnly(tool.kind)) {
|
|
2689
|
-
const maybePreview = tool.preview;
|
|
2690
|
-
if (typeof maybePreview === "function") {
|
|
2691
|
-
try {
|
|
2692
|
-
await maybePreview(toolArgs, ctx);
|
|
2693
|
-
} catch {
|
|
2694
|
-
}
|
|
2695
|
-
}
|
|
2696
|
-
}
|
|
2697
|
-
try {
|
|
2698
|
-
const result = await tool.execute(toolArgs, ctx);
|
|
2699
|
-
return {
|
|
2700
|
-
item: { name: toolName, callId: tc.id, result },
|
|
2701
|
-
record: { name: toolName, success: result.success, error: result.error, timestamp }
|
|
2702
|
-
};
|
|
2703
|
-
} catch (err) {
|
|
2704
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2705
|
-
const errorResult = { success: false, data: `\u5DE5\u5177 "${toolName}" \u6267\u884C\u5F02\u5E38\uFF1A${message}`, error: "EXECUTION_ERROR" };
|
|
2706
|
-
return {
|
|
2707
|
-
item: { name: toolName, callId: tc.id, result: errorResult },
|
|
2708
|
-
record: { name: toolName, success: false, error: "EXECUTION_ERROR", timestamp }
|
|
2709
|
-
};
|
|
2710
|
-
}
|
|
2711
|
-
}
|
|
2712
|
-
#checkStormBreak(currentCalls) {
|
|
2713
|
-
if (this.#stormRecords.length < 3) return false;
|
|
2714
|
-
const recentErrors = this.#stormRecords.slice(-3);
|
|
2715
|
-
if (recentErrors.length < 3) return false;
|
|
2716
|
-
const first = recentErrors[0];
|
|
2717
|
-
const allSame = recentErrors.every((r) => r.name === first.name && r.error === first.error && !r.success);
|
|
2718
|
-
if (!allSame) return false;
|
|
2719
|
-
return currentCalls.some((tc) => tc.name === first.name);
|
|
2720
|
-
}
|
|
3306
|
+
// -------------------------------------------------------------------------
|
|
3307
|
+
// 持久化与恢复
|
|
3308
|
+
// -------------------------------------------------------------------------
|
|
3309
|
+
/**
|
|
3310
|
+
* 中止当前会话:触发 AbortController,停止流式 LLM 和正在执行的工具。
|
|
3311
|
+
* 同时清掉节流持久化定时器。
|
|
3312
|
+
*
|
|
3313
|
+
* @sideEffect abort 当前 chat 循环、取消所有 #abortController 监听者
|
|
3314
|
+
*/
|
|
2721
3315
|
abort() {
|
|
2722
3316
|
this.#abortController.abort();
|
|
2723
3317
|
if (this.#persistTimer) {
|
|
@@ -2725,19 +3319,34 @@ ${item.result.diff.patch}`;
|
|
|
2725
3319
|
this.#persistTimer = null;
|
|
2726
3320
|
}
|
|
2727
3321
|
}
|
|
2728
|
-
/**
|
|
3322
|
+
/**
|
|
3323
|
+
* 刷新日志缓冲,确保所有已记录的事件落盘。
|
|
3324
|
+
* 通常在进程退出前调用,避免日志丢失。
|
|
3325
|
+
*
|
|
3326
|
+
* @sideEffect 写入 .dskcode/logs/ 下的日志文件
|
|
3327
|
+
*/
|
|
2729
3328
|
async flushLog() {
|
|
2730
3329
|
await this.#logger.flush();
|
|
2731
3330
|
}
|
|
3331
|
+
/**
|
|
3332
|
+
* 清空消息历史、会话成本、风暴记录、checkpoints。
|
|
3333
|
+
* 注意:不会清掉磁盘上的会话记录(用 delete() 删除),仅重置内存状态。
|
|
3334
|
+
*
|
|
3335
|
+
* @sideEffect 抹掉 messages / costTracker / stormRecords / checkpoints
|
|
3336
|
+
*/
|
|
2732
3337
|
reset() {
|
|
2733
3338
|
this.#messages.length = 0;
|
|
2734
3339
|
this.#costTracker.resetSession();
|
|
2735
3340
|
this.#stormRecords = [];
|
|
2736
3341
|
this.#checkpoints.clear();
|
|
3342
|
+
this.#lastRoundResults = null;
|
|
2737
3343
|
}
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
3344
|
+
/**
|
|
3345
|
+
* 立即把当前状态写入持久化(不等 500ms debounce)。
|
|
3346
|
+
* 用于 UI 上"立即保存"按钮或异常退出前的最后兜底。
|
|
3347
|
+
*
|
|
3348
|
+
* @sideEffect 调用 #doPersist(),写入磁盘
|
|
3349
|
+
*/
|
|
2741
3350
|
async persistNow() {
|
|
2742
3351
|
if (this.#persistTimer) {
|
|
2743
3352
|
clearTimeout(this.#persistTimer);
|
|
@@ -2745,6 +3354,12 @@ ${item.result.diff.patch}`;
|
|
|
2745
3354
|
}
|
|
2746
3355
|
await this.#doPersist();
|
|
2747
3356
|
}
|
|
3357
|
+
/**
|
|
3358
|
+
* 节流持久化:500ms 内多次调用只会真正落盘一次。
|
|
3359
|
+
* 通过 setTimeout + timer.refresh() 实现 debounce。
|
|
3360
|
+
*
|
|
3361
|
+
* @sideEffect 调度一个 500ms 后的 #doPersist() 任务
|
|
3362
|
+
*/
|
|
2748
3363
|
#persist() {
|
|
2749
3364
|
if (!this.#store) return;
|
|
2750
3365
|
if (this.#persistTimer) {
|
|
@@ -2757,6 +3372,12 @@ ${item.result.diff.patch}`;
|
|
|
2757
3372
|
}, 500);
|
|
2758
3373
|
this.#persistTimer.unref();
|
|
2759
3374
|
}
|
|
3375
|
+
/**
|
|
3376
|
+
* 真正执行持久化:构造 StoredSession 并写入 store。
|
|
3377
|
+
* 失败仅 console.error,不抛给调用方(持久化失败不应阻塞对话)。
|
|
3378
|
+
*
|
|
3379
|
+
* @sideEffect 写磁盘 ~/.dskcode/sessions/<id>.json
|
|
3380
|
+
*/
|
|
2760
3381
|
async #doPersist() {
|
|
2761
3382
|
if (!this.#store) return;
|
|
2762
3383
|
const stored = {
|
|
@@ -2775,12 +3396,24 @@ ${item.result.diff.patch}`;
|
|
|
2775
3396
|
console.error("[Session] \u6301\u4E45\u5316\u5931\u8D25:", err);
|
|
2776
3397
|
}
|
|
2777
3398
|
}
|
|
3399
|
+
/**
|
|
3400
|
+
* 从消息历史推导会话标题:取第一条非空 user 消息的前 40 字。
|
|
3401
|
+
* 无任何 user 消息时返回 "新会话"。
|
|
3402
|
+
*
|
|
3403
|
+
* @pure 不修改任何状态
|
|
3404
|
+
*/
|
|
2778
3405
|
#deriveTitle() {
|
|
2779
3406
|
for (const m of this.#messages) {
|
|
2780
3407
|
if (m.role === "user" && m.content.trim()) return m.content.trim().slice(0, 40);
|
|
2781
3408
|
}
|
|
2782
3409
|
return "\u65B0\u4F1A\u8BDD";
|
|
2783
3410
|
}
|
|
3411
|
+
/**
|
|
3412
|
+
* 把内存 messages 转成可持久化的 StoredSession["messages"]。
|
|
3413
|
+
* 给 user 消息挂上对应 checkpoint(若存在),用于 rewind 时文件回退。
|
|
3414
|
+
*
|
|
3415
|
+
* @pure 不修改任何状态
|
|
3416
|
+
*/
|
|
2784
3417
|
#serializeMessages() {
|
|
2785
3418
|
return this.#messages.map((msg, idx) => {
|
|
2786
3419
|
const checkpoint = this.#checkpoints.get(idx);
|
|
@@ -2788,6 +3421,19 @@ ${item.result.diff.patch}`;
|
|
|
2788
3421
|
return { ...msg };
|
|
2789
3422
|
});
|
|
2790
3423
|
}
|
|
3424
|
+
/**
|
|
3425
|
+
* 静态方法:从磁盘恢复一个之前保存的 Session。
|
|
3426
|
+
*
|
|
3427
|
+
* @param id — 之前 SessionStore 保存的会话 ID
|
|
3428
|
+
* @param provider — LLM Provider(必须)
|
|
3429
|
+
* @param tools — 工具列表/Registry(必须重新提供,因为工具不持久化)
|
|
3430
|
+
* @param costTracker — 成本追踪器(可选)
|
|
3431
|
+
* @param options — 会话选项(sessionId / store 必须与持久化 ID 对应)
|
|
3432
|
+
* @returns 恢复后的 Session 实例
|
|
3433
|
+
* @throws 持久化被禁用时、ID 不存在时抛错
|
|
3434
|
+
*
|
|
3435
|
+
* @sideEffect 从磁盘读 messages + checkpoints
|
|
3436
|
+
*/
|
|
2791
3437
|
static async resume(id, provider, tools = [], costTracker, options) {
|
|
2792
3438
|
const store = options?.store === false ? null : options?.store ?? new SessionStore();
|
|
2793
3439
|
if (!store) throw new Error("resume \u9700\u8981\u542F\u7528\u6301\u4E45\u5316\uFF08options.store \u4E0D\u80FD\u4E3A false\uFF09");
|
|
@@ -2813,6 +3459,13 @@ ${item.result.diff.patch}`;
|
|
|
2813
3459
|
// -------------------------------------------------------------------------
|
|
2814
3460
|
// 检查点与 Rewind
|
|
2815
3461
|
// -------------------------------------------------------------------------
|
|
3462
|
+
/**
|
|
3463
|
+
* 列出所有 user 消息对应的 checkpoint(按 index 升序)。
|
|
3464
|
+
* 用于 UI 展示 /rewind 列表。
|
|
3465
|
+
*
|
|
3466
|
+
* @returns 每个元素的 index = messages 数组索引,可直接传给 rewind()
|
|
3467
|
+
* @pure 不修改任何状态
|
|
3468
|
+
*/
|
|
2816
3469
|
listCheckpoints() {
|
|
2817
3470
|
const result = [];
|
|
2818
3471
|
for (const [index, checkpoint] of this.#checkpoints) {
|
|
@@ -2822,6 +3475,25 @@ ${item.result.diff.patch}`;
|
|
|
2822
3475
|
}
|
|
2823
3476
|
return result.sort((a, b) => a.index - b.index);
|
|
2824
3477
|
}
|
|
3478
|
+
/**
|
|
3479
|
+
* rewind — 把消息历史截断到 targetIndex 对应的 user 消息,
|
|
3480
|
+
* 并尝试把文件工作区也回退到那一刻。
|
|
3481
|
+
*
|
|
3482
|
+
* @param targetIndex — 要回退到的 user 消息索引(来自 listCheckpoints())
|
|
3483
|
+
* @returns
|
|
3484
|
+
* - `{ ok: true, fileRestored }`:成功;fileRestored 表示工作区是否也被还原
|
|
3485
|
+
* - `{ ok: false, error }`:失败(无效索引 / 非 user / 无 checkpoint / 文件恢复失败)
|
|
3486
|
+
*
|
|
3487
|
+
* 行为:
|
|
3488
|
+
* 1. 截断 messages 数组到 targetIndex + 1
|
|
3489
|
+
* 2. 移除并丢弃所有 > targetIndex 的 checkpoints
|
|
3490
|
+
* 3. 若目标 checkpoint 是 git 仓库:
|
|
3491
|
+
* - 有 stashSha:恢复该 stash(force)
|
|
3492
|
+
* - 无 stashSha:把工作区 restore 到 HEAD 干净状态
|
|
3493
|
+
* 4. 触发 #persist() 把截断后的状态写盘
|
|
3494
|
+
*
|
|
3495
|
+
* @sideEffect 改写 messages、checkpoints、磁盘
|
|
3496
|
+
*/
|
|
2825
3497
|
async rewind(targetIndex) {
|
|
2826
3498
|
if (targetIndex < 0 || targetIndex >= this.#messages.length) {
|
|
2827
3499
|
return { ok: false, error: `\u65E0\u6548\u7684\u6D88\u606F\u7D22\u5F15 ${targetIndex}` };
|
|
@@ -2861,9 +3533,20 @@ ${item.result.diff.patch}`;
|
|
|
2861
3533
|
this.#persist();
|
|
2862
3534
|
return { ok: true, fileRestored };
|
|
2863
3535
|
}
|
|
3536
|
+
/**
|
|
3537
|
+
* 是否存在可用的 checkpoint(决定 UI 是否显示 /rewind 入口)。
|
|
3538
|
+
*
|
|
3539
|
+
* @pure 不修改任何状态
|
|
3540
|
+
*/
|
|
2864
3541
|
hasCheckpoints() {
|
|
2865
3542
|
return this.listCheckpoints().length > 0;
|
|
2866
3543
|
}
|
|
3544
|
+
/**
|
|
3545
|
+
* 彻底删除会话:从磁盘移除 SessionStore 条目、丢弃所有 checkpoint、关闭日志。
|
|
3546
|
+
* 删除后该 Session 实例不应再被使用。
|
|
3547
|
+
*
|
|
3548
|
+
* @sideEffect 删磁盘文件、清 checkpoints、flush 并关闭 logger
|
|
3549
|
+
*/
|
|
2867
3550
|
async delete() {
|
|
2868
3551
|
if (this.#store) await this.#store.delete(this.#sessionId);
|
|
2869
3552
|
for (const cp2 of this.#checkpoints.values()) {
|
|
@@ -2876,6 +3559,32 @@ ${item.result.diff.patch}`;
|
|
|
2876
3559
|
// -------------------------------------------------------------------------
|
|
2877
3560
|
// 内部方法
|
|
2878
3561
|
// -------------------------------------------------------------------------
|
|
3562
|
+
/**
|
|
3563
|
+
* 构建本轮使用的工具执行器(每次 chat() 调用一次,绑定当前 signal)。
|
|
3564
|
+
* 每次新建确保 abort signal 是当下有效的,且 #baseCtx 不会被多次调用共享篡改。
|
|
3565
|
+
*
|
|
3566
|
+
* @returns 新的 ToolExecutor 实例
|
|
3567
|
+
* @pure 仅组装参数,不修改任何状态
|
|
3568
|
+
*/
|
|
3569
|
+
#buildToolExecutor() {
|
|
3570
|
+
return new ToolExecutor({
|
|
3571
|
+
registry: this.#toolRegistry,
|
|
3572
|
+
gate: this.#options.gate,
|
|
3573
|
+
baseCtx: {
|
|
3574
|
+
cwd: this.#options.cwd,
|
|
3575
|
+
signal: this.#abortController.signal,
|
|
3576
|
+
writeRoots: this.#options.writeRoots
|
|
3577
|
+
}
|
|
3578
|
+
});
|
|
3579
|
+
}
|
|
3580
|
+
/**
|
|
3581
|
+
* 构建 system prompt:注入当前模型、cwd、工具清单、项目上下文。
|
|
3582
|
+
* plan 模式使用 buildPlanSystemPrompt(只读工具 + 计划输出格式),
|
|
3583
|
+
* code 模式使用 buildSystemPrompt。
|
|
3584
|
+
*
|
|
3585
|
+
* @returns 渲染好的 prompt 字符串
|
|
3586
|
+
* @pure 不修改任何状态
|
|
3587
|
+
*/
|
|
2879
3588
|
#buildSystemPrompt() {
|
|
2880
3589
|
const enabledTools = this.#toolRegistry.list();
|
|
2881
3590
|
const toolDescs = enabledTools.map((t) => ({
|
|
@@ -2893,17 +3602,10 @@ ${item.result.diff.patch}`;
|
|
|
2893
3602
|
if (this.#mode === "plan") return buildPlanSystemPrompt(opts);
|
|
2894
3603
|
return buildSystemPrompt(opts);
|
|
2895
3604
|
}
|
|
2896
|
-
#buildToolDefinitions() {
|
|
2897
|
-
const tools = this.#mode === "plan" ? this.#toolRegistry.listReadTools() : this.#toolRegistry.list();
|
|
2898
|
-
return tools.map((t) => ({
|
|
2899
|
-
type: "function",
|
|
2900
|
-
function: { name: t.name, description: t.description, parameters: t.parameters }
|
|
2901
|
-
}));
|
|
2902
|
-
}
|
|
2903
3605
|
};
|
|
2904
3606
|
|
|
2905
3607
|
// src/tool/sandbox.ts
|
|
2906
|
-
import { resolve, relative, isAbsolute } from "path";
|
|
3608
|
+
import { resolve, relative as relative2, isAbsolute } from "path";
|
|
2907
3609
|
import { realpath as realpath2 } from "fs/promises";
|
|
2908
3610
|
import { spawn } from "child_process";
|
|
2909
3611
|
import process2 from "process";
|
|
@@ -2922,7 +3624,7 @@ async function realPath(target) {
|
|
|
2922
3624
|
const parent = resolve(target, "..");
|
|
2923
3625
|
try {
|
|
2924
3626
|
const realParent = await realpath2(parent);
|
|
2925
|
-
return resolve(realParent,
|
|
3627
|
+
return resolve(realParent, relative2(parent, target));
|
|
2926
3628
|
} catch {
|
|
2927
3629
|
return resolve(target);
|
|
2928
3630
|
}
|
|
@@ -2935,7 +3637,7 @@ async function confine(allowedRoots, target) {
|
|
|
2935
3637
|
const realTarget = await realPath(target);
|
|
2936
3638
|
for (const root of allowedRoots) {
|
|
2937
3639
|
const realRoot = await realPath(root);
|
|
2938
|
-
const rel =
|
|
3640
|
+
const rel = relative2(realRoot, realTarget);
|
|
2939
3641
|
if (!rel.startsWith("..") && rel !== "" && !rel.startsWith("/") && !rel.startsWith("\\")) {
|
|
2940
3642
|
return { ok: true };
|
|
2941
3643
|
}
|
|
@@ -3332,7 +4034,7 @@ async function writeFileWithEol(filePath, originalContent, newContent) {
|
|
|
3332
4034
|
// src/tool/builtins/read-file.ts
|
|
3333
4035
|
import { readFile as readFile5, stat } from "fs/promises";
|
|
3334
4036
|
import { open } from "fs/promises";
|
|
3335
|
-
import { relative as
|
|
4037
|
+
import { relative as relative3 } from "path";
|
|
3336
4038
|
async function checkBinary(filePath) {
|
|
3337
4039
|
const fileHandle = await open(filePath, "r");
|
|
3338
4040
|
try {
|
|
@@ -3415,7 +4117,7 @@ var readFileTool = {
|
|
|
3415
4117
|
const tailHint = remaining > 0 ? `
|
|
3416
4118
|
|
|
3417
4119
|
[\u8FD8\u6709 ${remaining} \u884C\uFF1B\u4F7F\u7528 startLine=${endLine + 1} \u7EE7\u7EED\u67E5\u770B]` : "";
|
|
3418
|
-
const relPath =
|
|
4120
|
+
const relPath = relative3(ctx.cwd, filePath).replace(/\\/g, "/");
|
|
3419
4121
|
const rangeLabel = startLine > 0 || endLine < lines.length ? `\u7B2C ${startLine + 1}-${endLine} \u884C` : `${lines.length} \u884C`;
|
|
3420
4122
|
return {
|
|
3421
4123
|
success: true,
|
|
@@ -3435,7 +4137,7 @@ var readFileTool = {
|
|
|
3435
4137
|
|
|
3436
4138
|
// src/tool/builtins/write-file.ts
|
|
3437
4139
|
import { mkdir as mkdir6, readFile as readFile6 } from "fs/promises";
|
|
3438
|
-
import { dirname, relative as
|
|
4140
|
+
import { dirname, relative as relative4, basename as basename2 } from "path";
|
|
3439
4141
|
var writeFileTool = {
|
|
3440
4142
|
name: "write_file",
|
|
3441
4143
|
kind: "edit" /* Edit */,
|
|
@@ -3490,7 +4192,7 @@ var writeFileTool = {
|
|
|
3490
4192
|
const summary = existedBefore ? `\u{1F4DD} \u4FEE\u6539: ${fileName} (+${diff.additions} -${diff.deletions})` : `\u{1F4DD} \u65B0\u5EFA: ${fileName} (+${diff.additions} \u884C)`;
|
|
3491
4193
|
return {
|
|
3492
4194
|
success: true,
|
|
3493
|
-
data: `\u6587\u4EF6${action}\uFF1A${
|
|
4195
|
+
data: `\u6587\u4EF6${action}\uFF1A${relative4(ctx.cwd, filePath).replace(/\\/g, "/")}\uFF08${lineCount} \u884C\uFF0C${byteSize} \u5B57\u8282${diffSummary}\uFF09`,
|
|
3494
4196
|
summary,
|
|
3495
4197
|
diff
|
|
3496
4198
|
};
|
|
@@ -3943,7 +4645,7 @@ ${truncateOutput(result.stderr)}`);
|
|
|
3943
4645
|
|
|
3944
4646
|
// src/tool/builtins/glob.ts
|
|
3945
4647
|
import { readdir as readdir3, stat as stat2 } from "fs/promises";
|
|
3946
|
-
import { join as join7, relative as
|
|
4648
|
+
import { join as join7, relative as relative5, isAbsolute as isAbsolute2 } from "path";
|
|
3947
4649
|
function globToRegex(pattern) {
|
|
3948
4650
|
let regexStr = pattern;
|
|
3949
4651
|
regexStr = regexStr.replace(/\*\*\//g, "<<GLOBSTAR_SLASH>>");
|
|
@@ -3969,7 +4671,7 @@ async function walkDir(dir, baseDir) {
|
|
|
3969
4671
|
continue;
|
|
3970
4672
|
}
|
|
3971
4673
|
const fullPath = join7(dir, entry.name);
|
|
3972
|
-
const relPath =
|
|
4674
|
+
const relPath = relative5(baseDir, fullPath);
|
|
3973
4675
|
if (entry.isDirectory()) {
|
|
3974
4676
|
results.push(...await walkDir(fullPath, baseDir));
|
|
3975
4677
|
} else {
|
|
@@ -4041,7 +4743,7 @@ var globTool = {
|
|
|
4041
4743
|
|
|
4042
4744
|
// src/tool/builtins/grep.ts
|
|
4043
4745
|
import { readdir as readdir4, readFile as readFile10, stat as stat3 } from "fs/promises";
|
|
4044
|
-
import { join as join8, relative as
|
|
4746
|
+
import { join as join8, relative as relative6, isAbsolute as isAbsolute3 } from "path";
|
|
4045
4747
|
async function collectFiles(dir, extension, maxFiles = 200) {
|
|
4046
4748
|
const results = [];
|
|
4047
4749
|
let entries;
|
|
@@ -4117,7 +4819,7 @@ var grepTool = {
|
|
|
4117
4819
|
try {
|
|
4118
4820
|
const content = await readFile10(filePath, "utf-8");
|
|
4119
4821
|
const lines = content.split("\n");
|
|
4120
|
-
const relPath =
|
|
4822
|
+
const relPath = relative6(searchDir, filePath);
|
|
4121
4823
|
for (let i = 0; i < lines.length; i++) {
|
|
4122
4824
|
if (regex.test(lines[i] ?? "")) {
|
|
4123
4825
|
regex.lastIndex = 0;
|
|
@@ -4162,7 +4864,7 @@ var grepTool = {
|
|
|
4162
4864
|
|
|
4163
4865
|
// src/tool/builtins/ls.ts
|
|
4164
4866
|
import { readdir as readdir5, stat as stat4 } from "fs/promises";
|
|
4165
|
-
import { join as join9, relative as
|
|
4867
|
+
import { join as join9, relative as relative7 } from "path";
|
|
4166
4868
|
var lsTool = {
|
|
4167
4869
|
name: "ls",
|
|
4168
4870
|
kind: "read" /* Read */,
|
|
@@ -4216,9 +4918,9 @@ var lsTool = {
|
|
|
4216
4918
|
lines.push(`${typeLabel} ${entry.name}${sizeStr ? ` (${sizeStr})` : ""}`);
|
|
4217
4919
|
}
|
|
4218
4920
|
if (lines.length === 0) {
|
|
4219
|
-
return { success: true, data: "\u76EE\u5F55\u4E3A\u7A7A", summary: `\u{1F4C2} ${
|
|
4921
|
+
return { success: true, data: "\u76EE\u5F55\u4E3A\u7A7A", summary: `\u{1F4C2} ${relative7(ctx.cwd, dirPath).replace(/\\/g, "/")}\uFF08\u7A7A\uFF09` };
|
|
4220
4922
|
}
|
|
4221
|
-
const relPath =
|
|
4923
|
+
const relPath = relative7(ctx.cwd, dirPath).replace(/\\/g, "/");
|
|
4222
4924
|
return {
|
|
4223
4925
|
success: true,
|
|
4224
4926
|
data: truncateOutput(`\u76EE\u5F55\uFF1A${dirPath}
|
|
@@ -4402,9 +5104,9 @@ function getGradientColors(text, phase, stops) {
|
|
|
4402
5104
|
}
|
|
4403
5105
|
|
|
4404
5106
|
// src/ui/ChatSession.tsx
|
|
4405
|
-
import { Fragment, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
5107
|
+
import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
4406
5108
|
var PHASE_CONFIG = {
|
|
4407
|
-
thinking: { icon: "\u{1F9E0}", label: "\u601D\u8003\u4E2D", color: "#ff9800" },
|
|
5109
|
+
thinking: { icon: "\u{1F9E0}", label: "\u6DF1\u5EA6\u601D\u8003\u4E2D", color: "#ff9800" },
|
|
4408
5110
|
generating: { icon: "\u2728", label: "\u751F\u6210\u4E2D", color: "#00ff41" },
|
|
4409
5111
|
calling_tools: { icon: "\u{1F6E0}", label: "\u8C03\u7528\u5DE5\u5177", color: "#f59e0b" },
|
|
4410
5112
|
executing_tools: { icon: "\u26A1", label: "\u6267\u884C\u5DE5\u5177", color: "#00ffff" }
|
|
@@ -4476,34 +5178,34 @@ function ChatSession({
|
|
|
4476
5178
|
}) {
|
|
4477
5179
|
const termWidth = typeof process.stdout.columns === "number" ? process.stdout.columns : 80;
|
|
4478
5180
|
const dividerWidth = Math.max(termWidth - 2, 1);
|
|
4479
|
-
const [offset, setOffset] =
|
|
4480
|
-
const [displayMessages, setDisplayMessages] =
|
|
4481
|
-
const [staticKey, setStaticKey] =
|
|
4482
|
-
const [input, setInput] =
|
|
4483
|
-
const [balance, setBalance] =
|
|
4484
|
-
const [balanceLoading, setBalanceLoading] =
|
|
4485
|
-
const [todayCost, setTodayCost] =
|
|
4486
|
-
const [isStreaming, setIsStreaming] =
|
|
4487
|
-
const [streamingPhase, setStreamingPhase] =
|
|
4488
|
-
const [streamingPlaceholder, setStreamingPlaceholder] =
|
|
4489
|
-
const [idlePlaceholder, setIdlePlaceholder] =
|
|
4490
|
-
const [gradientColors, setGradientColors] =
|
|
4491
|
-
const gradientPhaseRef =
|
|
4492
|
-
const [streamingGradientColors, setStreamingGradientColors] =
|
|
4493
|
-
const streamingPhaseRef =
|
|
4494
|
-
const [currentContent, setCurrentContent] =
|
|
4495
|
-
const [currentToolCalls, setCurrentToolCalls] =
|
|
4496
|
-
const [_currentUsage, setCurrentUsage] =
|
|
4497
|
-
const [_currentElapsed, setCurrentElapsed] =
|
|
4498
|
-
const [_currentCost, setCurrentCost] =
|
|
4499
|
-
const [activeModel, setActiveModel] =
|
|
4500
|
-
const [_streamingModel, setStreamingModel] =
|
|
4501
|
-
const [streamError, setStreamError] =
|
|
4502
|
-
const [sessionMode, setSessionMode] =
|
|
4503
|
-
const [thinkingEnabled, setThinkingEnabled] =
|
|
4504
|
-
const [thinkingEffort, setThinkingEffort] =
|
|
4505
|
-
const [responseFormat] =
|
|
4506
|
-
const [toolChoice, setToolChoice] =
|
|
5181
|
+
const [offset, setOffset] = useState4(0);
|
|
5182
|
+
const [displayMessages, setDisplayMessages] = useState4([]);
|
|
5183
|
+
const [staticKey, setStaticKey] = useState4(0);
|
|
5184
|
+
const [input, setInput] = useState4("");
|
|
5185
|
+
const [balance, setBalance] = useState4(null);
|
|
5186
|
+
const [balanceLoading, setBalanceLoading] = useState4(false);
|
|
5187
|
+
const [todayCost, setTodayCost] = useState4(null);
|
|
5188
|
+
const [isStreaming, setIsStreaming] = useState4(false);
|
|
5189
|
+
const [streamingPhase, setStreamingPhase] = useState4(null);
|
|
5190
|
+
const [streamingPlaceholder, setStreamingPlaceholder] = useState4("");
|
|
5191
|
+
const [idlePlaceholder, setIdlePlaceholder] = useState4(() => pickRandom(IDLE_PLACEHOLDERS));
|
|
5192
|
+
const [gradientColors, setGradientColors] = useState4([]);
|
|
5193
|
+
const gradientPhaseRef = useRef3(0);
|
|
5194
|
+
const [streamingGradientColors, setStreamingGradientColors] = useState4([]);
|
|
5195
|
+
const streamingPhaseRef = useRef3(0);
|
|
5196
|
+
const [currentContent, setCurrentContent] = useState4("");
|
|
5197
|
+
const [currentToolCalls, setCurrentToolCalls] = useState4([]);
|
|
5198
|
+
const [_currentUsage, setCurrentUsage] = useState4(void 0);
|
|
5199
|
+
const [_currentElapsed, setCurrentElapsed] = useState4(void 0);
|
|
5200
|
+
const [_currentCost, setCurrentCost] = useState4(void 0);
|
|
5201
|
+
const [activeModel, setActiveModel] = useState4(model);
|
|
5202
|
+
const [_streamingModel, setStreamingModel] = useState4(void 0);
|
|
5203
|
+
const [streamError, setStreamError] = useState4(void 0);
|
|
5204
|
+
const [sessionMode, setSessionMode] = useState4("code");
|
|
5205
|
+
const [thinkingEnabled, setThinkingEnabled] = useState4(true);
|
|
5206
|
+
const [thinkingEffort, setThinkingEffort] = useState4("high");
|
|
5207
|
+
const [responseFormat] = useState4("text");
|
|
5208
|
+
const [toolChoice, setToolChoice] = useState4(void 0);
|
|
4507
5209
|
const sessionCost = useMemo(() => {
|
|
4508
5210
|
return displayMessages.reduce((sum, msg) => {
|
|
4509
5211
|
if (msg.assistantDetail?.cost) {
|
|
@@ -4514,36 +5216,36 @@ function ChatSession({
|
|
|
4514
5216
|
}, [displayMessages]);
|
|
4515
5217
|
const hasConversationStarted = displayMessages.length > 0;
|
|
4516
5218
|
const cmdTips = Array.from(getRegisteredCommands()).filter(([name]) => name !== "/exit" && name !== "/quit").map(([name, cmd]) => ({ name, desc: cmd.desc }));
|
|
4517
|
-
const [cmdTipIndex, setCmdTipIndex] =
|
|
4518
|
-
const [cmdTipGradientColors, setCmdTipGradientColors] =
|
|
4519
|
-
const cmdTipPhaseRef =
|
|
4520
|
-
const [skillSelectIndex, setSkillSelectIndex] =
|
|
4521
|
-
const [fileSelectIndex, setFileSelectIndex] =
|
|
4522
|
-
const [inputKey, setInputKey] =
|
|
4523
|
-
const [rewindSelecting, setRewindSelecting] =
|
|
4524
|
-
const [rewindSelectIndex, setRewindSelectIndex] =
|
|
4525
|
-
const [rewindList, setRewindList] =
|
|
4526
|
-
const [rewinding, setRewinding] =
|
|
4527
|
-
const [rewindHintPhase, setRewindHintPhase] =
|
|
4528
|
-
const currentRoundModifiedRef =
|
|
4529
|
-
const rewindHintTimerRef =
|
|
4530
|
-
const [selectingModel, setSelectingModel] =
|
|
4531
|
-
const [modelSelectIndex, setModelSelectIndex] =
|
|
5219
|
+
const [cmdTipIndex, setCmdTipIndex] = useState4(0);
|
|
5220
|
+
const [cmdTipGradientColors, setCmdTipGradientColors] = useState4([]);
|
|
5221
|
+
const cmdTipPhaseRef = useRef3(0);
|
|
5222
|
+
const [skillSelectIndex, setSkillSelectIndex] = useState4(0);
|
|
5223
|
+
const [fileSelectIndex, setFileSelectIndex] = useState4(0);
|
|
5224
|
+
const [inputKey, setInputKey] = useState4(0);
|
|
5225
|
+
const [rewindSelecting, setRewindSelecting] = useState4(false);
|
|
5226
|
+
const [rewindSelectIndex, setRewindSelectIndex] = useState4(0);
|
|
5227
|
+
const [rewindList, setRewindList] = useState4([]);
|
|
5228
|
+
const [rewinding, setRewinding] = useState4(false);
|
|
5229
|
+
const [rewindHintPhase, setRewindHintPhase] = useState4("idle");
|
|
5230
|
+
const currentRoundModifiedRef = useRef3(false);
|
|
5231
|
+
const rewindHintTimerRef = useRef3(null);
|
|
5232
|
+
const [selectingModel, setSelectingModel] = useState4(false);
|
|
5233
|
+
const [modelSelectIndex, setModelSelectIndex] = useState4(0);
|
|
4532
5234
|
const modelOptions = ["deepseek-v4-flash", "deepseek-v4-pro"];
|
|
4533
|
-
const sessionRef =
|
|
4534
|
-
const abortRef =
|
|
4535
|
-
const currentContentRef =
|
|
4536
|
-
const currentToolCallsRef =
|
|
4537
|
-
const currentUsageRef =
|
|
4538
|
-
const currentElapsedRef =
|
|
4539
|
-
const currentCostRef =
|
|
4540
|
-
const currentModelRef =
|
|
4541
|
-
const streamErrorRef =
|
|
4542
|
-
|
|
5235
|
+
const sessionRef = useRef3(null);
|
|
5236
|
+
const abortRef = useRef3(null);
|
|
5237
|
+
const currentContentRef = useRef3("");
|
|
5238
|
+
const currentToolCallsRef = useRef3([]);
|
|
5239
|
+
const currentUsageRef = useRef3(void 0);
|
|
5240
|
+
const currentElapsedRef = useRef3(void 0);
|
|
5241
|
+
const currentCostRef = useRef3(void 0);
|
|
5242
|
+
const currentModelRef = useRef3(void 0);
|
|
5243
|
+
const streamErrorRef = useRef3(void 0);
|
|
5244
|
+
useEffect4(() => {
|
|
4543
5245
|
setSkillSelectIndex(0);
|
|
4544
5246
|
setFileSelectIndex(0);
|
|
4545
5247
|
}, [input]);
|
|
4546
|
-
|
|
5248
|
+
useEffect4(() => {
|
|
4547
5249
|
return () => {
|
|
4548
5250
|
if (rewindHintTimerRef.current) {
|
|
4549
5251
|
clearTimeout(rewindHintTimerRef.current);
|
|
@@ -4778,14 +5480,14 @@ function ChatSession({
|
|
|
4778
5480
|
[selectingModel, modelSelectIndex, modelOptions, activeModel, isStreaming, handleCtrlC, input, skills, skillSelectIndex, fileSelectIndex, getFilteredSkills, getFilteredFiles, sessionMode, setSessionMode, rewindSelecting, rewindSelectIndex, rewindList, doRewind]
|
|
4779
5481
|
)
|
|
4780
5482
|
);
|
|
4781
|
-
|
|
5483
|
+
useEffect4(() => {
|
|
4782
5484
|
if (cmdTips.length <= 1) return;
|
|
4783
5485
|
const timer = setInterval(() => {
|
|
4784
5486
|
setCmdTipIndex((prev) => (prev + 1) % cmdTips.length);
|
|
4785
5487
|
}, 2e3);
|
|
4786
5488
|
return () => clearInterval(timer);
|
|
4787
5489
|
}, [cmdTips.length]);
|
|
4788
|
-
|
|
5490
|
+
useEffect4(() => {
|
|
4789
5491
|
const tip = cmdTips[cmdTipIndex % cmdTips.length];
|
|
4790
5492
|
if (!tip) {
|
|
4791
5493
|
setCmdTipGradientColors([]);
|
|
@@ -4800,13 +5502,13 @@ function ChatSession({
|
|
|
4800
5502
|
}, GRADIENT_ANIMATION.cmdTipInterval);
|
|
4801
5503
|
return () => clearInterval(interval);
|
|
4802
5504
|
}, [cmdTipIndex, cmdTips.length]);
|
|
4803
|
-
|
|
5505
|
+
useEffect4(() => {
|
|
4804
5506
|
const timer = setInterval(() => {
|
|
4805
5507
|
setOffset((prev) => (prev + 1) % CYBER_PALETTE.length);
|
|
4806
5508
|
}, 500);
|
|
4807
5509
|
return () => clearInterval(timer);
|
|
4808
5510
|
}, []);
|
|
4809
|
-
|
|
5511
|
+
useEffect4(() => {
|
|
4810
5512
|
if (!apiKey || !baseUrl) return;
|
|
4811
5513
|
const provider = createProvider({
|
|
4812
5514
|
name: "deepseek",
|
|
@@ -4825,7 +5527,7 @@ function ChatSession({
|
|
|
4825
5527
|
sessionRef.current = null;
|
|
4826
5528
|
};
|
|
4827
5529
|
}, [apiKey, baseUrl, activeModel, externalCostTracker]);
|
|
4828
|
-
|
|
5530
|
+
useEffect4(() => {
|
|
4829
5531
|
if (!apiKey || !baseUrl) return;
|
|
4830
5532
|
let cancelled = false;
|
|
4831
5533
|
setBalanceLoading(true);
|
|
@@ -4850,7 +5552,7 @@ function ChatSession({
|
|
|
4850
5552
|
cancelled = true;
|
|
4851
5553
|
};
|
|
4852
5554
|
}, [apiKey, baseUrl]);
|
|
4853
|
-
|
|
5555
|
+
useEffect4(() => {
|
|
4854
5556
|
if (!externalCostTracker) return;
|
|
4855
5557
|
let cancelled = false;
|
|
4856
5558
|
let timer;
|
|
@@ -4868,7 +5570,7 @@ function ChatSession({
|
|
|
4868
5570
|
if (timer) clearInterval(timer);
|
|
4869
5571
|
};
|
|
4870
5572
|
}, [externalCostTracker]);
|
|
4871
|
-
|
|
5573
|
+
useEffect4(() => {
|
|
4872
5574
|
if (isStreaming || !idlePlaceholder) {
|
|
4873
5575
|
setGradientColors([]);
|
|
4874
5576
|
return;
|
|
@@ -4881,7 +5583,7 @@ function ChatSession({
|
|
|
4881
5583
|
}, GRADIENT_ANIMATION.idleInterval);
|
|
4882
5584
|
return () => clearInterval(interval);
|
|
4883
5585
|
}, [isStreaming, idlePlaceholder]);
|
|
4884
|
-
|
|
5586
|
+
useEffect4(() => {
|
|
4885
5587
|
if (!isStreaming || !streamingPlaceholder) {
|
|
4886
5588
|
setStreamingGradientColors([]);
|
|
4887
5589
|
return;
|
|
@@ -5231,7 +5933,7 @@ function ChatSession({
|
|
|
5231
5933
|
}
|
|
5232
5934
|
}
|
|
5233
5935
|
}, [onLaunchGame, onLaunchStock, currentContent, currentToolCalls, skills, skillSelectIndex, getFilteredSkills, thinkingEnabled, thinkingEffort, responseFormat, toolChoice, activeModel, sessionMode, isStreaming, rewinding]);
|
|
5234
|
-
|
|
5936
|
+
useEffect4(() => {
|
|
5235
5937
|
if (!isStreaming && externalCostTracker) {
|
|
5236
5938
|
setTodayCost(externalCostTracker.todayTotalCost);
|
|
5237
5939
|
}
|
|
@@ -5334,7 +6036,10 @@ function ChatSession({
|
|
|
5334
6036
|
{
|
|
5335
6037
|
content: currentContent,
|
|
5336
6038
|
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0,
|
|
5337
|
-
isStreaming: true
|
|
6039
|
+
isStreaming: true,
|
|
6040
|
+
usage: _currentUsage,
|
|
6041
|
+
cost: _currentCost,
|
|
6042
|
+
model: _streamingModel
|
|
5338
6043
|
}
|
|
5339
6044
|
),
|
|
5340
6045
|
!isStreaming && streamError && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, marginLeft: 3, children: /* @__PURE__ */ jsxs9(Text10, { color: "red", children: [
|
|
@@ -5406,7 +6111,7 @@ function ChatSession({
|
|
|
5406
6111
|
/* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text10, { color: "#808080", dimColor: true, children: "\u2191\u2193 \u9009\u62E9 \xB7 Enter \u786E\u8BA4 \xB7 Esc \u53D6\u6D88" }) })
|
|
5407
6112
|
] }),
|
|
5408
6113
|
/* @__PURE__ */ jsx9(Text10, { color: "#00ffff", dimColor: true, children: "\u2500".repeat(dividerWidth) })
|
|
5409
|
-
] }) : /* @__PURE__ */ jsxs9(
|
|
6114
|
+
] }) : /* @__PURE__ */ jsxs9(Fragment2, { children: [
|
|
5410
6115
|
(hasConversationStarted || sessionMode === "plan") && isStreaming && streamingPhase ? /* @__PURE__ */ jsx9(Box9, { marginTop: 1, justifyContent: "center", children: /* @__PURE__ */ jsxs9(Text10, { bold: true, color: PHASE_CONFIG[streamingPhase].color, children: [
|
|
5411
6116
|
PHASE_CONFIG[streamingPhase].icon,
|
|
5412
6117
|
" ",
|
|
@@ -5458,10 +6163,10 @@ function ChatSession({
|
|
|
5458
6163
|
|
|
5459
6164
|
// src/ui/GamePicker.tsx
|
|
5460
6165
|
import { Box as Box10, Text as Text11, useInput as useInput2 } from "ink";
|
|
5461
|
-
import { useState as
|
|
6166
|
+
import { useState as useState5, useCallback as useCallback3 } from "react";
|
|
5462
6167
|
import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
5463
6168
|
function GamePicker({ games, onSelect, onExit, onBackToChat }) {
|
|
5464
|
-
const [selectedIndex, setSelectedIndex] =
|
|
6169
|
+
const [selectedIndex, setSelectedIndex] = useState5(0);
|
|
5465
6170
|
const exitAction = onBackToChat ?? onExit ?? (() => process.exit(0));
|
|
5466
6171
|
const { doubleCtrlC, handleCtrlC } = useDoubleCtrlC(exitAction);
|
|
5467
6172
|
useInput2(
|
|
@@ -5516,7 +6221,7 @@ function listGames() {
|
|
|
5516
6221
|
|
|
5517
6222
|
// src/game/brick-breaker/index.tsx
|
|
5518
6223
|
import { Box as Box11, Text as Text12, useInput as useInput3, render as render2 } from "ink";
|
|
5519
|
-
import { useState as
|
|
6224
|
+
import { useState as useState6, useEffect as useEffect5, useRef as useRef4, useCallback as useCallback4 } from "react";
|
|
5520
6225
|
import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
5521
6226
|
var GAME_WIDTH = 40;
|
|
5522
6227
|
var GAME_HEIGHT = 18;
|
|
@@ -5648,13 +6353,13 @@ function buildBoard(state) {
|
|
|
5648
6353
|
return lines.map((l) => `\u2502${l}\u2502`).join("\n");
|
|
5649
6354
|
}
|
|
5650
6355
|
function BrickBreakerGame({ onExit: _onExit }) {
|
|
5651
|
-
const [initialLevel, setInitialLevel] =
|
|
5652
|
-
const [selectingLevel, setSelectingLevel] =
|
|
5653
|
-
const stateRef =
|
|
5654
|
-
const [tick2, setTick] =
|
|
5655
|
-
const onExitRef =
|
|
6356
|
+
const [initialLevel, setInitialLevel] = useState6(1);
|
|
6357
|
+
const [selectingLevel, setSelectingLevel] = useState6(true);
|
|
6358
|
+
const stateRef = useRef4(createInitialState(initialLevel));
|
|
6359
|
+
const [tick2, setTick] = useState6(0);
|
|
6360
|
+
const onExitRef = useRef4(_onExit);
|
|
5656
6361
|
onExitRef.current = _onExit;
|
|
5657
|
-
|
|
6362
|
+
useEffect5(() => {
|
|
5658
6363
|
if (selectingLevel) return;
|
|
5659
6364
|
const interval = setInterval(() => {
|
|
5660
6365
|
update(stateRef.current);
|
|
@@ -5788,7 +6493,7 @@ var brick_breaker_default = {
|
|
|
5788
6493
|
|
|
5789
6494
|
// src/game/coder-check/index.tsx
|
|
5790
6495
|
import { Box as Box12, Text as Text13, useInput as useInput4, render as render3 } from "ink";
|
|
5791
|
-
import { useState as
|
|
6496
|
+
import { useState as useState7, useEffect as useEffect6, useRef as useRef5, useCallback as useCallback5 } from "react";
|
|
5792
6497
|
import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
5793
6498
|
var GAME_W = 66;
|
|
5794
6499
|
var GAME_H = 20;
|
|
@@ -6180,18 +6885,18 @@ function buildGameView(s, scoreLines, scoreColor, message) {
|
|
|
6180
6885
|
return rows;
|
|
6181
6886
|
}
|
|
6182
6887
|
function CoderCheck({ onExit: _onExit }) {
|
|
6183
|
-
const stateRef =
|
|
6184
|
-
const [tick2, setTick] =
|
|
6185
|
-
const [colorOffset, setColorOffset] =
|
|
6186
|
-
const onExitRef =
|
|
6888
|
+
const stateRef = useRef5(createInitialState2());
|
|
6889
|
+
const [tick2, setTick] = useState7(0);
|
|
6890
|
+
const [colorOffset, setColorOffset] = useState7(0);
|
|
6891
|
+
const onExitRef = useRef5(_onExit);
|
|
6187
6892
|
onExitRef.current = _onExit;
|
|
6188
|
-
|
|
6893
|
+
useEffect6(() => {
|
|
6189
6894
|
const timer = setInterval(() => {
|
|
6190
6895
|
setColorOffset((prev) => (prev + 1) % CYBER_PALETTE2.length);
|
|
6191
6896
|
}, 400);
|
|
6192
6897
|
return () => clearInterval(timer);
|
|
6193
6898
|
}, []);
|
|
6194
|
-
|
|
6899
|
+
useEffect6(() => {
|
|
6195
6900
|
const interval = setInterval(() => {
|
|
6196
6901
|
update2(stateRef.current);
|
|
6197
6902
|
setTick((t) => t + 1);
|
|
@@ -6688,7 +7393,7 @@ import chalk5 from "chalk";
|
|
|
6688
7393
|
|
|
6689
7394
|
// src/stock/StockList.tsx
|
|
6690
7395
|
import { Box as Box13, Text as Text14, useInput as useInput5 } from "ink";
|
|
6691
|
-
import { useState as
|
|
7396
|
+
import { useState as useState8, useCallback as useCallback6, useEffect as useEffect7, useMemo as useMemo2 } from "react";
|
|
6692
7397
|
import asciichart from "asciichart";
|
|
6693
7398
|
import os from "os";
|
|
6694
7399
|
import { join as join10 } from "path";
|
|
@@ -6795,20 +7500,20 @@ function latestPoints(data, maxPoints = 60) {
|
|
|
6795
7500
|
return data.slice(data.length - maxPoints);
|
|
6796
7501
|
}
|
|
6797
7502
|
function StockList({ codes, onExit, onBackToChat }) {
|
|
6798
|
-
const [stocks, setStocks] =
|
|
6799
|
-
const [selectedIndex, setSelectedIndex] =
|
|
6800
|
-
const [loading, setLoading] =
|
|
6801
|
-
const [lastUpdate, setLastUpdate] =
|
|
6802
|
-
const [detailView, setDetailView] =
|
|
6803
|
-
const [detailPrices, setDetailPrices] =
|
|
6804
|
-
const [detailLoading, setDetailLoading] =
|
|
6805
|
-
const [detailCountdown, setDetailCountdown] =
|
|
6806
|
-
const [countdown, setCountdown] =
|
|
6807
|
-
const [currentTime, setCurrentTime] =
|
|
7503
|
+
const [stocks, setStocks] = useState8([]);
|
|
7504
|
+
const [selectedIndex, setSelectedIndex] = useState8(0);
|
|
7505
|
+
const [loading, setLoading] = useState8(true);
|
|
7506
|
+
const [lastUpdate, setLastUpdate] = useState8("");
|
|
7507
|
+
const [detailView, setDetailView] = useState8(null);
|
|
7508
|
+
const [detailPrices, setDetailPrices] = useState8(null);
|
|
7509
|
+
const [detailLoading, setDetailLoading] = useState8(false);
|
|
7510
|
+
const [detailCountdown, setDetailCountdown] = useState8(10);
|
|
7511
|
+
const [countdown, setCountdown] = useState8(5);
|
|
7512
|
+
const [currentTime, setCurrentTime] = useState8(
|
|
6808
7513
|
() => (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false })
|
|
6809
7514
|
);
|
|
6810
|
-
const [dimMode, setDimMode] =
|
|
6811
|
-
const [sortOrder, setSortOrder] =
|
|
7515
|
+
const [dimMode, setDimMode] = useState8(false);
|
|
7516
|
+
const [sortOrder, setSortOrder] = useState8("default");
|
|
6812
7517
|
const sortedStocks = useMemo2(() => {
|
|
6813
7518
|
if (sortOrder === "default") return stocks;
|
|
6814
7519
|
return [...stocks].toSorted(
|
|
@@ -6816,7 +7521,7 @@ function StockList({ codes, onExit, onBackToChat }) {
|
|
|
6816
7521
|
);
|
|
6817
7522
|
}, [stocks, sortOrder]);
|
|
6818
7523
|
const { doubleCtrlC, handleCtrlC } = useDoubleCtrlC(onExit);
|
|
6819
|
-
|
|
7524
|
+
useEffect7(() => {
|
|
6820
7525
|
const timer = setInterval(() => {
|
|
6821
7526
|
setCurrentTime((/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false }));
|
|
6822
7527
|
}, 1e3);
|
|
@@ -6832,10 +7537,10 @@ function StockList({ codes, onExit, onBackToChat }) {
|
|
|
6832
7537
|
}
|
|
6833
7538
|
setLoading(false);
|
|
6834
7539
|
}, [codes]);
|
|
6835
|
-
|
|
7540
|
+
useEffect7(() => {
|
|
6836
7541
|
void loadData();
|
|
6837
7542
|
}, [loadData]);
|
|
6838
|
-
|
|
7543
|
+
useEffect7(() => {
|
|
6839
7544
|
const interval = setInterval(() => {
|
|
6840
7545
|
setCountdown((prev) => {
|
|
6841
7546
|
if (prev <= 1) {
|
|
@@ -6847,7 +7552,7 @@ function StockList({ codes, onExit, onBackToChat }) {
|
|
|
6847
7552
|
}, 1e3);
|
|
6848
7553
|
return () => clearInterval(interval);
|
|
6849
7554
|
}, [loadData]);
|
|
6850
|
-
|
|
7555
|
+
useEffect7(() => {
|
|
6851
7556
|
if (detailView) {
|
|
6852
7557
|
const loadDetail = () => {
|
|
6853
7558
|
minuteCache.delete(detailView.code);
|
|
@@ -6866,7 +7571,7 @@ function StockList({ codes, onExit, onBackToChat }) {
|
|
|
6866
7571
|
setDetailPrices(null);
|
|
6867
7572
|
setDetailLoading(false);
|
|
6868
7573
|
}, [detailView]);
|
|
6869
|
-
|
|
7574
|
+
useEffect7(() => {
|
|
6870
7575
|
if (!detailView) return;
|
|
6871
7576
|
const timer = setInterval(() => {
|
|
6872
7577
|
setDetailCountdown((prev) => prev > 0 ? prev - 1 : 10);
|
|
@@ -7028,7 +7733,7 @@ function renderDetail(stock, _onBack, prices, countdown = 10, currentTime) {
|
|
|
7028
7733
|
|
|
7029
7734
|
// src/utils/scan-files.ts
|
|
7030
7735
|
import { readdir as readdir6 } from "fs/promises";
|
|
7031
|
-
import { join as join11, relative as
|
|
7736
|
+
import { join as join11, relative as relative8 } from "path";
|
|
7032
7737
|
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
7033
7738
|
"node_modules",
|
|
7034
7739
|
".git",
|
|
@@ -7093,7 +7798,7 @@ async function scanProjectFiles(baseDir, dir) {
|
|
|
7093
7798
|
} else if (entry.isFile()) {
|
|
7094
7799
|
const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
|
|
7095
7800
|
if (SOURCE_EXTS.has(ext)) {
|
|
7096
|
-
files.push(
|
|
7801
|
+
files.push(relative8(baseDir, join11(currentDir, name)));
|
|
7097
7802
|
}
|
|
7098
7803
|
}
|
|
7099
7804
|
}
|