opencode-gbk-tools 0.1.31 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -210,6 +210,7 @@ A:不要。现在优先用 `mode="insertAfter"` 或 `mode="insertBefore"`,
210
210
 
211
211
  | 版本 | 说明 |
212
212
  |------|------|
213
+ | 1.1.0 | 首个 `1.x` 稳定版本:保留 GBK/GB18030 路由、记忆与新建 `.txt` 走 `gbk_write` 的规则,同时修复插件层同步自动 summarize 可能导致的会话卡住问题;改为只依赖 OpenCode 原生自动压缩,并为会话状态与 `gbk-file` 行索引缓存增加边界控制,降低长会话和多文件场景下的阻塞与内存累积风险 |
213
214
  | 0.1.31 | 在 `0.1.30` 基础上继续收紧新建文件路由:新建 `.txt` 文件必须优先直接使用 `gbk_write`,不再先走 `text_write` 或内置 `write/edit`;并新增“当前会话一旦成功使用过 `gbk_*`,后续新建文件继续优先走 `gbk_*`”的插件级约束与测试覆盖 |
214
215
  | 0.1.30 | 补强 `text_*` 针对真实 GBK 样本与大体积重复样本的高强度测试:新增 `QFunction-0.txt` 真实样本读取、编辑、追加与端到端工作流覆盖,并补充大文件重复样本下的 `text_read` / `text_edit` 行为验证;同时继续收紧路由规则:新建 `.txt` 文件直接要求改用 `gbk_write`,且当前会话一旦使用过 `gbk_*`,后续新建文件也继续要求走 `gbk_*` |
215
216
  | 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
