opencode-gbk-tools 0.1.31 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,7 +25,7 @@
25
25
  ## 安装
26
26
 
27
27
  ```bash
28
- npx opencode-gbk-tools install
28
+ npx opencode-gbk-tools setup
29
29
  ```
30
30
 
31
31
  安装完成后会注册 `opencode-gbk-tools` plugin,由该 plugin 统一向全部 agents 暴露 `text_*` / `gbk_*` 工具与规则。
@@ -45,7 +45,7 @@ npx opencode-gbk-tools install
45
45
 
46
46
  ### 一键卸载(对应一键安装)
47
47
 
48
- 如果当初是用 `npx opencode-gbk-tools install` 全局安装的,执行:
48
+ 如果当初是用 `npx opencode-gbk-tools setup` 安装的,执行:
49
49
 
50
50
  ```bash
51
51
  npx opencode-gbk-tools uninstall
@@ -61,7 +61,7 @@ npx opencode-gbk-tools uninstall
61
61
 
62
62
  - 默认 `encoding=auto`
63
63
  - 新建 `.txt` 文件时,优先直接使用 `gbk_write` 创建;plugin 会把这类创建请求优先路由到 `gbk_*`
64
- - 当前会话只要已经成功使用过 `gbk_*`,后续新建文件也应继续优先使用 `gbk_write` / `gbk_*`
64
+ - 只要某个目标文件已确认或首次检测为 `gbk` / `gb18030`,该文件后续就会直接禁止内置工具与 `text_*`,统一改走 `gbk_*`
65
65
  - 已有文件会尽量保持:
66
66
  - 原编码
67
67
  - BOM
@@ -210,6 +210,8 @@ A:不要。现在优先用 `mode="insertAfter"` 或 `mode="insertBefore"`,
210
210
 
211
211
  | 版本 | 说明 |
212
212
  |------|------|
213
+ | 1.1.1 | 收敛最终路由规则并修正文档:继续保留“新建 `.txt` 强制走 `gbk_write`”,已有文件则严格按真实检测结果路由;一旦检测/记忆为 GBK/GB18030,就对该目标文件直接禁用内置 `read/write/edit` 与 `text_*`,后续统一走 `gbk_*`;同时把安装说明统一更正为 `npx opencode-gbk-tools setup` |
214
+ | 1.1.0 | 首个 `1.x` 稳定版本:保留 GBK/GB18030 路由、记忆与新建 `.txt` 走 `gbk_write` 的规则;对已确认或首次检测为 GBK/GB18030 的目标文件,统一禁止内置读写编辑工具与 `text_*`,直接改走 `gbk_*`;同时修复插件层同步自动 summarize 可能导致的会话卡住问题,并为会话状态与 `gbk-file` 行索引缓存增加边界控制,降低长会话和多文件场景下的阻塞与内存累积风险 |
213
215
  | 0.1.31 | 在 `0.1.30` 基础上继续收紧新建文件路由:新建 `.txt` 文件必须优先直接使用 `gbk_write`,不再先走 `text_write` 或内置 `write/edit`;并新增“当前会话一旦成功使用过 `gbk_*`,后续新建文件继续优先走 `gbk_*`”的插件级约束与测试覆盖 |
214
216
  | 0.1.30 | 补强 `text_*` 针对真实 GBK 样本与大体积重复样本的高强度测试:新增 `QFunction-0.txt` 真实样本读取、编辑、追加与端到端工作流覆盖,并补充大文件重复样本下的 `text_read` / `text_edit` 行为验证;同时继续收紧路由规则:新建 `.txt` 文件直接要求改用 `gbk_write`,且当前会话一旦使用过 `gbk_*`,后续新建文件也继续要求走 `gbk_*` |
215
217
  | 0.1.29 | 在方案 B 基础上新增 GBK 文件持久记忆:普通 agent 继续按编码分流,UTF-8 优先使用 OpenCode 内置工具;对现有文件,已确认或首次检测到是 GBK/GB18030 的路径会按完整路径 + mtime/size 持久记忆;后续再次操作同一路径时,`text_*` 与内置 `read` / `write` / `edit` 都会被直接拦截并提示改用 `gbk_*`,避免先误走 `text_edit` / `edit` 再失败;`gbk-engine` 继续保持为强制 GBK 专属模式 |
@@ -16243,10 +16243,18 @@ var FALLBACK_MAX_OUTPUT_CHARS = 8e3;
16243
16243
  var MIN_BASE_MAX_OUTPUT_CHARS = 4e3;
16244
16244
  var MAX_BASE_OUTPUT_CHARS = 32e3;
16245
16245
  var MIN_PRESSURED_OUTPUT_CHARS = 1500;
16246
+ var SESSION_STATE_TTL_MS = 6 * 60 * 60 * 1e3;
16246
16247
  var sessionStates = /* @__PURE__ */ new Map();
16248
+ function touchSessionState(state, touchedAt = Date.now()) {
16249
+ state.lastTouchedAt = touchedAt;
16250
+ }
16247
16251
  function getSessionState(sessionID) {
16248
16252
  if (!sessionID) return null;
16249
- return sessionStates.get(sessionID) ?? null;
16253
+ const state = sessionStates.get(sessionID) ?? null;
16254
+ if (state) {
16255
+ touchSessionState(state);
16256
+ }
16257
+ return state;
16250
16258
  }
16251
16259
  function getCompactionPressureFactor(compactionCount) {
16252
16260
  if (compactionCount >= 3) return 0.35;
@@ -16348,6 +16356,7 @@ async function assertPathAllowed(filePath, context, allowExternal = false) {
16348
16356
  // src/lib/gbk-file.ts
16349
16357
  var STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
16350
16358
  var STREAM_READ_CHUNK_SIZE_BYTES = 1024 * 1024;
16359
+ var MAX_GBK_LINE_INDEX_CACHE_ENTRIES = 32;
16351
16360
  var gbkLineIndexCache = /* @__PURE__ */ new Map();
16352
16361
  var ANSI_RED = "\x1B[31m";
16353
16362
  var ANSI_GREEN = "\x1B[32m";
@@ -16390,6 +16399,16 @@ function toSafeNumber(value) {
16390
16399
  function invalidateGbkLineIndex(filePath) {
16391
16400
  gbkLineIndexCache.delete(filePath);
16392
16401
  }
16402
+ function pruneGbkLineIndexCache() {
16403
+ if (gbkLineIndexCache.size <= MAX_GBK_LINE_INDEX_CACHE_ENTRIES) {
16404
+ return;
16405
+ }
16406
+ const overflow = gbkLineIndexCache.size - MAX_GBK_LINE_INDEX_CACHE_ENTRIES;
16407
+ const oldestKeys = [...gbkLineIndexCache.keys()].slice(0, overflow);
16408
+ for (const key of oldestKeys) {
16409
+ gbkLineIndexCache.delete(key);
16410
+ }
16411
+ }
16393
16412
  function assertStringArgument(value, name) {
16394
16413
  if (typeof value !== "string") {
16395
16414
  throw createGbkError("GBK_INVALID_ARGUMENT", `${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
@@ -16863,6 +16882,8 @@ async function visitDecodedTextChunks(input, visitor) {
16863
16882
  async function getGbkLineIndex(input) {
16864
16883
  const cached2 = gbkLineIndexCache.get(input.filePath);
16865
16884
  if (cached2 && cached2.fileSize === toSafeNumber(input.stat.size) && cached2.mtimeMs === toSafeNumber(input.stat.mtimeMs)) {
16885
+ gbkLineIndexCache.delete(input.filePath);
16886
+ gbkLineIndexCache.set(input.filePath, cached2);
16866
16887
  return cached2;
16867
16888
  }
16868
16889
  const lineStartOffsets = [0];
@@ -16908,6 +16929,7 @@ async function getGbkLineIndex(input) {
16908
16929
  newlineStyle: finalizeNewlineStyle(crlfCount, lfCount)
16909
16930
  };
16910
16931
  gbkLineIndexCache.set(input.filePath, result);
16932
+ pruneGbkLineIndexCache();
16911
16933
  return result;
16912
16934
  }
16913
16935
  async function readDecodedGbkByteRange(input, start, endExclusive) {
@@ -16306,10 +16306,21 @@ async function assertPathAllowed(filePath, context, allowExternal = false) {
16306
16306
  // src/lib/gbk-file.ts
16307
16307
  var STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
16308
16308
  var STREAM_READ_CHUNK_SIZE_BYTES = 1024 * 1024;
16309
+ var MAX_GBK_LINE_INDEX_CACHE_ENTRIES = 32;
16309
16310
  var gbkLineIndexCache = /* @__PURE__ */ new Map();
16310
16311
  function toSafeNumber(value) {
16311
16312
  return typeof value === "bigint" ? Number(value) : value;
16312
16313
  }
16314
+ function pruneGbkLineIndexCache() {
16315
+ if (gbkLineIndexCache.size <= MAX_GBK_LINE_INDEX_CACHE_ENTRIES) {
16316
+ return;
16317
+ }
16318
+ const overflow = gbkLineIndexCache.size - MAX_GBK_LINE_INDEX_CACHE_ENTRIES;
16319
+ const oldestKeys = [...gbkLineIndexCache.keys()].slice(0, overflow);
16320
+ for (const key of oldestKeys) {
16321
+ gbkLineIndexCache.delete(key);
16322
+ }
16323
+ }
16313
16324
  function assertEncodingSupported(encoding) {
16314
16325
  if (encoding !== "gbk" && encoding !== "gb18030") {
16315
16326
  throw createGbkError("GBK_INVALID_ENCODING", `\u4E0D\u652F\u6301\u7684\u7F16\u7801: ${encoding}`);
@@ -16451,6 +16462,8 @@ async function readWholeGbkTextFile(input) {
16451
16462
  async function getGbkLineIndex(input) {
16452
16463
  const cached2 = gbkLineIndexCache.get(input.filePath);
16453
16464
  if (cached2 && cached2.fileSize === toSafeNumber(input.stat.size) && cached2.mtimeMs === toSafeNumber(input.stat.mtimeMs)) {
16465
+ gbkLineIndexCache.delete(input.filePath);
16466
+ gbkLineIndexCache.set(input.filePath, cached2);
16454
16467
  return cached2;
16455
16468
  }
16456
16469
  const lineStartOffsets = [0];
@@ -16496,6 +16509,7 @@ async function getGbkLineIndex(input) {
16496
16509
  newlineStyle: finalizeNewlineStyle(crlfCount, lfCount)
16497
16510
  };
16498
16511
  gbkLineIndexCache.set(input.filePath, result);
16512
+ pruneGbkLineIndexCache();
16499
16513
  return result;
16500
16514
  }
16501
16515
  async function readDecodedGbkByteRange(input, start, endExclusive) {
@@ -16243,10 +16243,18 @@ var FALLBACK_MAX_OUTPUT_CHARS = 8e3;
16243
16243
  var MIN_BASE_MAX_OUTPUT_CHARS = 4e3;
16244
16244
  var MAX_BASE_OUTPUT_CHARS = 32e3;
16245
16245
  var MIN_PRESSURED_OUTPUT_CHARS = 1500;
16246
+ var SESSION_STATE_TTL_MS = 6 * 60 * 60 * 1e3;
16246
16247
  var sessionStates = /* @__PURE__ */ new Map();
16248
+ function touchSessionState(state, touchedAt = Date.now()) {
16249
+ state.lastTouchedAt = touchedAt;
16250
+ }
16247
16251
  function getSessionState(sessionID) {
16248
16252
  if (!sessionID) return null;
16249
- return sessionStates.get(sessionID) ?? null;
16253
+ const state = sessionStates.get(sessionID) ?? null;
16254
+ if (state) {
16255
+ touchSessionState(state);
16256
+ }
16257
+ return state;
16250
16258
  }
16251
16259
  function getCompactionPressureFactor(compactionCount) {
16252
16260
  if (compactionCount >= 3) return 0.35;
@@ -3817,102 +3817,9 @@ var require_lib = __commonJS({
3817
3817
  });
3818
3818
 
3819
3819
  // src/plugin/index.ts
3820
+ import fs5 from "fs/promises";
3820
3821
  import path5 from "path";
3821
3822
 
3822
- // src/lib/session-pressure.ts
3823
- var AUTO_SUMMARIZE_PRESSURE_RATIO = 0.85;
3824
- var AUTO_SUMMARIZE_COOLDOWN_MS = 6e4;
3825
- var PRESSURE_CHECK_INTERVAL_MS = 15e3;
3826
- var SESSION_PRESSURE_MESSAGE_LIMIT = 200;
3827
- function estimateUnknownChars(value) {
3828
- if (typeof value === "string") return value.length;
3829
- if (typeof value === "number" || typeof value === "boolean") return String(value).length;
3830
- if (Array.isArray(value)) {
3831
- return value.reduce((total, item) => total + estimateUnknownChars(item), 0);
3832
- }
3833
- if (!value || typeof value !== "object") return 0;
3834
- try {
3835
- return JSON.stringify(value).length;
3836
- } catch {
3837
- return 0;
3838
- }
3839
- }
3840
- function estimateDiffChars(summary) {
3841
- let chars = (summary.title?.length ?? 0) + (summary.body?.length ?? 0);
3842
- for (const diff of summary.diffs ?? []) {
3843
- chars += diff.file.length + diff.before.length + diff.after.length;
3844
- }
3845
- return chars;
3846
- }
3847
- function estimateToolPartChars(part) {
3848
- let chars = part.tool.length + estimateUnknownChars(part.metadata);
3849
- switch (part.state.status) {
3850
- case "pending":
3851
- chars += estimateUnknownChars(part.state.input) + part.state.raw.length;
3852
- break;
3853
- case "running":
3854
- chars += estimateUnknownChars(part.state.input);
3855
- chars += part.state.title?.length ?? 0;
3856
- chars += estimateUnknownChars(part.state.metadata);
3857
- break;
3858
- case "completed":
3859
- chars += estimateUnknownChars(part.state.input);
3860
- chars += part.state.output.length + part.state.title.length;
3861
- chars += estimateUnknownChars(part.state.metadata);
3862
- break;
3863
- case "error":
3864
- chars += estimateUnknownChars(part.state.input);
3865
- chars += part.state.error.length + estimateUnknownChars(part.state.metadata);
3866
- break;
3867
- }
3868
- return chars;
3869
- }
3870
- function estimatePartChars(part) {
3871
- switch (part.type) {
3872
- case "text":
3873
- return part.text.length;
3874
- case "reasoning":
3875
- return Math.round(part.text.length * 0.25);
3876
- case "file":
3877
- return (part.filename?.length ?? 0) + (part.source?.text.value.length ?? 0);
3878
- case "tool":
3879
- return estimateToolPartChars(part);
3880
- case "step-start":
3881
- return part.snapshot?.length ?? 0;
3882
- case "step-finish":
3883
- return part.reason.length + (part.snapshot?.length ?? 0);
3884
- case "snapshot":
3885
- return part.snapshot.length;
3886
- case "patch":
3887
- return part.hash.length + part.files.join("\n").length;
3888
- case "agent":
3889
- return part.name.length + (part.source?.value.length ?? 0);
3890
- case "retry":
3891
- return estimateUnknownChars(part.error);
3892
- case "compaction":
3893
- return 32;
3894
- case "subtask":
3895
- return part.prompt.length + part.description.length + part.agent.length;
3896
- }
3897
- }
3898
- function estimateMessageChars(message, parts) {
3899
- let chars = 0;
3900
- if (message.role === "user") {
3901
- chars += message.system?.length ?? 0;
3902
- if (message.summary) {
3903
- chars += estimateDiffChars(message.summary);
3904
- }
3905
- }
3906
- for (const part of parts) {
3907
- chars += estimatePartChars(part);
3908
- }
3909
- return chars;
3910
- }
3911
- function estimateSessionTokens(messages) {
3912
- const totalChars = messages.reduce((total, message) => total + estimateMessageChars(message.info, message.parts), 0);
3913
- return Math.ceil(totalChars / 4);
3914
- }
3915
-
3916
3823
  // src/lib/encoding-memory.ts
3917
3824
  import fs from "fs/promises";
3918
3825
  import os from "os";
@@ -16534,8 +16441,32 @@ var FALLBACK_MAX_OUTPUT_CHARS = 8e3;
16534
16441
  var MIN_BASE_MAX_OUTPUT_CHARS = 4e3;
16535
16442
  var MAX_BASE_OUTPUT_CHARS = 32e3;
16536
16443
  var MIN_PRESSURED_OUTPUT_CHARS = 1500;
16444
+ var SESSION_STATE_TTL_MS = 6 * 60 * 60 * 1e3;
16445
+ var MAX_SESSION_STATES = 200;
16537
16446
  var sessionStates = /* @__PURE__ */ new Map();
16447
+ function pruneSessionStates(now = Date.now()) {
16448
+ for (const [sessionID, state] of sessionStates) {
16449
+ if (now - state.lastTouchedAt > SESSION_STATE_TTL_MS) {
16450
+ sessionStates.delete(sessionID);
16451
+ }
16452
+ }
16453
+ if (sessionStates.size <= MAX_SESSION_STATES) {
16454
+ return;
16455
+ }
16456
+ const oldestEntries = [...sessionStates.entries()].sort((left, right) => left[1].lastTouchedAt - right[1].lastTouchedAt);
16457
+ for (const [sessionID] of oldestEntries) {
16458
+ if (sessionStates.size <= MAX_SESSION_STATES) {
16459
+ break;
16460
+ }
16461
+ sessionStates.delete(sessionID);
16462
+ }
16463
+ }
16464
+ function touchSessionState(state, touchedAt = Date.now()) {
16465
+ state.lastTouchedAt = touchedAt;
16466
+ }
16538
16467
  function getOrCreateSessionState(sessionID) {
16468
+ const now = Date.now();
16469
+ pruneSessionStates(now);
16539
16470
  let state = sessionStates.get(sessionID);
16540
16471
  if (!state) {
16541
16472
  state = {
@@ -16546,15 +16477,22 @@ function getOrCreateSessionState(sessionID) {
16546
16477
  lastPressureCheckedAt: null,
16547
16478
  autoSummarizeInFlight: false,
16548
16479
  lastAutoSummarizeAt: null,
16549
- autoSummarizeCount: 0
16480
+ autoSummarizeCount: 0,
16481
+ lastTouchedAt: now
16550
16482
  };
16551
16483
  sessionStates.set(sessionID, state);
16484
+ return state;
16552
16485
  }
16486
+ touchSessionState(state, now);
16553
16487
  return state;
16554
16488
  }
16555
16489
  function getSessionState(sessionID) {
16556
16490
  if (!sessionID) return null;
16557
- return sessionStates.get(sessionID) ?? null;
16491
+ const state = sessionStates.get(sessionID) ?? null;
16492
+ if (state) {
16493
+ touchSessionState(state);
16494
+ }
16495
+ return state;
16558
16496
  }
16559
16497
  function getCompactionPressureFactor(compactionCount) {
16560
16498
  if (compactionCount >= 3) return 0.35;
@@ -16590,20 +16528,6 @@ function markSessionCompacted(sessionID) {
16590
16528
  function getSessionCompactionCount(sessionID) {
16591
16529
  return getSessionState(sessionID)?.compactionCount ?? 0;
16592
16530
  }
16593
- function updateSessionPressure(sessionID, estimatedTokens, pressureRatio, checkedAt = Date.now()) {
16594
- if (!sessionID) return;
16595
- const state = getOrCreateSessionState(sessionID);
16596
- state.estimatedTokens = estimatedTokens;
16597
- state.pressureRatio = pressureRatio;
16598
- state.lastPressureCheckedAt = checkedAt;
16599
- }
16600
- function clearSessionPressure(sessionID) {
16601
- if (!sessionID) return;
16602
- const state = getOrCreateSessionState(sessionID);
16603
- state.estimatedTokens = null;
16604
- state.pressureRatio = null;
16605
- state.lastPressureCheckedAt = null;
16606
- }
16607
16531
  function getSessionPressure(sessionID) {
16608
16532
  const state = getSessionState(sessionID);
16609
16533
  if (!state || state.estimatedTokens === null || state.pressureRatio === null || state.lastPressureCheckedAt === null) {
@@ -16615,32 +16539,6 @@ function getSessionPressure(sessionID) {
16615
16539
  checkedAt: state.lastPressureCheckedAt
16616
16540
  };
16617
16541
  }
16618
- function isAutoSummarizeInFlight(sessionID) {
16619
- return getSessionState(sessionID)?.autoSummarizeInFlight ?? false;
16620
- }
16621
- function markAutoSummarizeStarted(sessionID) {
16622
- if (!sessionID) return;
16623
- const state = getOrCreateSessionState(sessionID);
16624
- state.autoSummarizeInFlight = true;
16625
- }
16626
- function markAutoSummarizeFinished(sessionID, summarized, finishedAt = Date.now()) {
16627
- if (!sessionID) return;
16628
- const state = getOrCreateSessionState(sessionID);
16629
- state.autoSummarizeInFlight = false;
16630
- if (summarized) {
16631
- state.lastAutoSummarizeAt = finishedAt;
16632
- state.autoSummarizeCount += 1;
16633
- state.estimatedTokens = null;
16634
- state.pressureRatio = null;
16635
- state.lastPressureCheckedAt = null;
16636
- }
16637
- }
16638
- function getLastAutoSummarizeAt(sessionID) {
16639
- return getSessionState(sessionID)?.lastAutoSummarizeAt ?? null;
16640
- }
16641
- function getAutoSummarizeCount(sessionID) {
16642
- return getSessionState(sessionID)?.autoSummarizeCount ?? 0;
16643
- }
16644
16542
  function getMaxOutputChars(sessionID, fallback = FALLBACK_MAX_OUTPUT_CHARS) {
16645
16543
  const state = getSessionState(sessionID);
16646
16544
  const base = getBaseMaxOutputChars(state?.contextTokens ?? null, fallback);
@@ -16663,6 +16561,7 @@ import fs3 from "fs/promises";
16663
16561
  import path3 from "path";
16664
16562
  var STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
16665
16563
  var STREAM_READ_CHUNK_SIZE_BYTES = 1024 * 1024;
16564
+ var MAX_GBK_LINE_INDEX_CACHE_ENTRIES = 32;
16666
16565
  var gbkLineIndexCache = /* @__PURE__ */ new Map();
16667
16566
  var ANSI_RED = "\x1B[31m";
16668
16567
  var ANSI_GREEN = "\x1B[32m";
@@ -16705,6 +16604,16 @@ function toSafeNumber(value) {
16705
16604
  function invalidateGbkLineIndex(filePath) {
16706
16605
  gbkLineIndexCache.delete(filePath);
16707
16606
  }
16607
+ function pruneGbkLineIndexCache() {
16608
+ if (gbkLineIndexCache.size <= MAX_GBK_LINE_INDEX_CACHE_ENTRIES) {
16609
+ return;
16610
+ }
16611
+ const overflow = gbkLineIndexCache.size - MAX_GBK_LINE_INDEX_CACHE_ENTRIES;
16612
+ const oldestKeys = [...gbkLineIndexCache.keys()].slice(0, overflow);
16613
+ for (const key of oldestKeys) {
16614
+ gbkLineIndexCache.delete(key);
16615
+ }
16616
+ }
16708
16617
  function assertStringArgument(value, name) {
16709
16618
  if (typeof value !== "string") {
16710
16619
  throw createGbkError("GBK_INVALID_ARGUMENT", `${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
@@ -17227,6 +17136,8 @@ async function visitDecodedTextChunks(input, visitor) {
17227
17136
  async function getGbkLineIndex(input) {
17228
17137
  const cached2 = gbkLineIndexCache.get(input.filePath);
17229
17138
  if (cached2 && cached2.fileSize === toSafeNumber(input.stat.size) && cached2.mtimeMs === toSafeNumber(input.stat.mtimeMs)) {
17139
+ gbkLineIndexCache.delete(input.filePath);
17140
+ gbkLineIndexCache.set(input.filePath, cached2);
17230
17141
  return cached2;
17231
17142
  }
17232
17143
  const lineStartOffsets = [0];
@@ -17272,6 +17183,7 @@ async function getGbkLineIndex(input) {
17272
17183
  newlineStyle: finalizeNewlineStyle(crlfCount, lfCount)
17273
17184
  };
17274
17185
  gbkLineIndexCache.set(input.filePath, result);
17186
+ pruneGbkLineIndexCache();
17275
17187
  return result;
17276
17188
  }
17277
17189
  async function readDecodedGbkByteRange(input, start, endExclusive) {
@@ -19492,7 +19404,8 @@ var TEXT_TOOL_SYSTEM_PROMPT = [
19492
19404
  "\u6587\u672C\u6587\u4EF6\u5904\u7406\u89C4\u5219\uFF1A",
19493
19405
  "- \u666E\u901A UTF-8 / UTF-8 BOM / UTF-16 \u6587\u672C\uFF0C\u4F18\u5148\u4F7F\u7528 OpenCode \u5185\u7F6E read\u3001write\u3001edit\u3002",
19494
19406
  "- \u9047\u5230 GBK / GB18030 \u6587\u4EF6\u3001\u4E2D\u6587\u4E71\u7801\u3001\u975E UTF-8 \u65E7\u6587\u672C\uFF0C\u4F18\u5148\u4F7F\u7528 gbk_read\u3001gbk_write\u3001gbk_edit\u3001gbk_search\uFF0C\u4E0D\u8981\u5148\u5C1D\u8BD5 text_* \u6216\u5185\u7F6E read/write/edit\u3002",
19495
- "- \u65B0\u5EFA .txt \u6587\u4EF6\u5728 encoding=auto \u4E0B\u9ED8\u8BA4\u4F7F\u7528 GBK\uFF1B\u5176\u4ED6\u65B0\u6587\u4EF6\u8BF7\u663E\u5F0F\u6307\u5B9A encoding\u3002",
19407
+ "- \u65B0\u5EFA .txt \u6587\u4EF6\u5FC5\u987B\u4F18\u5148\u76F4\u63A5\u4F7F\u7528 gbk_write \u521B\u5EFA\uFF0C\u4E0D\u8981\u5148\u7528 text_write \u6216\u5185\u7F6E write/edit\uFF1B\u5E95\u5C42 text_write \u5728 encoding=auto \u4E0B\u867D\u7136\u4E5F\u4F1A\u9ED8\u8BA4\u5199\u6210 GBK\uFF0C\u4F46\u63D2\u4EF6\u4F1A\u628A\u8FD9\u7C7B\u521B\u5EFA\u8BF7\u6C42\u76F4\u63A5\u8DEF\u7531\u5230 gbk_*\u3002",
19408
+ "- \u53EA\u8981\u67D0\u4E2A\u76EE\u6807\u6587\u4EF6\u5DF2\u786E\u8BA4\u6216\u9996\u6B21\u68C0\u6D4B\u4E3A GBK/GB18030\uFF0C\u8BE5\u6587\u4EF6\u540E\u7EED\u5C31\u76F4\u63A5\u7981\u6B62\u5185\u7F6E read/write/edit \u548C text_*\uFF0C\u7EDF\u4E00\u6539\u8D70 gbk_*\u3002",
19496
19409
  "- \u5BF9\u73B0\u6709 .txt / .cfg / .ini / .log \u7B49\u65E7\u6587\u672C\u6587\u4EF6\uFF0C\u53EA\u8981\u6000\u7591\u662F\u4E2D\u6587\u672C\u5730\u7F16\u7801\uFF0C\u4F18\u5148\u5148\u7528 gbk_read \u5224\u65AD\uFF0C\u4E0D\u8981\u5148 edit \u518D\u56E0\u4E3A\u5339\u914D\u5931\u8D25\u6216\u6587\u4EF6\u65F6\u95F4\u6233\u53D8\u5316\u800C\u56DE\u9000\u3002",
19497
19410
  "- \u5DF2\u786E\u8BA4\u6216\u9996\u6B21\u68C0\u6D4B\u5230\u662F GBK/GB18030 \u7684\u6587\u4EF6\u4F1A\u88AB\u63D2\u4EF6\u6301\u4E45\u8BB0\u5FC6\uFF1B\u518D\u6B21\u64CD\u4F5C\u540C\u4E00\u8DEF\u5F84\u65F6\uFF0C\u4F1A\u76F4\u63A5\u62E6\u622A\u5185\u7F6E read/write/edit \u548C text_*\uFF0C\u5E76\u8981\u6C42\u6539\u7528 gbk_*\u3002",
19498
19411
  "- \u5982\u679C\u610F\u56FE\u662F\u2018\u5728\u67D0\u6807\u7B7E\u524D\u540E\u63D2\u5165\u5185\u5BB9\u2019\uFF0C\u4F18\u5148\u4F7F\u7528 mode=insertAfter \u6216 mode=insertBefore\uFF0C\u5E76\u4F20 anchor/content\u3002",
@@ -19660,6 +19573,7 @@ var MANAGED_TOOL_IDS = /* @__PURE__ */ new Set([
19660
19573
  var BUILTIN_TEXT_TOOL_IDS = /* @__PURE__ */ new Set(["read", "write", "edit"]);
19661
19574
  var TEXT_TOOL_IDS = /* @__PURE__ */ new Set(["text_read", "text_write", "text_edit"]);
19662
19575
  var ROUTED_TEXT_TOOL_IDS = /* @__PURE__ */ new Set([...BUILTIN_TEXT_TOOL_IDS, ...TEXT_TOOL_IDS]);
19576
+ var CREATE_ROUTED_TEXT_TOOL_IDS = /* @__PURE__ */ new Set(["write", "edit", "text_write", "text_edit"]);
19663
19577
  function getToolFilePath(args) {
19664
19578
  if (!args || typeof args !== "object") {
19665
19579
  return null;
@@ -19683,6 +19597,23 @@ function buildGbkRoutingMessage(filePath, encoding) {
19683
19597
  function buildTextEditSessionRoutingMessage(filePath) {
19684
19598
  return `\u5F53\u524D\u4F1A\u8BDD\u5DF2\u5BF9\u8BE5\u6587\u4EF6\u4F7F\u7528 text_edit\uFF0C\u8BF7\u7EE7\u7EED\u4F7F\u7528 text_read\u3001text_write\u3001text_edit \u6216\u76F4\u63A5\u6539\u7528 gbk_*\uFF1B\u4E0D\u8981\u518D\u5207\u56DE\u5185\u7F6E read/write/edit\uFF0C\u4EE5\u514D\u89E6\u53D1\u6587\u4EF6\u65B0\u9C9C\u5EA6\u68C0\u67E5\u51B2\u7A81\uFF1A${filePath}`;
19685
19599
  }
19600
+ function buildNewTxtRoutingMessage(filePath) {
19601
+ return `\u65B0\u5EFA .txt \u6587\u4EF6\u8BF7\u76F4\u63A5\u4F7F\u7528 gbk_write \u521B\u5EFA\u4E3A GBK/GB18030\uFF1B\u4E0D\u8981\u5148\u4F7F\u7528\u5185\u7F6E write/edit \u6216 text_*\uFF1A${filePath}`;
19602
+ }
19603
+ function isNewTxtFilePath(filePath) {
19604
+ return path5.extname(filePath).toLowerCase() === ".txt";
19605
+ }
19606
+ async function doesPathExist(filePath) {
19607
+ try {
19608
+ await fs5.stat(filePath);
19609
+ return true;
19610
+ } catch (error45) {
19611
+ if (error45?.code === "ENOENT") {
19612
+ return false;
19613
+ }
19614
+ throw error45;
19615
+ }
19616
+ }
19686
19617
  async function detectExistingGbkEncoding(filePath, allowExternal, directory, worktree) {
19687
19618
  try {
19688
19619
  const detected = await detectTextFileEncoding({
@@ -19716,51 +19647,18 @@ function truncateMetadataPreview(value, sessionID) {
19716
19647
  return `${truncated}
19717
19648
  \x1B[2m... (metadata diffPreview \u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${previewMaxChars} \u5B57\u7B26\u4E0A\u9650)\x1B[0m`;
19718
19649
  }
19719
- async function maybeAutoSummarizeSession(client, directory, input) {
19720
- if (!client?.session?.messages || !client.session.summarize) return;
19721
- if (isAutoSummarizeInFlight(input.sessionID)) return;
19722
- const contextTokens = input.model.limit?.context;
19723
- if (typeof contextTokens !== "number" || contextTokens <= 0) return;
19724
- const now = Date.now();
19725
- const lastAutoSummarizeAt = getLastAutoSummarizeAt(input.sessionID);
19726
- if (lastAutoSummarizeAt !== null && now - lastAutoSummarizeAt < AUTO_SUMMARIZE_COOLDOWN_MS) {
19727
- return;
19728
- }
19729
- let pressure = getSessionPressure(input.sessionID);
19730
- if (!pressure || now - pressure.checkedAt >= PRESSURE_CHECK_INTERVAL_MS) {
19731
- const response = await client.session.messages({
19732
- path: { id: input.sessionID },
19733
- query: {
19734
- directory,
19735
- limit: SESSION_PRESSURE_MESSAGE_LIMIT
19736
- },
19737
- throwOnError: true
19738
- });
19739
- const estimatedTokens = estimateSessionTokens(Array.isArray(response.data) ? response.data : []);
19740
- const pressureRatio = estimatedTokens / contextTokens;
19741
- updateSessionPressure(input.sessionID, estimatedTokens, pressureRatio, now);
19742
- pressure = getSessionPressure(input.sessionID);
19743
- }
19744
- if (!pressure || pressure.pressureRatio < AUTO_SUMMARIZE_PRESSURE_RATIO) return;
19745
- markAutoSummarizeStarted(input.sessionID);
19746
- try {
19747
- await client.session.summarize({
19748
- path: { id: input.sessionID },
19749
- body: {
19750
- providerID: input.model.providerID,
19751
- modelID: input.model.id
19752
- },
19753
- query: { directory },
19754
- throwOnError: true
19755
- });
19756
- clearSessionPressure(input.sessionID);
19757
- markAutoSummarizeFinished(input.sessionID, true);
19758
- } catch {
19759
- markAutoSummarizeFinished(input.sessionID, false);
19760
- }
19761
- }
19762
- function createOpencodeGbkHooks(client, directory, worktree) {
19650
+ function createOpencodeGbkHooks(_client, directory, worktree) {
19763
19651
  const sessionTextEditedFiles = /* @__PURE__ */ new Map();
19652
+ const SESSION_TRACKING_LIMIT = 200;
19653
+ function pruneSessionTracking() {
19654
+ if (sessionTextEditedFiles.size > SESSION_TRACKING_LIMIT) {
19655
+ const overflow = sessionTextEditedFiles.size - SESSION_TRACKING_LIMIT;
19656
+ const sessionIDs = [...sessionTextEditedFiles.keys()].slice(0, overflow);
19657
+ for (const sessionID of sessionIDs) {
19658
+ sessionTextEditedFiles.delete(sessionID);
19659
+ }
19660
+ }
19661
+ }
19764
19662
  function rememberSessionTextEditFile(sessionID, normalizedFilePath) {
19765
19663
  if (!sessionID) {
19766
19664
  return;
@@ -19769,6 +19667,7 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19769
19667
  if (!files) {
19770
19668
  files = /* @__PURE__ */ new Set();
19771
19669
  sessionTextEditedFiles.set(sessionID, files);
19670
+ pruneSessionTracking();
19772
19671
  }
19773
19672
  files.add(normalizedFilePath);
19774
19673
  }
@@ -19794,13 +19693,13 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19794
19693
  if (sessionID) {
19795
19694
  markSessionCompacted(sessionID);
19796
19695
  }
19696
+ return;
19797
19697
  }
19798
19698
  },
19799
19699
  async "chat.params"(input) {
19800
19700
  const contextTokens = input.model?.limit?.context;
19801
19701
  if (typeof contextTokens === "number" && contextTokens > 0) {
19802
19702
  setCurrentContextTokens(input.sessionID, contextTokens);
19803
- await maybeAutoSummarizeSession(client, directory, input);
19804
19703
  }
19805
19704
  },
19806
19705
  async "tool.execute.before"(input, output) {
@@ -19816,6 +19715,11 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19816
19715
  throw new Error(buildTextEditSessionRoutingMessage(filePath));
19817
19716
  }
19818
19717
  const resolvedFilePath = resolveCandidatePath(filePath, { directory, worktree });
19718
+ if (CREATE_ROUTED_TEXT_TOOL_IDS.has(input.tool) && !await doesPathExist(resolvedFilePath)) {
19719
+ if (isNewTxtFilePath(resolvedFilePath)) {
19720
+ throw new Error(buildNewTxtRoutingMessage(filePath));
19721
+ }
19722
+ }
19819
19723
  const remembered = await getRememberedGbkEncoding(resolvedFilePath);
19820
19724
  if (remembered) {
19821
19725
  throw new Error(buildGbkRoutingMessage(filePath, remembered.encoding));
@@ -19840,7 +19744,6 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19840
19744
  const maxOutputChars = getMaxOutputChars(input.sessionID);
19841
19745
  const compactionCount = getSessionCompactionCount(input.sessionID);
19842
19746
  const sessionPressure = getSessionPressure(input.sessionID);
19843
- const autoSummarizeCount = getAutoSummarizeCount(input.sessionID);
19844
19747
  const metadata = output.metadata && typeof output.metadata === "object" ? { ...output.metadata } : {};
19845
19748
  const nextOutput = truncateToolOutput(output.output, input.sessionID);
19846
19749
  if (nextOutput !== output.output) {
@@ -19869,9 +19772,6 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19869
19772
  metadata.estimatedSessionTokens = sessionPressure.estimatedTokens;
19870
19773
  metadata.sessionPressureRatio = Number(sessionPressure.pressureRatio.toFixed(3));
19871
19774
  }
19872
- if (autoSummarizeCount > 0) {
19873
- metadata.autoSummarizeCount = autoSummarizeCount;
19874
- }
19875
19775
  output.metadata = metadata;
19876
19776
  },
19877
19777
  async "experimental.chat.system.transform"(_input, output) {
@@ -3817,102 +3817,9 @@ var require_lib = __commonJS({
3817
3817
  });
3818
3818
 
3819
3819
  // src/plugin/index.ts
3820
+ import fs5 from "fs/promises";
3820
3821
  import path5 from "path";
3821
3822
 
3822
- // src/lib/session-pressure.ts
3823
- var AUTO_SUMMARIZE_PRESSURE_RATIO = 0.85;
3824
- var AUTO_SUMMARIZE_COOLDOWN_MS = 6e4;
3825
- var PRESSURE_CHECK_INTERVAL_MS = 15e3;
3826
- var SESSION_PRESSURE_MESSAGE_LIMIT = 200;
3827
- function estimateUnknownChars(value) {
3828
- if (typeof value === "string") return value.length;
3829
- if (typeof value === "number" || typeof value === "boolean") return String(value).length;
3830
- if (Array.isArray(value)) {
3831
- return value.reduce((total, item) => total + estimateUnknownChars(item), 0);
3832
- }
3833
- if (!value || typeof value !== "object") return 0;
3834
- try {
3835
- return JSON.stringify(value).length;
3836
- } catch {
3837
- return 0;
3838
- }
3839
- }
3840
- function estimateDiffChars(summary) {
3841
- let chars = (summary.title?.length ?? 0) + (summary.body?.length ?? 0);
3842
- for (const diff of summary.diffs ?? []) {
3843
- chars += diff.file.length + diff.before.length + diff.after.length;
3844
- }
3845
- return chars;
3846
- }
3847
- function estimateToolPartChars(part) {
3848
- let chars = part.tool.length + estimateUnknownChars(part.metadata);
3849
- switch (part.state.status) {
3850
- case "pending":
3851
- chars += estimateUnknownChars(part.state.input) + part.state.raw.length;
3852
- break;
3853
- case "running":
3854
- chars += estimateUnknownChars(part.state.input);
3855
- chars += part.state.title?.length ?? 0;
3856
- chars += estimateUnknownChars(part.state.metadata);
3857
- break;
3858
- case "completed":
3859
- chars += estimateUnknownChars(part.state.input);
3860
- chars += part.state.output.length + part.state.title.length;
3861
- chars += estimateUnknownChars(part.state.metadata);
3862
- break;
3863
- case "error":
3864
- chars += estimateUnknownChars(part.state.input);
3865
- chars += part.state.error.length + estimateUnknownChars(part.state.metadata);
3866
- break;
3867
- }
3868
- return chars;
3869
- }
3870
- function estimatePartChars(part) {
3871
- switch (part.type) {
3872
- case "text":
3873
- return part.text.length;
3874
- case "reasoning":
3875
- return Math.round(part.text.length * 0.25);
3876
- case "file":
3877
- return (part.filename?.length ?? 0) + (part.source?.text.value.length ?? 0);
3878
- case "tool":
3879
- return estimateToolPartChars(part);
3880
- case "step-start":
3881
- return part.snapshot?.length ?? 0;
3882
- case "step-finish":
3883
- return part.reason.length + (part.snapshot?.length ?? 0);
3884
- case "snapshot":
3885
- return part.snapshot.length;
3886
- case "patch":
3887
- return part.hash.length + part.files.join("\n").length;
3888
- case "agent":
3889
- return part.name.length + (part.source?.value.length ?? 0);
3890
- case "retry":
3891
- return estimateUnknownChars(part.error);
3892
- case "compaction":
3893
- return 32;
3894
- case "subtask":
3895
- return part.prompt.length + part.description.length + part.agent.length;
3896
- }
3897
- }
3898
- function estimateMessageChars(message, parts) {
3899
- let chars = 0;
3900
- if (message.role === "user") {
3901
- chars += message.system?.length ?? 0;
3902
- if (message.summary) {
3903
- chars += estimateDiffChars(message.summary);
3904
- }
3905
- }
3906
- for (const part of parts) {
3907
- chars += estimatePartChars(part);
3908
- }
3909
- return chars;
3910
- }
3911
- function estimateSessionTokens(messages) {
3912
- const totalChars = messages.reduce((total, message) => total + estimateMessageChars(message.info, message.parts), 0);
3913
- return Math.ceil(totalChars / 4);
3914
- }
3915
-
3916
3823
  // src/lib/encoding-memory.ts
3917
3824
  import fs from "fs/promises";
3918
3825
  import os from "os";
@@ -16534,8 +16441,32 @@ var FALLBACK_MAX_OUTPUT_CHARS = 8e3;
16534
16441
  var MIN_BASE_MAX_OUTPUT_CHARS = 4e3;
16535
16442
  var MAX_BASE_OUTPUT_CHARS = 32e3;
16536
16443
  var MIN_PRESSURED_OUTPUT_CHARS = 1500;
16444
+ var SESSION_STATE_TTL_MS = 6 * 60 * 60 * 1e3;
16445
+ var MAX_SESSION_STATES = 200;
16537
16446
  var sessionStates = /* @__PURE__ */ new Map();
16447
+ function pruneSessionStates(now = Date.now()) {
16448
+ for (const [sessionID, state] of sessionStates) {
16449
+ if (now - state.lastTouchedAt > SESSION_STATE_TTL_MS) {
16450
+ sessionStates.delete(sessionID);
16451
+ }
16452
+ }
16453
+ if (sessionStates.size <= MAX_SESSION_STATES) {
16454
+ return;
16455
+ }
16456
+ const oldestEntries = [...sessionStates.entries()].sort((left, right) => left[1].lastTouchedAt - right[1].lastTouchedAt);
16457
+ for (const [sessionID] of oldestEntries) {
16458
+ if (sessionStates.size <= MAX_SESSION_STATES) {
16459
+ break;
16460
+ }
16461
+ sessionStates.delete(sessionID);
16462
+ }
16463
+ }
16464
+ function touchSessionState(state, touchedAt = Date.now()) {
16465
+ state.lastTouchedAt = touchedAt;
16466
+ }
16538
16467
  function getOrCreateSessionState(sessionID) {
16468
+ const now = Date.now();
16469
+ pruneSessionStates(now);
16539
16470
  let state = sessionStates.get(sessionID);
16540
16471
  if (!state) {
16541
16472
  state = {
@@ -16546,15 +16477,22 @@ function getOrCreateSessionState(sessionID) {
16546
16477
  lastPressureCheckedAt: null,
16547
16478
  autoSummarizeInFlight: false,
16548
16479
  lastAutoSummarizeAt: null,
16549
- autoSummarizeCount: 0
16480
+ autoSummarizeCount: 0,
16481
+ lastTouchedAt: now
16550
16482
  };
16551
16483
  sessionStates.set(sessionID, state);
16484
+ return state;
16552
16485
  }
16486
+ touchSessionState(state, now);
16553
16487
  return state;
16554
16488
  }
16555
16489
  function getSessionState(sessionID) {
16556
16490
  if (!sessionID) return null;
16557
- return sessionStates.get(sessionID) ?? null;
16491
+ const state = sessionStates.get(sessionID) ?? null;
16492
+ if (state) {
16493
+ touchSessionState(state);
16494
+ }
16495
+ return state;
16558
16496
  }
16559
16497
  function getCompactionPressureFactor(compactionCount) {
16560
16498
  if (compactionCount >= 3) return 0.35;
@@ -16590,20 +16528,6 @@ function markSessionCompacted(sessionID) {
16590
16528
  function getSessionCompactionCount(sessionID) {
16591
16529
  return getSessionState(sessionID)?.compactionCount ?? 0;
16592
16530
  }
16593
- function updateSessionPressure(sessionID, estimatedTokens, pressureRatio, checkedAt = Date.now()) {
16594
- if (!sessionID) return;
16595
- const state = getOrCreateSessionState(sessionID);
16596
- state.estimatedTokens = estimatedTokens;
16597
- state.pressureRatio = pressureRatio;
16598
- state.lastPressureCheckedAt = checkedAt;
16599
- }
16600
- function clearSessionPressure(sessionID) {
16601
- if (!sessionID) return;
16602
- const state = getOrCreateSessionState(sessionID);
16603
- state.estimatedTokens = null;
16604
- state.pressureRatio = null;
16605
- state.lastPressureCheckedAt = null;
16606
- }
16607
16531
  function getSessionPressure(sessionID) {
16608
16532
  const state = getSessionState(sessionID);
16609
16533
  if (!state || state.estimatedTokens === null || state.pressureRatio === null || state.lastPressureCheckedAt === null) {
@@ -16615,32 +16539,6 @@ function getSessionPressure(sessionID) {
16615
16539
  checkedAt: state.lastPressureCheckedAt
16616
16540
  };
16617
16541
  }
16618
- function isAutoSummarizeInFlight(sessionID) {
16619
- return getSessionState(sessionID)?.autoSummarizeInFlight ?? false;
16620
- }
16621
- function markAutoSummarizeStarted(sessionID) {
16622
- if (!sessionID) return;
16623
- const state = getOrCreateSessionState(sessionID);
16624
- state.autoSummarizeInFlight = true;
16625
- }
16626
- function markAutoSummarizeFinished(sessionID, summarized, finishedAt = Date.now()) {
16627
- if (!sessionID) return;
16628
- const state = getOrCreateSessionState(sessionID);
16629
- state.autoSummarizeInFlight = false;
16630
- if (summarized) {
16631
- state.lastAutoSummarizeAt = finishedAt;
16632
- state.autoSummarizeCount += 1;
16633
- state.estimatedTokens = null;
16634
- state.pressureRatio = null;
16635
- state.lastPressureCheckedAt = null;
16636
- }
16637
- }
16638
- function getLastAutoSummarizeAt(sessionID) {
16639
- return getSessionState(sessionID)?.lastAutoSummarizeAt ?? null;
16640
- }
16641
- function getAutoSummarizeCount(sessionID) {
16642
- return getSessionState(sessionID)?.autoSummarizeCount ?? 0;
16643
- }
16644
16542
  function getMaxOutputChars(sessionID, fallback = FALLBACK_MAX_OUTPUT_CHARS) {
16645
16543
  const state = getSessionState(sessionID);
16646
16544
  const base = getBaseMaxOutputChars(state?.contextTokens ?? null, fallback);
@@ -16663,6 +16561,7 @@ import fs3 from "fs/promises";
16663
16561
  import path3 from "path";
16664
16562
  var STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
16665
16563
  var STREAM_READ_CHUNK_SIZE_BYTES = 1024 * 1024;
16564
+ var MAX_GBK_LINE_INDEX_CACHE_ENTRIES = 32;
16666
16565
  var gbkLineIndexCache = /* @__PURE__ */ new Map();
16667
16566
  var ANSI_RED = "\x1B[31m";
16668
16567
  var ANSI_GREEN = "\x1B[32m";
@@ -16705,6 +16604,16 @@ function toSafeNumber(value) {
16705
16604
  function invalidateGbkLineIndex(filePath) {
16706
16605
  gbkLineIndexCache.delete(filePath);
16707
16606
  }
16607
+ function pruneGbkLineIndexCache() {
16608
+ if (gbkLineIndexCache.size <= MAX_GBK_LINE_INDEX_CACHE_ENTRIES) {
16609
+ return;
16610
+ }
16611
+ const overflow = gbkLineIndexCache.size - MAX_GBK_LINE_INDEX_CACHE_ENTRIES;
16612
+ const oldestKeys = [...gbkLineIndexCache.keys()].slice(0, overflow);
16613
+ for (const key of oldestKeys) {
16614
+ gbkLineIndexCache.delete(key);
16615
+ }
16616
+ }
16708
16617
  function assertStringArgument(value, name) {
16709
16618
  if (typeof value !== "string") {
16710
16619
  throw createGbkError("GBK_INVALID_ARGUMENT", `${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
@@ -17227,6 +17136,8 @@ async function visitDecodedTextChunks(input, visitor) {
17227
17136
  async function getGbkLineIndex(input) {
17228
17137
  const cached2 = gbkLineIndexCache.get(input.filePath);
17229
17138
  if (cached2 && cached2.fileSize === toSafeNumber(input.stat.size) && cached2.mtimeMs === toSafeNumber(input.stat.mtimeMs)) {
17139
+ gbkLineIndexCache.delete(input.filePath);
17140
+ gbkLineIndexCache.set(input.filePath, cached2);
17230
17141
  return cached2;
17231
17142
  }
17232
17143
  const lineStartOffsets = [0];
@@ -17272,6 +17183,7 @@ async function getGbkLineIndex(input) {
17272
17183
  newlineStyle: finalizeNewlineStyle(crlfCount, lfCount)
17273
17184
  };
17274
17185
  gbkLineIndexCache.set(input.filePath, result);
17186
+ pruneGbkLineIndexCache();
17275
17187
  return result;
17276
17188
  }
17277
17189
  async function readDecodedGbkByteRange(input, start, endExclusive) {
@@ -19492,7 +19404,8 @@ var TEXT_TOOL_SYSTEM_PROMPT = [
19492
19404
  "\u6587\u672C\u6587\u4EF6\u5904\u7406\u89C4\u5219\uFF1A",
19493
19405
  "- \u666E\u901A UTF-8 / UTF-8 BOM / UTF-16 \u6587\u672C\uFF0C\u4F18\u5148\u4F7F\u7528 OpenCode \u5185\u7F6E read\u3001write\u3001edit\u3002",
19494
19406
  "- \u9047\u5230 GBK / GB18030 \u6587\u4EF6\u3001\u4E2D\u6587\u4E71\u7801\u3001\u975E UTF-8 \u65E7\u6587\u672C\uFF0C\u4F18\u5148\u4F7F\u7528 gbk_read\u3001gbk_write\u3001gbk_edit\u3001gbk_search\uFF0C\u4E0D\u8981\u5148\u5C1D\u8BD5 text_* \u6216\u5185\u7F6E read/write/edit\u3002",
19495
- "- \u65B0\u5EFA .txt \u6587\u4EF6\u5728 encoding=auto \u4E0B\u9ED8\u8BA4\u4F7F\u7528 GBK\uFF1B\u5176\u4ED6\u65B0\u6587\u4EF6\u8BF7\u663E\u5F0F\u6307\u5B9A encoding\u3002",
19407
+ "- \u65B0\u5EFA .txt \u6587\u4EF6\u5FC5\u987B\u4F18\u5148\u76F4\u63A5\u4F7F\u7528 gbk_write \u521B\u5EFA\uFF0C\u4E0D\u8981\u5148\u7528 text_write \u6216\u5185\u7F6E write/edit\uFF1B\u5E95\u5C42 text_write \u5728 encoding=auto \u4E0B\u867D\u7136\u4E5F\u4F1A\u9ED8\u8BA4\u5199\u6210 GBK\uFF0C\u4F46\u63D2\u4EF6\u4F1A\u628A\u8FD9\u7C7B\u521B\u5EFA\u8BF7\u6C42\u76F4\u63A5\u8DEF\u7531\u5230 gbk_*\u3002",
19408
+ "- \u53EA\u8981\u67D0\u4E2A\u76EE\u6807\u6587\u4EF6\u5DF2\u786E\u8BA4\u6216\u9996\u6B21\u68C0\u6D4B\u4E3A GBK/GB18030\uFF0C\u8BE5\u6587\u4EF6\u540E\u7EED\u5C31\u76F4\u63A5\u7981\u6B62\u5185\u7F6E read/write/edit \u548C text_*\uFF0C\u7EDF\u4E00\u6539\u8D70 gbk_*\u3002",
19496
19409
  "- \u5BF9\u73B0\u6709 .txt / .cfg / .ini / .log \u7B49\u65E7\u6587\u672C\u6587\u4EF6\uFF0C\u53EA\u8981\u6000\u7591\u662F\u4E2D\u6587\u672C\u5730\u7F16\u7801\uFF0C\u4F18\u5148\u5148\u7528 gbk_read \u5224\u65AD\uFF0C\u4E0D\u8981\u5148 edit \u518D\u56E0\u4E3A\u5339\u914D\u5931\u8D25\u6216\u6587\u4EF6\u65F6\u95F4\u6233\u53D8\u5316\u800C\u56DE\u9000\u3002",
19497
19410
  "- \u5DF2\u786E\u8BA4\u6216\u9996\u6B21\u68C0\u6D4B\u5230\u662F GBK/GB18030 \u7684\u6587\u4EF6\u4F1A\u88AB\u63D2\u4EF6\u6301\u4E45\u8BB0\u5FC6\uFF1B\u518D\u6B21\u64CD\u4F5C\u540C\u4E00\u8DEF\u5F84\u65F6\uFF0C\u4F1A\u76F4\u63A5\u62E6\u622A\u5185\u7F6E read/write/edit \u548C text_*\uFF0C\u5E76\u8981\u6C42\u6539\u7528 gbk_*\u3002",
19498
19411
  "- \u5982\u679C\u610F\u56FE\u662F\u2018\u5728\u67D0\u6807\u7B7E\u524D\u540E\u63D2\u5165\u5185\u5BB9\u2019\uFF0C\u4F18\u5148\u4F7F\u7528 mode=insertAfter \u6216 mode=insertBefore\uFF0C\u5E76\u4F20 anchor/content\u3002",
@@ -19660,6 +19573,7 @@ var MANAGED_TOOL_IDS = /* @__PURE__ */ new Set([
19660
19573
  var BUILTIN_TEXT_TOOL_IDS = /* @__PURE__ */ new Set(["read", "write", "edit"]);
19661
19574
  var TEXT_TOOL_IDS = /* @__PURE__ */ new Set(["text_read", "text_write", "text_edit"]);
19662
19575
  var ROUTED_TEXT_TOOL_IDS = /* @__PURE__ */ new Set([...BUILTIN_TEXT_TOOL_IDS, ...TEXT_TOOL_IDS]);
19576
+ var CREATE_ROUTED_TEXT_TOOL_IDS = /* @__PURE__ */ new Set(["write", "edit", "text_write", "text_edit"]);
19663
19577
  function getToolFilePath(args) {
19664
19578
  if (!args || typeof args !== "object") {
19665
19579
  return null;
@@ -19683,6 +19597,23 @@ function buildGbkRoutingMessage(filePath, encoding) {
19683
19597
  function buildTextEditSessionRoutingMessage(filePath) {
19684
19598
  return `\u5F53\u524D\u4F1A\u8BDD\u5DF2\u5BF9\u8BE5\u6587\u4EF6\u4F7F\u7528 text_edit\uFF0C\u8BF7\u7EE7\u7EED\u4F7F\u7528 text_read\u3001text_write\u3001text_edit \u6216\u76F4\u63A5\u6539\u7528 gbk_*\uFF1B\u4E0D\u8981\u518D\u5207\u56DE\u5185\u7F6E read/write/edit\uFF0C\u4EE5\u514D\u89E6\u53D1\u6587\u4EF6\u65B0\u9C9C\u5EA6\u68C0\u67E5\u51B2\u7A81\uFF1A${filePath}`;
19685
19599
  }
19600
+ function buildNewTxtRoutingMessage(filePath) {
19601
+ return `\u65B0\u5EFA .txt \u6587\u4EF6\u8BF7\u76F4\u63A5\u4F7F\u7528 gbk_write \u521B\u5EFA\u4E3A GBK/GB18030\uFF1B\u4E0D\u8981\u5148\u4F7F\u7528\u5185\u7F6E write/edit \u6216 text_*\uFF1A${filePath}`;
19602
+ }
19603
+ function isNewTxtFilePath(filePath) {
19604
+ return path5.extname(filePath).toLowerCase() === ".txt";
19605
+ }
19606
+ async function doesPathExist(filePath) {
19607
+ try {
19608
+ await fs5.stat(filePath);
19609
+ return true;
19610
+ } catch (error45) {
19611
+ if (error45?.code === "ENOENT") {
19612
+ return false;
19613
+ }
19614
+ throw error45;
19615
+ }
19616
+ }
19686
19617
  async function detectExistingGbkEncoding(filePath, allowExternal, directory, worktree) {
19687
19618
  try {
19688
19619
  const detected = await detectTextFileEncoding({
@@ -19716,51 +19647,18 @@ function truncateMetadataPreview(value, sessionID) {
19716
19647
  return `${truncated}
19717
19648
  \x1B[2m... (metadata diffPreview \u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${previewMaxChars} \u5B57\u7B26\u4E0A\u9650)\x1B[0m`;
19718
19649
  }
19719
- async function maybeAutoSummarizeSession(client, directory, input) {
19720
- if (!client?.session?.messages || !client.session.summarize) return;
19721
- if (isAutoSummarizeInFlight(input.sessionID)) return;
19722
- const contextTokens = input.model.limit?.context;
19723
- if (typeof contextTokens !== "number" || contextTokens <= 0) return;
19724
- const now = Date.now();
19725
- const lastAutoSummarizeAt = getLastAutoSummarizeAt(input.sessionID);
19726
- if (lastAutoSummarizeAt !== null && now - lastAutoSummarizeAt < AUTO_SUMMARIZE_COOLDOWN_MS) {
19727
- return;
19728
- }
19729
- let pressure = getSessionPressure(input.sessionID);
19730
- if (!pressure || now - pressure.checkedAt >= PRESSURE_CHECK_INTERVAL_MS) {
19731
- const response = await client.session.messages({
19732
- path: { id: input.sessionID },
19733
- query: {
19734
- directory,
19735
- limit: SESSION_PRESSURE_MESSAGE_LIMIT
19736
- },
19737
- throwOnError: true
19738
- });
19739
- const estimatedTokens = estimateSessionTokens(Array.isArray(response.data) ? response.data : []);
19740
- const pressureRatio = estimatedTokens / contextTokens;
19741
- updateSessionPressure(input.sessionID, estimatedTokens, pressureRatio, now);
19742
- pressure = getSessionPressure(input.sessionID);
19743
- }
19744
- if (!pressure || pressure.pressureRatio < AUTO_SUMMARIZE_PRESSURE_RATIO) return;
19745
- markAutoSummarizeStarted(input.sessionID);
19746
- try {
19747
- await client.session.summarize({
19748
- path: { id: input.sessionID },
19749
- body: {
19750
- providerID: input.model.providerID,
19751
- modelID: input.model.id
19752
- },
19753
- query: { directory },
19754
- throwOnError: true
19755
- });
19756
- clearSessionPressure(input.sessionID);
19757
- markAutoSummarizeFinished(input.sessionID, true);
19758
- } catch {
19759
- markAutoSummarizeFinished(input.sessionID, false);
19760
- }
19761
- }
19762
- function createOpencodeGbkHooks(client, directory, worktree) {
19650
+ function createOpencodeGbkHooks(_client, directory, worktree) {
19763
19651
  const sessionTextEditedFiles = /* @__PURE__ */ new Map();
19652
+ const SESSION_TRACKING_LIMIT = 200;
19653
+ function pruneSessionTracking() {
19654
+ if (sessionTextEditedFiles.size > SESSION_TRACKING_LIMIT) {
19655
+ const overflow = sessionTextEditedFiles.size - SESSION_TRACKING_LIMIT;
19656
+ const sessionIDs = [...sessionTextEditedFiles.keys()].slice(0, overflow);
19657
+ for (const sessionID of sessionIDs) {
19658
+ sessionTextEditedFiles.delete(sessionID);
19659
+ }
19660
+ }
19661
+ }
19764
19662
  function rememberSessionTextEditFile(sessionID, normalizedFilePath) {
19765
19663
  if (!sessionID) {
19766
19664
  return;
@@ -19769,6 +19667,7 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19769
19667
  if (!files) {
19770
19668
  files = /* @__PURE__ */ new Set();
19771
19669
  sessionTextEditedFiles.set(sessionID, files);
19670
+ pruneSessionTracking();
19772
19671
  }
19773
19672
  files.add(normalizedFilePath);
19774
19673
  }
@@ -19794,13 +19693,13 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19794
19693
  if (sessionID) {
19795
19694
  markSessionCompacted(sessionID);
19796
19695
  }
19696
+ return;
19797
19697
  }
19798
19698
  },
19799
19699
  async "chat.params"(input) {
19800
19700
  const contextTokens = input.model?.limit?.context;
19801
19701
  if (typeof contextTokens === "number" && contextTokens > 0) {
19802
19702
  setCurrentContextTokens(input.sessionID, contextTokens);
19803
- await maybeAutoSummarizeSession(client, directory, input);
19804
19703
  }
19805
19704
  },
19806
19705
  async "tool.execute.before"(input, output) {
@@ -19816,6 +19715,11 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19816
19715
  throw new Error(buildTextEditSessionRoutingMessage(filePath));
19817
19716
  }
19818
19717
  const resolvedFilePath = resolveCandidatePath(filePath, { directory, worktree });
19718
+ if (CREATE_ROUTED_TEXT_TOOL_IDS.has(input.tool) && !await doesPathExist(resolvedFilePath)) {
19719
+ if (isNewTxtFilePath(resolvedFilePath)) {
19720
+ throw new Error(buildNewTxtRoutingMessage(filePath));
19721
+ }
19722
+ }
19819
19723
  const remembered = await getRememberedGbkEncoding(resolvedFilePath);
19820
19724
  if (remembered) {
19821
19725
  throw new Error(buildGbkRoutingMessage(filePath, remembered.encoding));
@@ -19840,7 +19744,6 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19840
19744
  const maxOutputChars = getMaxOutputChars(input.sessionID);
19841
19745
  const compactionCount = getSessionCompactionCount(input.sessionID);
19842
19746
  const sessionPressure = getSessionPressure(input.sessionID);
19843
- const autoSummarizeCount = getAutoSummarizeCount(input.sessionID);
19844
19747
  const metadata = output.metadata && typeof output.metadata === "object" ? { ...output.metadata } : {};
19845
19748
  const nextOutput = truncateToolOutput(output.output, input.sessionID);
19846
19749
  if (nextOutput !== output.output) {
@@ -19869,9 +19772,6 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19869
19772
  metadata.estimatedSessionTokens = sessionPressure.estimatedTokens;
19870
19773
  metadata.sessionPressureRatio = Number(sessionPressure.pressureRatio.toFixed(3));
19871
19774
  }
19872
- if (autoSummarizeCount > 0) {
19873
- metadata.autoSummarizeCount = autoSummarizeCount;
19874
- }
19875
19775
  output.metadata = metadata;
19876
19776
  },
19877
19777
  async "experimental.chat.system.transform"(_input, output) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "manifestVersion": 1,
3
3
  "packageName": "opencode-gbk-tools",
4
- "packageVersion": "0.1.30",
4
+ "packageVersion": "1.1.1",
5
5
  "artifacts": [
6
6
  {
7
7
  "relativePath": "plugins/opencode-gbk-tools.js",
8
8
  "kind": "plugin",
9
- "expectedHash": "ce0215b44f5166b283e083969533307a4fc54f62e594dc11c1ca669d6c9b495e",
9
+ "expectedHash": "d58d1daf90890e6a40651356aee8d1d8f0369c0b6f4a7a910b519e90a53bcf41",
10
10
  "hashAlgorithm": "sha256"
11
11
  },
12
12
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-gbk-tools",
3
- "version": "0.1.31",
3
+ "version": "1.1.1",
4
4
  "description": "Auto-encoding text tools plus GBK/GB18030 tools for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin/index.js",