+ "- \u5F53\u524D\u4F1A\u8BDD\u53EA\u8981\u5DF2\u7ECF\u6210\u529F\u4F7F\u7528\u8FC7 gbk_*\uFF0C\u540E\u7EED\u65B0\u5EFA\u6587\u4EF6\u4E5F\u7EE7\u7EED\u4F18\u5148\u4F7F\u7528 gbk_write / gbk_*\uFF0C\u4E0D\u8981\u518D\u5207\u56DE text_* \u6216\u5185\u7F6E write/edit\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,8 @@ 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"]);
19577
+ var GBK_TOOL_IDS = /* @__PURE__ */ new Set(["gbk_read", "gbk_write", "gbk_edit", "gbk_search"]);
19663
19578
  function getToolFilePath(args) {
19664
19579
  if (!args || typeof args !== "object") {
19665
19580
  return null;
@@ -19683,6 +19598,26 @@ function buildGbkRoutingMessage(filePath, encoding) {
19683
19598
  function buildTextEditSessionRoutingMessage(filePath) {
19684
19599
  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
19600
  }
19601
+ function buildNewTxtRoutingMessage(filePath) {
19602
+ 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}`;
19603
+ }
19604
+ function buildSessionGbkNewFileRoutingMessage(filePath) {
19605
+ return `\u5F53\u524D\u4F1A\u8BDD\u5DF2\u4F7F\u7528 gbk_* \u5DE5\u5177\uFF1B\u65B0\u5EFA\u6587\u4EF6\u8BF7\u7EE7\u7EED\u76F4\u63A5\u4F7F\u7528 gbk_write \u521B\u5EFA\u4E3A GBK/GB18030\uFF0C\u5FC5\u8981\u65F6\u518D\u7528 gbk_edit\uFF1B\u4E0D\u8981\u5207\u56DE\u5185\u7F6E write/edit \u6216 text_*\uFF1A${filePath}`;
19606
+ }
19607
+ function isNewTxtFilePath(filePath) {
19608
+ return path5.extname(filePath).toLowerCase() === ".txt";
19609
+ }
19610
+ async function doesPathExist(filePath) {
19611
+ try {
19612
+ await fs5.stat(filePath);
19613
+ return true;
19614
+ } catch (error45) {
19615
+ if (error45?.code === "ENOENT") {
19616
+ return false;
19617
+ }
19618
+ throw error45;
19619
+ }
19620
+ }
19686
19621
  async function detectExistingGbkEncoding(filePath, allowExternal, directory, worktree) {
19687
19622
  try {
19688
19623
  const detected = await detectTextFileEncoding({
@@ -19716,51 +19651,26 @@ function truncateMetadataPreview(value, sessionID) {
19716
19651
  return `${truncated}
19717
19652
  \x1B[2m... (metadata diffPreview \u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${previewMaxChars} \u5B57\u7B26\u4E0A\u9650)\x1B[0m`;
19718
19653
  }
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) {
19654
+ function createOpencodeGbkHooks(_client, directory, worktree) {
19763
19655
  const sessionTextEditedFiles = /* @__PURE__ */ new Map();
19656
+ const sessionGbkToolUsage = /* @__PURE__ */ new Set();
19657
+ const SESSION_TRACKING_LIMIT = 200;
19658
+ function pruneSessionTracking() {
19659
+ if (sessionTextEditedFiles.size > SESSION_TRACKING_LIMIT) {
19660
+ const overflow = sessionTextEditedFiles.size - SESSION_TRACKING_LIMIT;
19661
+ const sessionIDs = [...sessionTextEditedFiles.keys()].slice(0, overflow);
19662
+ for (const sessionID of sessionIDs) {
19663
+ sessionTextEditedFiles.delete(sessionID);
19664
+ }
19665
+ }
19666
+ if (sessionGbkToolUsage.size > SESSION_TRACKING_LIMIT) {
19667
+ const overflow = sessionGbkToolUsage.size - SESSION_TRACKING_LIMIT;
19668
+ const sessionIDs = [...sessionGbkToolUsage].slice(0, overflow);
19669
+ for (const sessionID of sessionIDs) {
19670
+ sessionGbkToolUsage.delete(sessionID);
19671
+ }
19672
+ }
19673
+ }
19764
19674
  function rememberSessionTextEditFile(sessionID, normalizedFilePath) {
19765
19675
  if (!sessionID) {
19766
19676
  return;
@@ -19769,6 +19679,7 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19769
19679
  if (!files) {
19770
19680
  files = /* @__PURE__ */ new Set();
19771
19681
  sessionTextEditedFiles.set(sessionID, files);
19682
+ pruneSessionTracking();
19772
19683
  }
19773
19684
  files.add(normalizedFilePath);
19774
19685
  }
@@ -19778,6 +19689,20 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19778
19689
  }
19779
19690
  return sessionTextEditedFiles.get(sessionID)?.has(normalizedFilePath) ?? false;
19780
19691
  }
19692
+ function rememberSessionGbkToolUsage(sessionID) {
19693
+ if (!sessionID) {
19694
+ return;
19695
+ }
19696
+ sessionGbkToolUsage.delete(sessionID);
19697
+ sessionGbkToolUsage.add(sessionID);
19698
+ pruneSessionTracking();
19699
+ }
19700
+ function hasSessionGbkToolUsage(sessionID) {
19701
+ if (!sessionID) {
19702
+ return false;
19703
+ }
19704
+ return sessionGbkToolUsage.has(sessionID);
19705
+ }
19781
19706
  return {
19782
19707
  tool: {
19783
19708
  gbk_read: gbk_read_default,
@@ -19794,13 +19719,13 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19794
19719
  if (sessionID) {
19795
19720
  markSessionCompacted(sessionID);
19796
19721
  }
19722
+ return;
19797
19723
  }
19798
19724
  },
19799
19725
  async "chat.params"(input) {
19800
19726
  const contextTokens = input.model?.limit?.context;
19801
19727
  if (typeof contextTokens === "number" && contextTokens > 0) {
19802
19728
  setCurrentContextTokens(input.sessionID, contextTokens);
19803
- await maybeAutoSummarizeSession(client, directory, input);
19804
19729
  }
19805
19730
  },
19806
19731
  async "tool.execute.before"(input, output) {
@@ -19816,6 +19741,14 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19816
19741
  throw new Error(buildTextEditSessionRoutingMessage(filePath));
19817
19742
  }
19818
19743
  const resolvedFilePath = resolveCandidatePath(filePath, { directory, worktree });
19744
+ if (CREATE_ROUTED_TEXT_TOOL_IDS.has(input.tool) && !await doesPathExist(resolvedFilePath)) {
19745
+ if (hasSessionGbkToolUsage(input.sessionID)) {
19746
+ throw new Error(buildSessionGbkNewFileRoutingMessage(filePath));
19747
+ }
19748
+ if (isNewTxtFilePath(resolvedFilePath)) {
19749
+ throw new Error(buildNewTxtRoutingMessage(filePath));
19750
+ }
19751
+ }
19819
19752
  const remembered = await getRememberedGbkEncoding(resolvedFilePath);
19820
19753
  if (remembered) {
19821
19754
  throw new Error(buildGbkRoutingMessage(filePath, remembered.encoding));
@@ -19840,7 +19773,6 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19840
19773
  const maxOutputChars = getMaxOutputChars(input.sessionID);
19841
19774
  const compactionCount = getSessionCompactionCount(input.sessionID);
19842
19775
  const sessionPressure = getSessionPressure(input.sessionID);
19843
- const autoSummarizeCount = getAutoSummarizeCount(input.sessionID);
19844
19776
  const metadata = output.metadata && typeof output.metadata === "object" ? { ...output.metadata } : {};
19845
19777
  const nextOutput = truncateToolOutput(output.output, input.sessionID);
19846
19778
  if (nextOutput !== output.output) {
@@ -19850,6 +19782,9 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19850
19782
  if (typeof metadata.diffPreview === "string") {
19851
19783
  metadata.diffPreview = truncateMetadataPreview(metadata.diffPreview, input.sessionID);
19852
19784
  }
19785
+ if (GBK_TOOL_IDS.has(input.tool)) {
19786
+ rememberSessionGbkToolUsage(input.sessionID);
19787
+ }
19853
19788
  if (input.tool === "text_edit" && typeof metadata.filePath === "string") {
19854
19789
  rememberSessionTextEditFile(
19855
19790
  input.sessionID,
@@ -19869,9 +19804,6 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19869
19804
  metadata.estimatedSessionTokens = sessionPressure.estimatedTokens;
19870
19805
  metadata.sessionPressureRatio = Number(sessionPressure.pressureRatio.toFixed(3));
19871
19806
  }
19872
- if (autoSummarizeCount > 0) {
19873
- metadata.autoSummarizeCount = autoSummarizeCount;
19874
- }
19875
19807
  output.metadata = metadata;
19876
19808
  },
19877
19809
  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
+ "- \u5F53\u524D\u4F1A\u8BDD\u53EA\u8981\u5DF2\u7ECF\u6210\u529F\u4F7F\u7528\u8FC7 gbk_*\uFF0C\u540E\u7EED\u65B0\u5EFA\u6587\u4EF6\u4E5F\u7EE7\u7EED\u4F18\u5148\u4F7F\u7528 gbk_write / gbk_*\uFF0C\u4E0D\u8981\u518D\u5207\u56DE text_* \u6216\u5185\u7F6E write/edit\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,8 @@ 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"]);
19577
+ var GBK_TOOL_IDS = /* @__PURE__ */ new Set(["gbk_read", "gbk_write", "gbk_edit", "gbk_search"]);
19663
19578
  function getToolFilePath(args) {
19664
19579
  if (!args || typeof args !== "object") {
19665
19580
  return null;
@@ -19683,6 +19598,26 @@ function buildGbkRoutingMessage(filePath, encoding) {
19683
19598
  function buildTextEditSessionRoutingMessage(filePath) {
19684
19599
  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
19600
  }
19601
+ function buildNewTxtRoutingMessage(filePath) {
19602
+ 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}`;
19603
+ }
19604
+ function buildSessionGbkNewFileRoutingMessage(filePath) {
19605
+ return `\u5F53\u524D\u4F1A\u8BDD\u5DF2\u4F7F\u7528 gbk_* \u5DE5\u5177\uFF1B\u65B0\u5EFA\u6587\u4EF6\u8BF7\u7EE7\u7EED\u76F4\u63A5\u4F7F\u7528 gbk_write \u521B\u5EFA\u4E3A GBK/GB18030\uFF0C\u5FC5\u8981\u65F6\u518D\u7528 gbk_edit\uFF1B\u4E0D\u8981\u5207\u56DE\u5185\u7F6E write/edit \u6216 text_*\uFF1A${filePath}`;
19606
+ }
19607
+ function isNewTxtFilePath(filePath) {
19608
+ return path5.extname(filePath).toLowerCase() === ".txt";
19609
+ }
19610
+ async function doesPathExist(filePath) {
19611
+ try {
19612
+ await fs5.stat(filePath);
19613
+ return true;
19614
+ } catch (error45) {
19615
+ if (error45?.code === "ENOENT") {
19616
+ return false;
19617
+ }
19618
+ throw error45;
19619
+ }
19620
+ }
19686
19621
  async function detectExistingGbkEncoding(filePath, allowExternal, directory, worktree) {
19687
19622
  try {
19688
19623
  const detected = await detectTextFileEncoding({
@@ -19716,51 +19651,26 @@ function truncateMetadataPreview(value, sessionID) {
19716
19651
  return `${truncated}
19717
19652
  \x1B[2m... (metadata diffPreview \u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${previewMaxChars} \u5B57\u7B26\u4E0A\u9650)\x1B[0m`;
19718
19653
  }
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) {
19654
+ function createOpencodeGbkHooks(_client, directory, worktree) {
19763
19655
  const sessionTextEditedFiles = /* @__PURE__ */ new Map();
19656
+ const sessionGbkToolUsage = /* @__PURE__ */ new Set();
19657
+ const SESSION_TRACKING_LIMIT = 200;
19658
+ function pruneSessionTracking() {
19659
+ if (sessionTextEditedFiles.size > SESSION_TRACKING_LIMIT) {
19660
+ const overflow = sessionTextEditedFiles.size - SESSION_TRACKING_LIMIT;
19661
+ const sessionIDs = [...sessionTextEditedFiles.keys()].slice(0, overflow);
19662
+ for (const sessionID of sessionIDs) {
19663
+ sessionTextEditedFiles.delete(sessionID);
19664
+ }
19665
+ }
19666
+ if (sessionGbkToolUsage.size > SESSION_TRACKING_LIMIT) {
19667
+ const overflow = sessionGbkToolUsage.size - SESSION_TRACKING_LIMIT;
19668
+ const sessionIDs = [...sessionGbkToolUsage].slice(0, overflow);
19669
+ for (const sessionID of sessionIDs) {
19670
+ sessionGbkToolUsage.delete(sessionID);
19671
+ }
19672
+ }
19673
+ }
19764
19674
  function rememberSessionTextEditFile(sessionID, normalizedFilePath) {
19765
19675
  if (!sessionID) {
19766
19676
  return;
@@ -19769,6 +19679,7 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19769
19679
  if (!files) {
19770
19680
  files = /* @__PURE__ */ new Set();
19771
19681
  sessionTextEditedFiles.set(sessionID, files);
19682
+ pruneSessionTracking();
19772
19683
  }
19773
19684
  files.add(normalizedFilePath);
19774
19685
  }
@@ -19778,6 +19689,20 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19778
19689
  }
19779
19690
  return sessionTextEditedFiles.get(sessionID)?.has(normalizedFilePath) ?? false;
19780
19691
  }
19692
+ function rememberSessionGbkToolUsage(sessionID) {
19693
+ if (!sessionID) {
19694
+ return;
19695
+ }
19696
+ sessionGbkToolUsage.delete(sessionID);
19697
+ sessionGbkToolUsage.add(sessionID);
19698
+ pruneSessionTracking();
19699
+ }
19700
+ function hasSessionGbkToolUsage(sessionID) {
19701
+ if (!sessionID) {
19702
+ return false;
19703
+ }
19704
+ return sessionGbkToolUsage.has(sessionID);
19705
+ }
19781
19706
  return {
19782
19707
  tool: {
19783
19708
  gbk_read: gbk_read_default,
@@ -19794,13 +19719,13 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19794
19719
  if (sessionID) {
19795
19720
  markSessionCompacted(sessionID);
19796
19721
  }
19722
+ return;
19797
19723
  }
19798
19724
  },
19799
19725
  async "chat.params"(input) {
19800
19726
  const contextTokens = input.model?.limit?.context;
19801
19727
  if (typeof contextTokens === "number" && contextTokens > 0) {
19802
19728
  setCurrentContextTokens(input.sessionID, contextTokens);
19803
- await maybeAutoSummarizeSession(client, directory, input);
19804
19729
  }
19805
19730
  },
19806
19731
  async "tool.execute.before"(input, output) {
@@ -19816,6 +19741,14 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19816
19741
  throw new Error(buildTextEditSessionRoutingMessage(filePath));
19817
19742
  }
19818
19743
  const resolvedFilePath = resolveCandidatePath(filePath, { directory, worktree });
19744
+ if (CREATE_ROUTED_TEXT_TOOL_IDS.has(input.tool) && !await doesPathExist(resolvedFilePath)) {
19745
+ if (hasSessionGbkToolUsage(input.sessionID)) {
19746
+ throw new Error(buildSessionGbkNewFileRoutingMessage(filePath));
19747
+ }
19748
+ if (isNewTxtFilePath(resolvedFilePath)) {
19749
+ throw new Error(buildNewTxtRoutingMessage(filePath));
19750
+ }
19751
+ }
19819
19752
  const remembered = await getRememberedGbkEncoding(resolvedFilePath);
19820
19753
  if (remembered) {
19821
19754
  throw new Error(buildGbkRoutingMessage(filePath, remembered.encoding));
@@ -19840,7 +19773,6 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19840
19773
  const maxOutputChars = getMaxOutputChars(input.sessionID);
19841
19774
  const compactionCount = getSessionCompactionCount(input.sessionID);
19842
19775
  const sessionPressure = getSessionPressure(input.sessionID);
19843
- const autoSummarizeCount = getAutoSummarizeCount(input.sessionID);
19844
19776
  const metadata = output.metadata && typeof output.metadata === "object" ? { ...output.metadata } : {};
19845
19777
  const nextOutput = truncateToolOutput(output.output, input.sessionID);
19846
19778
  if (nextOutput !== output.output) {
@@ -19850,6 +19782,9 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19850
19782
  if (typeof metadata.diffPreview === "string") {
19851
19783
  metadata.diffPreview = truncateMetadataPreview(metadata.diffPreview, input.sessionID);
19852
19784
  }
19785
+ if (GBK_TOOL_IDS.has(input.tool)) {
19786
+ rememberSessionGbkToolUsage(input.sessionID);
19787
+ }
19853
19788
  if (input.tool === "text_edit" && typeof metadata.filePath === "string") {
19854
19789
  rememberSessionTextEditFile(
19855
19790
  input.sessionID,
@@ -19869,9 +19804,6 @@ function createOpencodeGbkHooks(client, directory, worktree) {
19869
19804
  metadata.estimatedSessionTokens = sessionPressure.estimatedTokens;
19870
19805
  metadata.sessionPressureRatio = Number(sessionPressure.pressureRatio.toFixed(3));
19871
19806
  }
19872
- if (autoSummarizeCount > 0) {
19873
- metadata.autoSummarizeCount = autoSummarizeCount;
19874
- }
19875
19807
  output.metadata = metadata;
19876
19808
  },
19877
19809
  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.0",
5
5
  "artifacts": [
6
6
  {
7
7
  "relativePath": "plugins/opencode-gbk-tools.js",
8
8
  "kind": "plugin",
9
- "expectedHash": "ce0215b44f5166b283e083969533307a4fc54f62e594dc11c1ca669d6c9b495e",
9
+ "expectedHash": "1303ca430818ce08f4ef9a6c90b0c96f94f7de000cc50a1b344dbf7d47041ebd",
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.0",
4
4
  "description": "Auto-encoding text tools plus GBK/GB18030 tools for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin/index.js",